155 lines
6.5 KiB
Python
155 lines
6.5 KiB
Python
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
|