383 lines
12 KiB
GDScript
383 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 AnimalRoutineSite.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)
|