Mi2dRPGamEng/resources/logic/actions.py

146 lines
5.9 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_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,
}
ABILITY_COOLDOWNS = {
"time_warp_sphere": 8.0, # seconds; longer than the zone's own default 5s duration
}
def _cast_ability(character: Character, world: World, ability_id: str, now: float):
"""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.
"""
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)
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)
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,
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)
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, now=now)
return None