diff --git a/config/keybindings.conf b/config/keybindings.conf index 52f3abc..6f8af3b 100644 --- a/config/keybindings.conf +++ b/config/keybindings.conf @@ -19,6 +19,8 @@ toggle_dev_mode = F1 dev_cycle_spawn = F2 dev_spawn_object = F3 dev_spawn_npc_menu = F4 +dev_cycle_turret = F5 +dev_place_turret = F6 pickup = f [hands] diff --git a/engine/config.py b/engine/config.py index d213e79..856a3dc 100644 --- a/engine/config.py +++ b/engine/config.py @@ -34,6 +34,8 @@ DEFAULT_KEYBINDINGS: dict[str, list[str]] = { "dev_cycle_spawn": ["F2"], "dev_spawn_object": ["F3"], "dev_spawn_npc_menu": ["F4"], + "dev_cycle_turret": ["F5"], + "dev_place_turret": ["F6"], } # Human-friendly config names for keys that don't have a printable representation. diff --git a/engine/dev_tools.py b/engine/dev_tools.py index b70ee19..e675453 100644 --- a/engine/dev_tools.py +++ b/engine/dev_tools.py @@ -52,3 +52,32 @@ class DevSpawnMenu: return False instance = ItemInstance(f"dev-spawn-{uuid.uuid4().hex[:8]}", item_id) return character.inventory.place(instance, item_def, *slot) + + +@dataclass +class DevTurretMenu: + """Dev-mode-only cheat tool: cycle through every turret entity template (see entities.json's + turret_pistol/turret_autocannon - any template with turret_weapon_item_id set, so a new + variant only needs adding to entities.json, not here) and place the selected one into the + world - see main.py's dev_place_turret handling. + """ + + registry: DefRegistry + entity_def_ids: list[str] = field(default_factory=list) + selected_index: int = 0 + + def __post_init__(self) -> None: + if not self.entity_def_ids: + self.entity_def_ids = sorted( + def_id for def_id, entity_def in self.registry.entity_defs.items() + if entity_def.turret_weapon_item_id is not None + ) + + def cycle(self, delta: int) -> None: + if self.entity_def_ids: + self.selected_index = (self.selected_index + delta) % len(self.entity_def_ids) + + def selected_entity_def_id(self) -> str | None: + if not self.entity_def_ids: + return None + return self.entity_def_ids[self.selected_index] diff --git a/main.py b/main.py index 31e8128..b758339 100644 --- a/main.py +++ b/main.py @@ -17,7 +17,7 @@ from engine.controls_menu import ControlsMenu from engine.db import WorldRepository from engine.defs import DefRegistry from engine.dev_config import DevConfig -from engine.dev_tools import DevSpawnMenu +from engine.dev_tools import DevSpawnMenu, DevTurretMenu from engine.entity import EntityPosition from engine.gamepad import GamepadBindings, GamepadButtonEdgeTracker, GamepadState from engine.game_modes import CharacterCreationFlow, MainMenu, Mode, PauseMenu @@ -63,6 +63,7 @@ from resources.logic.organs import apply_organ_interactions_tick from resources.logic.pickup import drop_item, try_pickup from resources.logic.steering import advance_all_ship_rotations, at_helm, try_steer_ship from resources.logic.swimming import in_water, try_swim_vertical +from resources.logic.turrets import spawn_turret ROOT_DIR = Path(__file__).resolve().parent DEFS_DIR = ROOT_DIR / "resources" / "defs" @@ -145,6 +146,7 @@ class AppState: self.dragging_item: tuple[str, str] | None = None # (item_id, def_id) picked up off the inventory grid self.ability_bar = AbilityBar() self.dev_spawn_menu = DevSpawnMenu(registry) + self.dev_turret_menu = DevTurretMenu(registry) def main() -> None: @@ -408,6 +410,23 @@ def main() -> None: state.npc_spawn_flow = CharacterCreationFlow(CharacterCreator(registry, selectable_species=NPC_SELECTABLE_SPECIES)) state.mode = Mode.NPC_SPAWN return + if dev_config.enabled and action == "dev_cycle_turret": + state.dev_turret_menu.cycle(1) + return + if dev_config.enabled and action == "dev_place_turret": + player = state.world.player if state.world is not None else None + entity_def_id = state.dev_turret_menu.selected_entity_def_id() + if isinstance(player, Character) and player.position is not None and entity_def_id is not None and state.world is not None: + dx, dy, dz = current_aim_direction(player) + position = EntityPosition( + player.position.map_id, player.position.x + dx, player.position.y + dy, player.position.z + dz + ) + # hostile to the placing player by default - a dev/debug placement is meant to + # be immediately testable (see it track and fire), not require setting up + # faction tags first; see resources/logic/turrets.py::is_hostile. + turret = spawn_turret(registry, entity_def_id, position, hostile_entity_ids=(player.entity_id,)) + state.world.add_entity(turret) + return input_state.handle_event(event) def handle_character_card_pointer_event(event_type: str, event: dict) -> bool: @@ -632,6 +651,7 @@ def main() -> None: if dev_config.enabled: draw_text(ui_buckets, "DEV MODE", viewport_w - 130, 10) draw_text(ui_buckets, f"spawn: {state.dev_spawn_menu.selected_item_id()}", viewport_w - 260, 30) + draw_text(ui_buckets, f"turret: {state.dev_turret_menu.selected_entity_def_id()}", viewport_w - 260, 50) renderer.render_frame(context, world, camera, ui_buckets) return diff --git a/tests/test_dev_tools.py b/tests/test_dev_tools.py index 3476a3e..d85a26c 100644 --- a/tests/test_dev_tools.py +++ b/tests/test_dev_tools.py @@ -5,7 +5,7 @@ import pytest from engine.character import Character from engine.defs import DefRegistry from engine.dev_config import DevConfig -from engine.dev_tools import DevSpawnMenu +from engine.dev_tools import DevSpawnMenu, DevTurretMenu from engine.inventory import Inventory DEFS_DIR = Path(__file__).resolve().parent.parent / "resources" / "defs" @@ -66,6 +66,34 @@ def test_spawn_into_fails_when_inventory_is_full(registry): assert menu.spawn_into(character) is False +# --- DevTurretMenu -------------------------------------------------------------------------------- + + +def test_turret_menu_defaults_to_every_turret_entity_def_sorted(registry): + menu = DevTurretMenu(registry) + assert menu.entity_def_ids == ["turret_autocannon", "turret_pistol"] + assert menu.selected_entity_def_id() == "turret_autocannon" + + +def test_turret_menu_excludes_non_turret_entity_defs(registry): + menu = DevTurretMenu(registry) + assert "player" not in menu.entity_def_ids + assert "npc_reptilian" not in menu.entity_def_ids + assert "companion_dog" not in menu.entity_def_ids + + +def test_turret_menu_cycle_wraps_around(registry): + menu = DevTurretMenu(registry) + menu.cycle(-1) + assert menu.selected_entity_def_id() == menu.entity_def_ids[-1] + + +def test_turret_menu_selected_entity_def_id_is_none_when_empty(registry): + menu = DevTurretMenu(registry) + menu.entity_def_ids = [] + assert menu.selected_entity_def_id() is None + + # --- DevConfig ------------------------------------------------------------------------------------