Add dev-mode turret placement

F5 cycles the turret variant (pistol/autocannon), F6 places the selected one
in front of the player, hostile to them by default so it's immediately
testable. Backed by a new DevTurretMenu mirroring the existing item spawn
menu, sourced generically from any entity template with turret_weapon_item_id
set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyY5qQB6XHRoBFNbsgdRQn
main
Amir Alexander Abdelbaki 2026-07-16 16:48:49 +02:00
parent e04761f7b5
commit 1099183788
5 changed files with 83 additions and 2 deletions

View File

@ -19,6 +19,8 @@ toggle_dev_mode = F1
dev_cycle_spawn = F2 dev_cycle_spawn = F2
dev_spawn_object = F3 dev_spawn_object = F3
dev_spawn_npc_menu = F4 dev_spawn_npc_menu = F4
dev_cycle_turret = F5
dev_place_turret = F6
pickup = f pickup = f
[hands] [hands]

View File

@ -34,6 +34,8 @@ DEFAULT_KEYBINDINGS: dict[str, list[str]] = {
"dev_cycle_spawn": ["F2"], "dev_cycle_spawn": ["F2"],
"dev_spawn_object": ["F3"], "dev_spawn_object": ["F3"],
"dev_spawn_npc_menu": ["F4"], "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. # Human-friendly config names for keys that don't have a printable representation.

View File

@ -52,3 +52,32 @@ class DevSpawnMenu:
return False return False
instance = ItemInstance(f"dev-spawn-{uuid.uuid4().hex[:8]}", item_id) instance = ItemInstance(f"dev-spawn-{uuid.uuid4().hex[:8]}", item_id)
return character.inventory.place(instance, item_def, *slot) 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]

22
main.py
View File

@ -17,7 +17,7 @@ from engine.controls_menu import ControlsMenu
from engine.db import WorldRepository from engine.db import WorldRepository
from engine.defs import DefRegistry from engine.defs import DefRegistry
from engine.dev_config import DevConfig 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.entity import EntityPosition
from engine.gamepad import GamepadBindings, GamepadButtonEdgeTracker, GamepadState from engine.gamepad import GamepadBindings, GamepadButtonEdgeTracker, GamepadState
from engine.game_modes import CharacterCreationFlow, MainMenu, Mode, PauseMenu 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.pickup import drop_item, try_pickup
from resources.logic.steering import advance_all_ship_rotations, at_helm, try_steer_ship 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.swimming import in_water, try_swim_vertical
from resources.logic.turrets import spawn_turret
ROOT_DIR = Path(__file__).resolve().parent ROOT_DIR = Path(__file__).resolve().parent
DEFS_DIR = ROOT_DIR / "resources" / "defs" 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.dragging_item: tuple[str, str] | None = None # (item_id, def_id) picked up off the inventory grid
self.ability_bar = AbilityBar() self.ability_bar = AbilityBar()
self.dev_spawn_menu = DevSpawnMenu(registry) self.dev_spawn_menu = DevSpawnMenu(registry)
self.dev_turret_menu = DevTurretMenu(registry)
def main() -> None: def main() -> None:
@ -408,6 +410,23 @@ def main() -> None:
state.npc_spawn_flow = CharacterCreationFlow(CharacterCreator(registry, selectable_species=NPC_SELECTABLE_SPECIES)) state.npc_spawn_flow = CharacterCreationFlow(CharacterCreator(registry, selectable_species=NPC_SELECTABLE_SPECIES))
state.mode = Mode.NPC_SPAWN state.mode = Mode.NPC_SPAWN
return 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) input_state.handle_event(event)
def handle_character_card_pointer_event(event_type: str, event: dict) -> bool: def handle_character_card_pointer_event(event_type: str, event: dict) -> bool:
@ -632,6 +651,7 @@ def main() -> None:
if dev_config.enabled: if dev_config.enabled:
draw_text(ui_buckets, "DEV MODE", viewport_w - 130, 10) 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"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) renderer.render_frame(context, world, camera, ui_buckets)
return return

View File

@ -5,7 +5,7 @@ import pytest
from engine.character import Character from engine.character import Character
from engine.defs import DefRegistry from engine.defs import DefRegistry
from engine.dev_config import DevConfig from engine.dev_config import DevConfig
from engine.dev_tools import DevSpawnMenu from engine.dev_tools import DevSpawnMenu, DevTurretMenu
from engine.inventory import Inventory from engine.inventory import Inventory
DEFS_DIR = Path(__file__).resolve().parent.parent / "resources" / "defs" 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 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 ------------------------------------------------------------------------------------ # --- DevConfig ------------------------------------------------------------------------------------