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