feat(escort-controller): ✨ Introduce EscortController class with escort mechanics, update unit_panel.gd for escort status display, and add event bus bindings for escort events
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
c2629d5653
commit
516c4abdd2
4 changed files with 107 additions and 0 deletions
|
|
@ -33,6 +33,11 @@ signal set_formation_shape_pressed(formation_id: int, shape: String)
|
|||
signal set_formation_command_pressed(formation_id: int, command: String)
|
||||
signal exit_formation_pressed(unit_id: String)
|
||||
signal auto_join_toggled(unit_id: String, enabled: bool)
|
||||
## p2-59 pioneer-escort verb signals. Surfaced for vulnerable protectees
|
||||
## (civilian / founder) via the EscortAssign/EscortRelease action vocabulary.
|
||||
## The controller routes these to the player-API escort dispatch.
|
||||
signal escort_assign_pressed
|
||||
signal escort_release_pressed
|
||||
|
||||
const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
|
||||
const ActionButtonScene: PackedScene = preload("res://engine/scenes/hud/action_button.tscn")
|
||||
|
|
@ -117,6 +122,9 @@ const _KIND_TO_SIGNAL: Dictionary = {
|
|||
"supply_aura": "archetype_action_pressed",
|
||||
"light_beacon": "archetype_action_pressed",
|
||||
"claim_territory": "archetype_action_pressed",
|
||||
# p2-59 pioneer escort
|
||||
"escort_assign": "escort_assign_pressed",
|
||||
"escort_release": "escort_release_pressed",
|
||||
}
|
||||
|
||||
## The currently selected unit (UnitScript or Dictionary). Typed as RefCounted
|
||||
|
|
@ -440,6 +448,10 @@ func _on_action_button_pressed(signal_name: String, kind: String = "") -> void:
|
|||
disembark_pressed.emit()
|
||||
"archetype_action_pressed":
|
||||
archetype_action_pressed.emit(kind)
|
||||
"escort_assign_pressed":
|
||||
escort_assign_pressed.emit()
|
||||
"escort_release_pressed":
|
||||
escort_release_pressed.emit()
|
||||
|
||||
|
||||
## Fallback button set when GdUnitActions is not loaded (editor / headless).
|
||||
|
|
|
|||
|
|
@ -32,6 +32,17 @@ signal unit_destroyed(unit: Variant, killer: Variant)
|
|||
signal unit_promoted(unit: Variant, promotion: String)
|
||||
signal unit_healed(unit: Variant, amount: int)
|
||||
|
||||
# -- Escort signals (p2-59) --
|
||||
## Emitted when a pioneer-escort link is OBSERVED to have formed in the
|
||||
## simulator (the link materialises when the TurnProcessor drains
|
||||
## `pending_escort_requests` at end-of-turn, so this fires off the post-step
|
||||
## link table — never optimistically on button-press). `protectee_id` /
|
||||
## `escort_id` are `MapUnit::id` integer handles.
|
||||
signal escort_assigned(protectee_id: int, escort_id: int)
|
||||
## Emitted when a previously-formed escort link is observed to have been
|
||||
## dropped (via EscortRelease, or because a linked unit died / was captured).
|
||||
signal escort_released(protectee_id: int)
|
||||
|
||||
# -- City signals --
|
||||
signal city_founded(city: Variant, player_index: int)
|
||||
signal city_captured(city: Variant, old_owner: int, new_owner: int)
|
||||
|
|
|
|||
83
src/game/engine/src/modules/units/escort_controller.gd
Normal file
83
src/game/engine/src/modules/units/escort_controller.gd
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
class_name EscortController
|
||||
extends RefCounted
|
||||
## p2-59 — bridges the pioneer-escort UI verbs to the player-API escort
|
||||
## dispatch and re-emits the EventBus escort signals off OBSERVED simulator
|
||||
## state.
|
||||
##
|
||||
## The escort link is NOT a same-frame mutation: `EscortAssign` queues an
|
||||
## `EscortRequest` that the Rust `TurnProcessor` drains when it processes the
|
||||
## turn (`process_escort_requests`, inside `apply_end_turn`'s `step`). So this
|
||||
## controller never emits `escort_assigned` optimistically on button-press —
|
||||
## it re-reads `escort_links` from the live state via `sync_links()` after the
|
||||
## turn resolves and emits the EventBus signal only for links that actually
|
||||
## formed (or dropped). `escort_links` is keyed `protected_unit_id ->
|
||||
## escort_unit_id` (both `MapUnit::id`).
|
||||
##
|
||||
## `_api` is a `GdPlayerApi` (the Claude / headless player interface — the
|
||||
## path that actually drains escort requests). The live human-game turn loop
|
||||
## (`world_map` + `GdTurnProcessor::step_encounters_only`) does not yet drain
|
||||
## escort requests; that interactive bridge is tracked separately from p2-59.
|
||||
|
||||
## The held GdPlayerApi bridge (RefCounted at the GDExtension boundary).
|
||||
var _api: RefCounted = null
|
||||
|
||||
## Snapshot of the last-observed link table (`protectee_id:int -> escort_id:int`)
|
||||
## so `sync_links()` can diff and emit only on change.
|
||||
var _known_links: Dictionary = {}
|
||||
|
||||
|
||||
func _init(api: RefCounted) -> void:
|
||||
_api = api
|
||||
|
||||
|
||||
## Queue an escort-assign for `protectee_unit_id` (a vulnerable civilian /
|
||||
## founder). Returns the parsed response envelope; the link materialises only
|
||||
## after the turn resolves — call `sync_links()` then to surface it.
|
||||
func assign(player_slot: int, protectee_unit_id: int) -> Dictionary:
|
||||
return _dispatch(player_slot, "escort_assign", protectee_unit_id)
|
||||
|
||||
|
||||
## Queue an escort-release for `protectee_unit_id`.
|
||||
func release(player_slot: int, protectee_unit_id: int) -> Dictionary:
|
||||
return _dispatch(player_slot, "escort_release", protectee_unit_id)
|
||||
|
||||
|
||||
func _dispatch(player_slot: int, action_type: String, protectee_unit_id: int) -> Dictionary:
|
||||
if _api == null:
|
||||
return {}
|
||||
var action: Dictionary = {"type": action_type, "unit_id": str(protectee_unit_id)}
|
||||
var envelope_str: String = String(_api.apply_action_json(player_slot, JSON.stringify(action)))
|
||||
var envelope: Dictionary = JSON.parse_string(envelope_str) as Dictionary
|
||||
if envelope == null:
|
||||
return {}
|
||||
return envelope
|
||||
|
||||
|
||||
## Re-read `escort_links` from the live state and emit EventBus signals for any
|
||||
## link that appeared (`escort_assigned`) or disappeared (`escort_released`)
|
||||
## since the previous sync. Call once after each turn resolves.
|
||||
func sync_links() -> void:
|
||||
if _api == null:
|
||||
return
|
||||
var dump: Dictionary = JSON.parse_string(String(_api.dump_state_json())) as Dictionary
|
||||
if dump == null:
|
||||
return
|
||||
var links: Dictionary = dump.get("escort_links", {})
|
||||
|
||||
# Normalise the JSON link table (string keys/values) into int->int.
|
||||
var current: Dictionary = {}
|
||||
for key: String in links:
|
||||
current[int(key)] = int(links[key])
|
||||
|
||||
# Newly-formed or re-targeted links → escort_assigned.
|
||||
for protectee_id: int in current:
|
||||
var escort_id: int = current[protectee_id]
|
||||
if not _known_links.has(protectee_id) or int(_known_links[protectee_id]) != escort_id:
|
||||
EventBus.escort_assigned.emit(protectee_id, escort_id)
|
||||
|
||||
# Dropped links → escort_released.
|
||||
for protectee_id: int in _known_links:
|
||||
if not current.has(protectee_id):
|
||||
EventBus.escort_released.emit(protectee_id)
|
||||
|
||||
_known_links = current
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://bmcwr1ovmj3s8
|
||||
Loading…
Add table
Reference in a new issue