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
|
The clip has an understandable beginning, decision, and consequence. It does not
|
||||||
depend on a voiceover to prove that the simulation is real.
|
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
|
## Build-in-public content pillars
|
||||||
|
|
||||||
Rotate between four types of updates.
|
Rotate between four types of updates.
|
||||||
@@ -720,11 +729,9 @@ Completed:
|
|||||||
|
|
||||||
Next:
|
Next:
|
||||||
|
|
||||||
1. Package one 20–30 second, repeatable pantry-crisis sequence that communicates
|
1. Resume the second bounded opportunity proof from a real missing-wood
|
||||||
shortage, purposeful gathering, visible carrying, deposit, and recovery
|
`task_blocked` fact. Include deterministic interested-party death/staleness
|
||||||
without debug UI. Treat any art or timing work as support for that single
|
handling before extracting shared opportunity machinery, as sequenced in
|
||||||
readable cause-and-effect loop. Resume the second missing-wood opportunity
|
|
||||||
proof after this public-demo exit test, as sequenced in
|
|
||||||
`LEARNING_ROADMAP.md`.
|
`LEARNING_ROADMAP.md`.
|
||||||
|
|
||||||
Do not start with GIS data, a full city, a large asset pack, or more NPC
|
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 repeatable runtime capture now includes an empty/restocked close pair in
|
||||||
the established elevated third-person visual language.
|
the established elevated third-person visual language.
|
||||||
|
|
||||||
Milestone 7 is not complete. The immediate outcome-defined slice is now one
|
The outcome-defined pantry-crisis demo loop is complete:
|
||||||
short, repeatable pantry-crisis sequence that reads without debug UI: shortage,
|
|
||||||
interested villager, finite-resource trip, visible carrying, deposit, and
|
- `F11` reloads the authored seed and runs a measured 20–30 second cinematic
|
||||||
physical recovery. Improve route timing, work feedback, and village composition
|
sequence with the development UI hidden;
|
||||||
only where that sequence exposes a readability problem. Once that public-demo
|
- a real final-food withdrawal and consumption leave the physical pantry empty
|
||||||
loop passes, prove a second bounded unresolved-condition consumer from a real
|
and open the knowledge-gated need for its worried witness;
|
||||||
missing-wood `task_blocked` fact, include deterministic interested-party
|
- a trusted acquaintance selects ordinary `gather_food`, reserves and visits a
|
||||||
death/staleness handling, and only then extract shared opportunity machinery.
|
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:
|
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
|
interested-villager concern cue, and a brief refill response. Save/load rebuilds
|
||||||
the stable state without replaying the response.
|
the stable state without replaying the response.
|
||||||
|
|
||||||
Milestone 7 remains in progress. The immediate next slice is an outcome-defined
|
The outcome-defined pantry-crisis demo loop is complete. `F11` reloads a fixed
|
||||||
20–30 second pantry-crisis demo loop—shortage, finite-resource travel, visible
|
authored start and packages a measured 20–30 second cinematic story with brief
|
||||||
carrying, deposit, and physical recovery—without debug UI. The second
|
captions: real withdrawal and consumption expose the shortage, a worried
|
||||||
missing-wood consumer and deterministic interested-party death/staleness rules
|
informed villager remains at the empty pantry, and a trusted helper selects a
|
||||||
follow that visual exit test, before common opportunity machinery is extracted.
|
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:
|
The remaining simulation-garden target still aims for:
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,41 @@ offset_right = 940.0
|
|||||||
offset_bottom = 66.0
|
offset_bottom = 66.0
|
||||||
script = ExtResource("12_timedial")
|
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")
|
script = ExtResource("13_demo")
|
||||||
ui = NodePath("../UI")
|
ui = NodePath("../UI")
|
||||||
active_npcs_parent = NodePath("../ActiveNPCs")
|
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()])
|
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:
|
func _physics_process(delta: float) -> void:
|
||||||
if is_dead_visual:
|
if is_dead_visual:
|
||||||
velocity = Vector3.ZERO
|
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
|
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 ui: CanvasLayer
|
||||||
@export var active_npcs_parent: Node
|
@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
|
@export var debug_overlay_visible := true
|
||||||
|
|
||||||
var _refresh_accumulator := 0.0
|
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:
|
func _ready() -> void:
|
||||||
_apply_debug_overlay_visibility()
|
_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:
|
func _process(delta: float) -> void:
|
||||||
@@ -25,13 +53,20 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
|||||||
if event.keycode == KEY_F10:
|
if event.keycode == KEY_F10:
|
||||||
toggle_debug_overlay()
|
toggle_debug_overlay()
|
||||||
get_viewport().set_input_as_handled()
|
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:
|
elif event.keycode == KEY_F12:
|
||||||
reset_demo_scene()
|
reset_demo_scene()
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
func toggle_debug_overlay() -> void:
|
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()
|
_apply_debug_overlay_visibility()
|
||||||
print(
|
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:
|
func reset_demo_scene() -> void:
|
||||||
|
start_pantry_crisis_after_reload = false
|
||||||
print("[DemoController] Resetting simulation garden demo")
|
print("[DemoController] Resetting simulation garden demo")
|
||||||
get_tree().reload_current_scene()
|
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:
|
func _apply_debug_overlay_visibility() -> void:
|
||||||
if ui != null and ui.has_method("set_debug_overlay_visible"):
|
if ui != null and ui.has_method("set_debug_overlay_visible"):
|
||||||
ui.set_debug_overlay_visible(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"
|
+ "%s"
|
||||||
+ "Why\n%s%s\n\n"
|
+ "Why\n%s%s\n\n"
|
||||||
+ "Own history\n%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,
|
npc.npc_name,
|
||||||
|
|||||||
Reference in New Issue
Block a user