Files
gamedev-the-steward/world/demo/DemoController.gd
T
2026-07-26 22:26:00 +02:00

379 lines
12 KiB
GDScript

extends Node
signal pantry_crisis_staged(helper_id: int, interested_id: int)
signal pantry_crisis_completed(helper_id: int, resolution_event_id: int)
signal pantry_crisis_failed(reason: String)
const FEATURED_HELPER_INDEX := 0
const INTERESTED_VILLAGER_INDEX := 1
const SHORTAGE_ACTOR_INDEX := 2
const VALLEY_ESTABLISH_SECONDS := 2.4
const ESTABLISH_SECONDS := 2.4
const RECOVERY_SECONDS := 5.7
const DEMO_TIMEOUT_SECONDS := 32.0
static var start_pantry_crisis_after_reload := false
@export var ui: CanvasLayer
@export var active_npcs_parent: Node
@export var simulation_manager: Node
@export var world_view_manager: Node
@export var camera_rig: Node3D
@export var player: CharacterBody3D
@export var pantry_storage: StorageNode
@export var crisis_caption: Label
@export var debug_overlay_visible := true
var _refresh_accumulator := 0.0
var _caption_tween: Tween
var _demo_running := false
var _featured_helper_id := -1
var _interested_villager_id := -1
func _ready() -> void:
_apply_debug_overlay_visibility()
_hide_caption()
if start_pantry_crisis_after_reload:
start_pantry_crisis_after_reload = false
call_deferred("start_pantry_crisis_demo")
func _process(delta: float) -> void:
_refresh_accumulator += delta
if _refresh_accumulator < 0.5:
return
_refresh_accumulator = 0.0
_apply_debug_overlay_visibility()
func _unhandled_key_input(event: InputEvent) -> void:
if not event is InputEventKey or not event.pressed or event.echo:
return
if event.keycode == KEY_F10:
toggle_debug_overlay()
get_viewport().set_input_as_handled()
elif event.keycode == KEY_F11:
request_pantry_crisis_demo()
get_viewport().set_input_as_handled()
elif event.keycode == KEY_F12:
reset_demo_scene()
get_viewport().set_input_as_handled()
func toggle_debug_overlay() -> void:
set_debug_overlay_visible(not debug_overlay_visible)
func set_debug_overlay_visible(is_visible: bool) -> void:
debug_overlay_visible = is_visible
_apply_debug_overlay_visibility()
print(
(
"[DemoController] %s"
% ("Debug overlay visible" if debug_overlay_visible else "Cinematic overlay hidden")
)
)
func request_pantry_crisis_demo() -> void:
start_pantry_crisis_after_reload = true
print("[DemoController] Reloading the repeatable pantry-crisis demo")
get_tree().reload_current_scene()
func reset_demo_scene() -> void:
start_pantry_crisis_after_reload = false
print("[DemoController] Resetting simulation garden demo")
get_tree().reload_current_scene()
func start_pantry_crisis_demo() -> void:
if _demo_running:
return
_demo_running = true
set_debug_overlay_visible(false)
if player != null:
player.set_physics_process(false)
player.velocity = Vector3.ZERO
_focus_camera_on_village()
_show_caption("Morning settles over the village.")
await get_tree().create_timer(VALLEY_ESTABLISH_SECONDS).timeout
if not _demo_running or not is_inside_tree():
return
var staged := stage_pantry_crisis()
if staged.is_empty():
_fail_pantry_crisis("Could not stage the pantry shortage")
return
var helper: SimNPC = staged["helper"]
var interested: SimNPC = staged["interested"]
_show_caption("The pantry is empty. %s is worried." % interested.npc_name)
pantry_crisis_staged.emit(helper.id, interested.id)
await get_tree().create_timer(ESTABLISH_SECONDS).timeout
if not _demo_running or not is_inside_tree():
return
if not begin_pantry_restock():
_fail_pantry_crisis("No villager could begin the food run")
return
_show_caption("%s goes to help." % helper.npc_name)
_focus_camera_on_npc(helper.id)
var elapsed := 0.0
var carrying_was_shown := false
while elapsed < DEMO_TIMEOUT_SECONDS:
if not is_inside_tree():
return
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
if opportunity == null:
var resolved: OpportunityStateRecord = (
simulation_manager.opportunity_system.get_latest()
)
var resolution_event_id := -1
if resolved != null:
resolution_event_id = resolved.get_resolution_event_id()
_show_caption("%s restocks the pantry." % helper.npc_name)
await get_tree().create_timer(RECOVERY_SECONDS).timeout
_demo_running = false
pantry_crisis_completed.emit(helper.id, resolution_event_id)
return
if (
not carrying_was_shown
and helper.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0
):
carrying_was_shown = true
_show_caption("Fresh berries, carried all the way home.")
await get_tree().process_frame
elapsed += get_process_delta_time()
_fail_pantry_crisis("The food route did not finish in time")
func stage_pantry_crisis() -> Dictionary:
if not _has_demo_dependencies() or simulation_manager.npcs.size() < 3:
return {}
var helper: SimNPC = simulation_manager.npcs[FEATURED_HELPER_INDEX]
var interested: SimNPC = simulation_manager.npcs[INTERESTED_VILLAGER_INDEX]
var shortage_actor: SimNPC = simulation_manager.npcs[SHORTAGE_ACTOR_INDEX]
var pantry_position := pantry_storage.get_interaction_position()
for npc in simulation_manager.npcs:
_park_npc_for_demo(npc)
_place_npc_for_demo(helper, pantry_position + Vector3(-1.25, 0.0, 0.15))
_place_npc_for_demo(interested, pantry_position + Vector3(0.0, 0.0, 0.2))
_place_npc_for_demo(shortage_actor, pantry_position + Vector3(1.25, 0.0, 0.15))
var relationship: RelationshipStateRecord = (
simulation_manager.relationship_system.get_relationship(helper.id, interested.id)
)
if relationship == null:
return {}
if relationship.get_trust() < 0.6:
helper.hunger = 85.0
interested.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0)
if (
simulation_manager.economy.deposit_inventory(interested, SimulationIds.RESOURCE_FOOD)
<= 0.0
):
return {}
if relationship.get_trust() < 0.6:
return {}
var pantry: StorageStateRecord = simulation_manager.get_pantry()
if pantry == null:
return {}
var current_food := pantry.get_amount(SimulationIds.RESOURCE_FOOD)
if current_food > 1.0:
pantry.withdraw(SimulationIds.RESOURCE_FOOD, current_food - 1.0)
elif current_food < 1.0:
pantry.deposit(SimulationIds.RESOURCE_FOOD, 1.0 - current_food)
simulation_manager.economy.sync_resource(SimulationIds.RESOURCE_FOOD)
_set_demo_needs(helper, 45.0, false)
_set_demo_needs(interested, 92.0, true)
_set_demo_needs(shortage_actor, 45.0, false)
var withdrawn: float = simulation_manager.economy.withdraw_to_inventory(
shortage_actor, SimulationIds.RESOURCE_FOOD, 1.0
)
var opportunity: OpportunityStateRecord = simulation_manager.get_active_opportunity()
if (
not is_equal_approx(withdrawn, 1.0)
or opportunity == null
or opportunity.get_interested_npc_id() != interested.id
):
return {}
if not simulation_manager.economy.consume_npc_food(shortage_actor):
return {}
_featured_helper_id = helper.id
_interested_villager_id = interested.id
_focus_camera_on_pantry(interested.id)
return {
"helper": helper,
"interested": interested,
"shortage_actor": shortage_actor,
"opportunity": opportunity,
}
func begin_pantry_restock() -> bool:
var helper := _get_npc(_featured_helper_id)
var interested := _get_npc(_interested_villager_id)
if helper == null or interested == null or simulation_manager.get_active_opportunity() == null:
return false
var opportunity_helper: OpportunityHelperResult = (
simulation_manager.get_active_opportunity_helper()
)
if opportunity_helper == null or opportunity_helper.helper_npc_id != helper.id:
return false
var selection: ActionSelectionResult = simulation_manager.action_selector.select_action(
helper,
simulation_manager.village,
simulation_manager.clock.time_of_day(),
simulation_manager.npcs,
opportunity_helper
)
if (
selection == null
or selection.action_id != SimulationIds.ACTION_GATHER_FOOD
or interested.npc_name not in selection.reason
):
return false
var previous_task := helper.current_task
simulation_manager.latest_decisions[helper.id] = selection
simulation_manager.npc_decision_recorded.emit(helper, selection)
helper.set_task(selection.action_id, selection.duration_override)
simulation_manager.npc_task_changed.emit(helper, previous_task, selection.action_id)
simulation_manager.npc_target_requested.emit(helper)
return (
helper.task_state == SimNPC.TASK_STATE_TRAVELING
and not helper.target_id.is_empty()
and helper.has_travel_target
)
func is_pantry_crisis_running() -> bool:
return _demo_running
func _has_demo_dependencies() -> bool:
return (
simulation_manager != null
and world_view_manager != null
and camera_rig != null
and pantry_storage != null
and crisis_caption != null
and "npcs" in simulation_manager
and "active_npc_visuals" in world_view_manager
)
func _park_npc_for_demo(npc: SimNPC) -> void:
var previous_task := npc.current_task
simulation_manager.release_npc_reservation(npc.id)
npc.set_task(SimulationIds.ACTION_WANDER, 1000.0)
npc.start_working()
simulation_manager.npc_task_changed.emit(npc, previous_task, npc.current_task)
func _place_npc_for_demo(npc: SimNPC, position: Vector3) -> void:
var visual := world_view_manager.active_npc_visuals.get(npc.id) as Node3D
if visual == null:
return
if visual.has_method("stop_travel"):
visual.stop_travel()
visual.global_position = position
simulation_manager.synchronize_npc_position(npc.id, position)
func _set_demo_needs(npc: SimNPC, hunger: float, starving: bool) -> void:
npc.hunger = hunger
npc.energy = 90.0
npc.is_starving = starving
npc.starvation_ticks = 0
func _focus_camera_on_pantry(interested_id: int) -> void:
var interested_visual := world_view_manager.active_npc_visuals.get(interested_id) as Node3D
if interested_visual == null:
return
camera_rig.target = interested_visual
camera_rig.pitch_degrees = -44.0
camera_rig.apply_presentation_preset(
pantry_storage.global_position + Vector3(0.0, 0.9, 0.5), 128.0, 0.28
)
func _focus_camera_on_village() -> void:
camera_rig.target = player
camera_rig.pitch_degrees = -42.0
camera_rig.apply_presentation_preset(Vector3(-1.5, 1.2, -4.0), 128.0, 0.7)
func _focus_camera_on_npc(npc_id: int) -> void:
var visual := world_view_manager.active_npc_visuals.get(npc_id) as Node3D
if visual != null:
camera_rig.target = visual
func _show_caption(message: String) -> void:
if crisis_caption == null:
return
if _caption_tween != null:
_caption_tween.kill()
crisis_caption.text = message
crisis_caption.visible = true
crisis_caption.modulate.a = 0.0
_caption_tween = create_tween()
_caption_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
_caption_tween.tween_property(crisis_caption, "modulate:a", 1.0, 0.28)
func _hide_caption() -> void:
if crisis_caption == null:
return
crisis_caption.visible = false
crisis_caption.modulate.a = 1.0
func _fail_pantry_crisis(reason: String) -> void:
_demo_running = false
_show_caption("The food route was interrupted.")
push_error("DemoController: %s" % reason)
pantry_crisis_failed.emit(reason)
func _get_npc(npc_id: int) -> SimNPC:
for npc in simulation_manager.npcs:
if npc.id == npc_id:
return npc
return null
func _apply_debug_overlay_visibility() -> void:
if ui != null and ui.has_method("set_debug_overlay_visible"):
ui.set_debug_overlay_visible(debug_overlay_visible)
for node in ResourceNode.get_all():
if node.has_method("set_debug_label_enabled"):
node.set_debug_label_enabled(debug_overlay_visible)
for node in AnimalNode.get_all():
if node.has_method("set_debug_label_enabled"):
node.set_debug_label_enabled(debug_overlay_visible)
for node in StorageNode.get_all():
if node.has_method("set_debug_label_enabled"):
node.set_debug_label_enabled(debug_overlay_visible)
for node in ActivitySite.get_all():
if node.has_method("set_debug_label_enabled"):
node.set_debug_label_enabled(debug_overlay_visible)
if active_npcs_parent == null:
return
for child in active_npcs_parent.get_children():
if child.has_method("set_debug_overlay_visible"):
child.set_debug_overlay_visible(debug_overlay_visible)