feat: package pantry crisis demo
This commit is contained in:
@@ -503,6 +503,15 @@ Jasmin—rather than an unnamed cinematic extra.
|
||||
The clip has an understandable beginning, decision, and consequence. It does not
|
||||
depend on a voiceover to prove that the simulation is real.
|
||||
|
||||
**Status: complete.** `F11` reloads the fixed authored start and runs one
|
||||
cinematic pantry-crisis story: a brief valley establishment, the empty pantry
|
||||
and worried villager, a trusted helper's real finite-resource trip, visible
|
||||
carrying, deposit, and physical recovery. Four short cardless captions fade over
|
||||
the world while the camera hands off from the pantry to the helper. The same
|
||||
withdrawal, consumption, extraction, and deposit records drive the need and its
|
||||
resolution. `tools/validate_pantry_crisis_demo.gd` verifies that the unassisted
|
||||
route completes inside the 20–30 second budget.
|
||||
|
||||
## Build-in-public content pillars
|
||||
|
||||
Rotate between four types of updates.
|
||||
@@ -720,11 +729,9 @@ Completed:
|
||||
|
||||
Next:
|
||||
|
||||
1. Package one 20–30 second, repeatable pantry-crisis sequence that communicates
|
||||
shortage, purposeful gathering, visible carrying, deposit, and recovery
|
||||
without debug UI. Treat any art or timing work as support for that single
|
||||
readable cause-and-effect loop. Resume the second missing-wood opportunity
|
||||
proof after this public-demo exit test, as sequenced in
|
||||
1. Resume the second bounded opportunity proof from a real missing-wood
|
||||
`task_blocked` fact. Include deterministic interested-party death/staleness
|
||||
handling before extracting shared opportunity machinery, as sequenced in
|
||||
`LEARNING_ROADMAP.md`.
|
||||
|
||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
||||
|
||||
@@ -742,14 +742,26 @@ complete:
|
||||
- the repeatable runtime capture now includes an empty/restocked close pair in
|
||||
the established elevated third-person visual language.
|
||||
|
||||
Milestone 7 is not complete. The immediate outcome-defined slice is now one
|
||||
short, repeatable pantry-crisis sequence that reads without debug UI: shortage,
|
||||
interested villager, finite-resource trip, visible carrying, deposit, and
|
||||
physical recovery. Improve route timing, work feedback, and village composition
|
||||
only where that sequence exposes a readability problem. Once that public-demo
|
||||
loop passes, prove a second bounded unresolved-condition consumer from a real
|
||||
missing-wood `task_blocked` fact, include deterministic interested-party
|
||||
death/staleness handling, and only then extract shared opportunity machinery.
|
||||
The outcome-defined pantry-crisis demo loop is complete:
|
||||
|
||||
- `F11` reloads the authored seed and runs a measured 20–30 second cinematic
|
||||
sequence with the development UI hidden;
|
||||
- a real final-food withdrawal and consumption leave the physical pantry empty
|
||||
and open the knowledge-gated need for its worried witness;
|
||||
- a trusted acquaintance selects ordinary `gather_food`, reserves and visits a
|
||||
finite berry source, visibly carries its yield home, and autonomously selects
|
||||
the real pantry deposit;
|
||||
- that exact deposit resolves the opportunity, restores the physical stock,
|
||||
clears concern, and triggers the existing refill response;
|
||||
- restrained captions and a camera handoff clarify the three beats without
|
||||
replacing state-driven world cues, while a runtime validator measures the
|
||||
unassisted route and a headless scenario verifies its full event chain.
|
||||
|
||||
Milestone 7 is not complete. The immediate next slice is the second bounded
|
||||
unresolved-condition consumer from a real missing-wood `task_blocked` fact,
|
||||
including deterministic interested-party death/staleness handling. Extract
|
||||
shared opportunity machinery only after that second consumer proves the common
|
||||
contract.
|
||||
|
||||
Recently completed:
|
||||
|
||||
|
||||
+12
-5
@@ -882,11 +882,18 @@ facts now have a cardless world expression: empty/low/stocked pantry props, one
|
||||
interested-villager concern cue, and a brief refill response. Save/load rebuilds
|
||||
the stable state without replaying the response.
|
||||
|
||||
Milestone 7 remains in progress. The immediate next slice is an outcome-defined
|
||||
20–30 second pantry-crisis demo loop—shortage, finite-resource travel, visible
|
||||
carrying, deposit, and physical recovery—without debug UI. The second
|
||||
missing-wood consumer and deterministic interested-party death/staleness rules
|
||||
follow that visual exit test, before common opportunity machinery is extracted.
|
||||
The outcome-defined pantry-crisis demo loop is complete. `F11` reloads a fixed
|
||||
authored start and packages a measured 20–30 second cinematic story with brief
|
||||
captions: real withdrawal and consumption expose the shortage, a worried
|
||||
informed villager remains at the empty pantry, and a trusted helper selects a
|
||||
finite berry source, visibly carries its yield, deposits it, and causes the
|
||||
physical recovery. A headless lifecycle scenario and an unassisted runtime
|
||||
timing validator cover the complete path.
|
||||
|
||||
Milestone 7 remains in progress. The immediate next slice is the second
|
||||
missing-wood consumer from a real `task_blocked` fact, including deterministic
|
||||
interested-party death/staleness handling before common opportunity machinery
|
||||
is extracted.
|
||||
|
||||
The remaining simulation-garden target still aims for:
|
||||
|
||||
|
||||
@@ -126,7 +126,41 @@ offset_right = 940.0
|
||||
offset_bottom = 66.0
|
||||
script = ExtResource("12_timedial")
|
||||
|
||||
[node name="DemoController" type="Node" parent="." unique_id=644713371 node_paths=PackedStringArray("ui", "active_npcs_parent")]
|
||||
[node name="CrisisCaptionLayer" type="CanvasLayer" parent="."]
|
||||
layer = 4
|
||||
|
||||
[node name="CrisisCaption" type="Label" parent="CrisisCaptionLayer"]
|
||||
visible = false
|
||||
anchors_preset = 7
|
||||
anchor_left = 0.5
|
||||
anchor_top = 1.0
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -560.0
|
||||
offset_top = -112.0
|
||||
offset_right = 560.0
|
||||
offset_bottom = -54.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
mouse_filter = 2
|
||||
theme_override_colors/font_color = Color(0.98, 0.92, 0.78, 1)
|
||||
theme_override_colors/font_outline_color = Color(0.08, 0.055, 0.035, 0.92)
|
||||
theme_override_colors/font_shadow_color = Color(0.02, 0.015, 0.01, 0.7)
|
||||
theme_override_constants/outline_size = 9
|
||||
theme_override_constants/shadow_offset_x = 2
|
||||
theme_override_constants/shadow_offset_y = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "The pantry is empty."
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="DemoController" type="Node" parent="." unique_id=644713371 node_paths=PackedStringArray("ui", "active_npcs_parent", "simulation_manager", "world_view_manager", "camera_rig", "player", "pantry_storage", "crisis_caption")]
|
||||
script = ExtResource("13_demo")
|
||||
ui = NodePath("../UI")
|
||||
active_npcs_parent = NodePath("../ActiveNPCs")
|
||||
simulation_manager = NodePath("../SimulationManager")
|
||||
world_view_manager = NodePath("../WorldViewManager")
|
||||
camera_rig = NodePath("../CameraRig")
|
||||
player = NodePath("../Player")
|
||||
pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry")
|
||||
crisis_caption = NodePath("../CrisisCaptionLayer/CrisisCaption")
|
||||
|
||||
@@ -327,6 +327,17 @@ func set_target_position(pos: Vector3) -> void:
|
||||
debug_log("New target: %s Path points: %s" % [pos, current_path.size()])
|
||||
|
||||
|
||||
func stop_travel() -> void:
|
||||
path_request_id += 1
|
||||
current_path = PackedVector3Array()
|
||||
path_index = 0
|
||||
current_target = Vector3.INF
|
||||
path_pending = false
|
||||
has_reported_arrival = true
|
||||
stuck_time = 0.0
|
||||
velocity = Vector3.ZERO
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if is_dead_visual:
|
||||
velocity = Vector3.ZERO
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var main_scene: Node = load("res://main.tscn").instantiate()
|
||||
root.add_child(main_scene)
|
||||
await process_frame
|
||||
for _frame in 10:
|
||||
await physics_frame
|
||||
|
||||
var manager := main_scene.get_node("SimulationManager")
|
||||
var world_view := main_scene.get_node("WorldViewManager")
|
||||
var demo := main_scene.get_node("DemoController")
|
||||
var caption := main_scene.get_node("CrisisCaptionLayer/CrisisCaption") as Label
|
||||
manager.set_process(false)
|
||||
|
||||
var staged: Dictionary = demo.stage_pantry_crisis()
|
||||
_check(not staged.is_empty(), "The repeatable pantry crisis should stage from a fresh scene")
|
||||
if staged.is_empty():
|
||||
_finish()
|
||||
return
|
||||
var helper: SimNPC = staged["helper"]
|
||||
var interested: SimNPC = staged["interested"]
|
||||
var shortage_actor: SimNPC = staged["shortage_actor"]
|
||||
var opportunity: OpportunityStateRecord = staged["opportunity"]
|
||||
var pantry: StorageStateRecord = manager.get_pantry()
|
||||
var helper_visual: Node3D = world_view.active_npc_visuals.get(helper.id)
|
||||
var interested_visual: Node3D = world_view.active_npc_visuals.get(interested.id)
|
||||
|
||||
_check(
|
||||
is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 0.0),
|
||||
"The demo shortage should empty the authoritative pantry"
|
||||
)
|
||||
_check(
|
||||
is_equal_approx(shortage_actor.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0),
|
||||
"The final pantry unit should be consumed through the real economy before the food run"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
opportunity.get_interested_npc_id() == interested.id
|
||||
and interested_visual.get_node("OpportunityConcernRoot").visible
|
||||
and interested_visual.current_target == Vector3.INF
|
||||
),
|
||||
"The informed hungry villager should own and visibly express the open need"
|
||||
)
|
||||
_check(
|
||||
not caption.visible,
|
||||
"The cinematic caption should remain hidden until the full sequence starts"
|
||||
)
|
||||
|
||||
_check(demo.begin_pantry_restock(), "A trusted helper should begin the real food run")
|
||||
var decision: ActionSelectionResult = manager.get_latest_decision(helper.id)
|
||||
_check(
|
||||
(
|
||||
decision != null
|
||||
and decision.action_id == SimulationIds.ACTION_GATHER_FOOD
|
||||
and interested.npc_name in decision.reason
|
||||
),
|
||||
"The helper's selected action should be caused by the starving trusted villager"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
helper.task_state == SimNPC.TASK_STATE_TRAVELING
|
||||
and helper.has_travel_target
|
||||
and not helper.target_id.is_empty()
|
||||
),
|
||||
"The helper should reserve and travel to a finite food ResourceNode"
|
||||
)
|
||||
var resource_state: ResourceStateRecord = manager.get_resource_state(helper.target_id)
|
||||
_check(
|
||||
(
|
||||
resource_state != null
|
||||
and resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD
|
||||
and resource_state.get_reserved_by() == helper.id
|
||||
),
|
||||
"The demo trip should use the normal finite-resource reservation contract"
|
||||
)
|
||||
|
||||
helper_visual.global_position = helper.travel_target_position
|
||||
manager.synchronize_npc_position(helper.id, helper_visual.global_position)
|
||||
manager.notify_npc_arrived(helper.id)
|
||||
for _tick in int(ceil(helper.task_duration)):
|
||||
manager.simulate_tick()
|
||||
_check(
|
||||
(
|
||||
helper.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0
|
||||
and helper_visual.get_node("CarriedFood").visible
|
||||
),
|
||||
"Completing the real gather action should create visible carried food"
|
||||
)
|
||||
|
||||
manager.simulate_tick()
|
||||
_check(
|
||||
(
|
||||
helper.current_task == SimulationIds.ACTION_DEPOSIT_FOOD
|
||||
and helper.target_id == SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||
and helper.has_travel_target
|
||||
),
|
||||
"The helper should autonomously choose the real pantry deposit after gathering"
|
||||
)
|
||||
helper_visual.global_position = helper.travel_target_position
|
||||
manager.synchronize_npc_position(helper.id, helper_visual.global_position)
|
||||
manager.notify_npc_arrived(helper.id)
|
||||
manager.simulate_tick()
|
||||
|
||||
var pantry_visual := (
|
||||
main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry/PantryPresentation")
|
||||
as PantryStockVisual
|
||||
)
|
||||
var resolved: OpportunityStateRecord = manager.opportunity_system.get_latest()
|
||||
_check(
|
||||
(
|
||||
manager.get_active_opportunity() == null
|
||||
and resolved != null
|
||||
and resolved.get_status() == SimulationIds.OPPORTUNITY_STATUS_RESOLVED
|
||||
and resolved.get_resolution_event_id() > resolved.get_trigger_event_id()
|
||||
),
|
||||
"The exact later deposit event should resolve the same opportunity record"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
pantry.get_amount(SimulationIds.RESOURCE_FOOD) > 0.0
|
||||
and pantry_visual.get_node("RefillResponse").visible
|
||||
and not interested_visual.get_node("OpportunityConcernRoot").visible
|
||||
),
|
||||
"The deposit should physically restock the pantry and clear the concern cue"
|
||||
)
|
||||
_check(
|
||||
(
|
||||
_has_event(manager.economic_events, SimulationIds.EVENT_RESOURCE_EXTRACTED, helper.id)
|
||||
and _has_event(
|
||||
manager.economic_events, SimulationIds.EVENT_STORAGE_DEPOSITED, helper.id
|
||||
)
|
||||
),
|
||||
"The sequence should be traceable through real extraction and deposit events"
|
||||
)
|
||||
|
||||
_finish()
|
||||
|
||||
|
||||
func _has_event(events: Array[EconomicEventRecord], event_type: StringName, actor_id: int) -> bool:
|
||||
for event in events:
|
||||
if (
|
||||
StringName(event.data["event_type"]) == event_type
|
||||
and int(event.data["actor_id"]) == actor_id
|
||||
):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("[TEST] Pantry crisis demo scenario passed")
|
||||
quit(0)
|
||||
return
|
||||
for failure in failures:
|
||||
push_error("[TEST] " + failure)
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://drf7vq0pk51ys
|
||||
@@ -0,0 +1,58 @@
|
||||
extends SceneTree
|
||||
|
||||
const MINIMUM_DURATION_SECONDS := 20.0
|
||||
const MAXIMUM_DURATION_SECONDS := 30.0
|
||||
const HARD_TIMEOUT_SECONDS := 36.0
|
||||
|
||||
var completed := false
|
||||
var failed_reason := ""
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var main_scene: Node = load("res://main.tscn").instantiate()
|
||||
root.add_child(main_scene)
|
||||
await process_frame
|
||||
for _frame in 10:
|
||||
await physics_frame
|
||||
|
||||
var demo := main_scene.get_node("DemoController")
|
||||
demo.pantry_crisis_completed.connect(_on_demo_completed)
|
||||
demo.pantry_crisis_failed.connect(_on_demo_failed)
|
||||
var started_at := Time.get_ticks_msec()
|
||||
demo.start_pantry_crisis_demo()
|
||||
|
||||
while not completed and failed_reason.is_empty():
|
||||
var elapsed := (Time.get_ticks_msec() - started_at) / 1000.0
|
||||
if elapsed > HARD_TIMEOUT_SECONDS:
|
||||
failed_reason = "Timed out after %.1f seconds" % elapsed
|
||||
break
|
||||
await process_frame
|
||||
|
||||
var duration := (Time.get_ticks_msec() - started_at) / 1000.0
|
||||
if not failed_reason.is_empty():
|
||||
push_error("[TOOL] Pantry crisis demo failed: %s" % failed_reason)
|
||||
quit(1)
|
||||
return
|
||||
if duration < MINIMUM_DURATION_SECONDS or duration > MAXIMUM_DURATION_SECONDS:
|
||||
push_error(
|
||||
(
|
||||
"[TOOL] Pantry crisis demo took %.1f seconds; expected %.0f–%.0f"
|
||||
% [duration, MINIMUM_DURATION_SECONDS, MAXIMUM_DURATION_SECONDS]
|
||||
)
|
||||
)
|
||||
quit(1)
|
||||
return
|
||||
print("[TOOL] Pantry crisis demo completed in %.1f seconds" % duration)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _on_demo_completed(_helper_id: int, _resolution_event_id: int) -> void:
|
||||
completed = true
|
||||
|
||||
|
||||
func _on_demo_failed(reason: String) -> void:
|
||||
failed_reason = reason
|
||||
@@ -0,0 +1 @@
|
||||
uid://be73skpg0q6ub
|
||||
@@ -1,14 +1,42 @@
|
||||
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:
|
||||
@@ -25,13 +53,20 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
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:
|
||||
debug_overlay_visible = not debug_overlay_visible
|
||||
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(
|
||||
(
|
||||
@@ -41,11 +76,275 @@ func toggle_debug_overlay() -> void:
|
||||
)
|
||||
|
||||
|
||||
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 selection: ActionSelectionResult = simulation_manager.action_selector.select_action(
|
||||
helper,
|
||||
simulation_manager.village,
|
||||
simulation_manager.clock.time_of_day(),
|
||||
simulation_manager.npcs
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://bivrsk5ukgnoo
|
||||
+1
-1
@@ -226,7 +226,7 @@ func _refresh_npc_inspector() -> void:
|
||||
+ "%s"
|
||||
+ "Why\n%s%s\n\n"
|
||||
+ "Own history\n%s\n\n"
|
||||
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
|
||||
+ "[Tab] Next villager [F10] Cinematic/debug [F11] Pantry story [F12] Reset"
|
||||
)
|
||||
% [
|
||||
npc.npc_name,
|
||||
|
||||
Reference in New Issue
Block a user