Mi2dRPGamEng/resources/logic/hacking.py

84 lines
3.5 KiB
Python

from __future__ import annotations
import random
from engine.character import Character
from engine.defs import DefRegistry
from engine.item import ItemDef
from engine.tile import TileLayerInstance
from engine.world import World
from resources.logic.combat import SKILL_CHECK_DIE, DiceRoller
TECH_SKILL_ID = "tech"
def find_hacking_implant(character: Character, registry: DefRegistry) -> ItemDef | None:
"""The first installed implant that assists hacking (a cyberdeck - see
ItemDef.is_hacking_implant), if any. Having one both applies its hack_bonus/
hack_min_roll_percent to every tech check (see roll_tech_check) and lets a bare hand
attempt a hack with no wielded hacking_tool at all (see resources/logic/actions.py::
activate_hand) - a character is only ever expected to have one installed at a time.
"""
for item_id in character.implants:
item_def = registry.get_item_def(item_id)
if item_def.is_hacking_implant:
return item_def
return None
def roll_tech_check(character: Character, registry: DefRegistry, rng: DiceRoller) -> tuple[int, float]:
"""Same shape as combat.py::roll_skill_check (1d20 + effective skill, reduced by
total_check_penalty), but tech's base skill is derived (see Character.base_tech_skill)
rather than a directly-bought bio value. An installed hacking implant (see
find_hacking_implant) then adds its own flat hack_bonus and, if it guarantees a minimum
roll quality (hack_min_roll_percent), floors the raw d20 roll at that fraction of
SKILL_CHECK_DIE before it's returned.
"""
base_skill = character.base_tech_skill()
effective_skill = base_skill * (1.0 - character.total_check_penalty(TECH_SKILL_ID, registry))
roll = rng.randint(1, SKILL_CHECK_DIE)
implant = find_hacking_implant(character, registry)
if implant is not None:
effective_skill += implant.hack_bonus
if implant.hack_min_roll_percent is not None:
roll = max(roll, round(implant.hack_min_roll_percent * SKILL_CHECK_DIE))
return roll, effective_skill
def try_hack_tile_layer(
character: Character,
world: World,
registry: DefRegistry,
map_id: str,
x: int,
y: int,
z: int,
layer: str,
rng: DiceRoller | None = None,
) -> bool:
"""A tech skill check against a hackable tile layer (e.g. a locked door - see
TileLayerDef.hack_difficulty/hackable_unlocked_def_id). On success, swaps the layer to its
unlocked def in place - the same "replace the layer's def_id" idiom
resources/logic/construction.py's build/deconstruct actions already use, so every other
system that reads tile layers (walkability, LOS, rendering) understands the new state with
no changes of its own. Returns whether it succeeded - False for a tile with nothing
hackable at that layer, or a failed check (rollable again any time, no cooldown/lockout).
"""
tile = world.maps[map_id].get_tile(x, y, z)
instance = tile.get_layer(layer)
if instance is None:
return False
layer_def = registry.get_tile_layer_def(layer, instance.def_id)
if layer_def is None or layer_def.hack_difficulty is None or layer_def.hackable_unlocked_def_id is None:
return False
rng = rng or random.Random()
roll, effective_skill = roll_tech_check(character, registry, rng)
if roll + effective_skill < layer_def.hack_difficulty:
return False
tile.set_layer(layer, TileLayerInstance(layer_def.hackable_unlocked_def_id))
return True