from __future__ import annotations from engine.character import Character from engine.defs import DefRegistry from engine.ui import AbilityBarSlot from engine.world import World from resources.logic.abilities import cast_launch_lightning, cast_time_warp_sphere from resources.logic.combat import DiceRoller, perform_attack, perform_shoot from resources.logic.construction import interact_with_tile from resources.logic.knockback import resolve_melee_knockback from resources.logic.ranged_combat import fire_held_weapon ABILITY_CASTERS = { "time_warp_sphere": cast_time_warp_sphere, "launch_lightning": cast_launch_lightning, } ABILITY_COOLDOWNS = { "time_warp_sphere": 8.0, # seconds; longer than the zone's own default 5s duration "launch_lightning": 10.0, # a hefty cooldown given how severe a hit it lands } DEFAULT_AIM_DIRECTION = (0, 1, 0) def _cast_ability( character: Character, world: World, ability_id: str, now: float, aim_direction: tuple[int, int, int] = DEFAULT_AIM_DIRECTION ): """Shared by the item-in-hand and implant ability paths: casts `ability_id` if it's off cooldown, then starts its cooldown (see ABILITY_COOLDOWNS) - a no-op returning None while still on cooldown, or if no caster is registered for it. Every ABILITY_CASTERS entry accepts aim_direction (directional abilities like launch_lightning use it; omnidirectional ones like time_warp_sphere just ignore it) so this dispatch stays uniform either way. """ caster = ABILITY_CASTERS.get(ability_id) if caster is None or not character.is_ability_ready(ability_id, now): return None result = caster(character, world, now, aim_direction=aim_direction) character.start_ability_cooldown(ability_id, now, ABILITY_COOLDOWNS.get(ability_id, 0.0)) return result def _first_buildable_layer(character: Character, registry: DefRegistry) -> str | None: if character.inventory is None: return None for placed in character.inventory.items(): material_def = registry.get_item_def(placed.item.def_id) if material_def.builds_tile_layer is not None: return material_def.builds_tile_layer return None def activate_hand( character: Character, world: World, registry: DefRegistry, hand_slot: str, aim_direction: tuple[int, int, int], rng: DiceRoller | None = None, now: float = 0.0, ): """Resolves whatever's held in `hand_slot` against `aim_direction`: a tool interacts with the tile immediately ahead, an ability-granting item casts its ability (`now` is the game clock time, needed for timed effects like time_warp_sphere), an ammo-fed ranged weapon fires (see fire_held_weapon), a simple non-ammo ranged weapon shoots directly, and melee (weapon or bare hand) attacks whoever's standing there - with knockback on a successful hit. Returns whatever the underlying action returned, or None if the hand is empty or nothing applicable is in range. """ assert character.position is not None held = character.get_held_item(hand_slot) dx, dy, dz = aim_direction ahead = (character.position.x + dx, character.position.y + dy, character.position.z + dz) if held is None: target = world.entity_at(character.position.map_id, *ahead) if isinstance(target, Character): result = perform_attack(character, target, registry, weapon_def=None, rng=rng) resolve_melee_knockback(world, character, target, None, result.hit, registry) return result return None item_def = registry.get_item_def(held.item_def_id) if item_def.grants_ability is not None: return _cast_ability(character, world, item_def.grants_ability, now, aim_direction=aim_direction) if item_def.tool_kind is not None: map_ = world.maps[character.position.map_id] if not map_.in_bounds(*ahead): return None button = "left" if hand_slot in ("right_hand", "right_hand_2") else "right" if item_def.tool_kind == "constructor": layer = _first_buildable_layer(character, registry) else: tile = map_.get_tile(*ahead) layer = next((layer for layer in ("room", "flooring", "subfloor", "roof") if tile.get_layer(layer)), None) if layer is None: return None return interact_with_tile(character, world, character.position.map_id, ahead[0], ahead[1], ahead[2], layer, button) if item_def.magazine_size > 0: return fire_held_weapon(character, world, registry, hand_slot, aim_direction, rng=rng) if item_def.weapon_range > 1: return perform_shoot(character, world, registry, item_def, aim_direction, rng=rng) target = world.entity_at(character.position.map_id, *ahead) if isinstance(target, Character): result = perform_attack(character, target, registry, weapon_def=item_def, rng=rng) resolve_melee_knockback(world, character, target, item_def, result.hit, registry) return result return None def activate_implant( character: Character, world: World, registry: DefRegistry, implant_item_def_id: str, aim_direction: tuple[int, int, int] = DEFAULT_AIM_DIRECTION, now: float = 0.0, ): """Casts the ability granted by an installed slotless implant (e.g. a chronoimplant granting time_warp_sphere) - mirrors the item-in-hand ability path in activate_hand, but implants have no hand slot and are checked against character.implants instead. """ if implant_item_def_id not in character.implants: return None item_def = registry.get_item_def(implant_item_def_id) if item_def.grants_ability is None: return None return _cast_ability(character, world, item_def.grants_ability, now, aim_direction=aim_direction) def activate_ability_bar_slot( character: Character, world: World, registry: DefRegistry, slot: AbilityBarSlot, aim_direction: tuple[int, int, int], rng: DiceRoller | None = None, now: float = 0.0, ): """Triggers whatever an ability-bar slot points at, whether that's a wielded hand (dispatches through activate_hand, so a bar slot pointed at a hand behaves identically to clicking that hand directly) or an installed implant (dispatches through activate_implant). """ if slot.kind == "hand": return activate_hand(character, world, registry, slot.source_id, aim_direction, rng=rng, now=now) if slot.kind == "implant": return activate_implant(character, world, registry, slot.source_id, aim_direction=aim_direction, now=now) return None