84 lines
3.5 KiB
Python
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
|