Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
277 lines
15 KiB
GDScript
277 lines
15 KiB
GDScript
extends SceneTree
|
|
|
|
const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd")
|
|
var _finishing := false
|
|
|
|
## A real inn from the default 5,000 G / 50 materials. No inserted guests,
|
|
## inventory, employees, completed construction, or free resource commands.
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _runtime: Variant
|
|
var _clock: Variant
|
|
var _state: Variant
|
|
var _ids: Dictionary = {}
|
|
var _saw_loaded := false
|
|
var _saw_movement := false
|
|
var _saw_batch := false
|
|
var _save_checked := false
|
|
var _last_porter_cell := Vector3i.ZERO
|
|
var _save_path := "user://tests/normal_auto_supply_%d/cargo" % OS.get_process_id()
|
|
var _main: Control
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
_runtime = root.get_node("SimulationRuntime")
|
|
_clock = root.get_node("GameClock")
|
|
_state = root.get_node("GameState")
|
|
_clock.reset(1, 6, 0)
|
|
_state.reset()
|
|
_runtime.initialize(_clock, _state, 424242)
|
|
_clock.set_paused(true)
|
|
root.size = Vector2i(1440, 810)
|
|
_main = load("res://scenes/main/main.tscn").instantiate() as Control
|
|
_main.set("manual_save_path", "user://tests/supply_auto_normal_%d/manual" % OS.get_process_id())
|
|
root.add_child(_main)
|
|
await process_frame
|
|
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
_expect(_state.get_resource(&"gold") == 5000 and _state.get_resource(&"materials") == 50, "ordinary starting resources")
|
|
for row: Array in [[&"reception", Vector3i(-8, 0, 0)], [&"cafeteria", Vector3i(2, 0, 0)], [&"warehouse", Vector3i(-8, 0, 1)], [&"kitchen", Vector3i(2, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]:
|
|
if not _command(&"build_facility", {&"definition_id": row[0], &"origin": row[1]}):
|
|
_finish()
|
|
return
|
|
await _advance(180)
|
|
_command(&"build_facility", {&"definition_id": &"standard_room", &"origin": Vector3i(-8, 2, 0)})
|
|
_command(&"build_facility", {&"definition_id": &"guest_stairs", &"origin": Vector3i(-4, 1, 0)})
|
|
await _advance(60)
|
|
_command(&"build_facility", {&"definition_id": &"guest_stairs", &"origin": Vector3i(-4, 2, 0)})
|
|
await _advance(10)
|
|
for x: int in [-5, -6]:
|
|
_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(x, 2, 0)})
|
|
await _advance(10)
|
|
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
|
_ids[facility.definition_id] = facility.instance_id
|
|
if not _command(&"set_facility_conditions", {&"facility_id": _ids[&"cafeteria"], &"conditions": {&"related_facility_id": _ids[&"kitchen"]}}):
|
|
_finish()
|
|
return
|
|
for assignment: Array in [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"], [&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]]:
|
|
if not _command(&"hire_staff_candidate", {&"candidate_id": assignment[0]}) or not _command(&"set_staff_work_assignment", {&"staff_id": assignment[0], &"facility_id": _ids[assignment[1]], &"template_id": &"DAY"}):
|
|
_finish()
|
|
return
|
|
_expect(_state.get_resource(&"materials") == 4, "all buildings and the staff door fit within the starting material budget")
|
|
var warehouse: FacilityInstance = _runtime.facility_service.get_instance(_ids[&"warehouse"])
|
|
var cash_before: int = _state.get_resource(&"gold")
|
|
_command(&"purchase_supplies", {&"warehouse_id": warehouse.instance_id, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
|
|
_expect(_state.get_resource(&"gold") == cash_before - 120 and _runtime.inventory_service.total_quantity(warehouse.local_inventory_id, SupplyCatalog.RAW_FOOD) == 40, "40 raw food costs exactly 120 G and arrives only in the warehouse")
|
|
_main.call("_activate_management_mode", &"product")
|
|
var auto_panel := _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
|
|
_expect(auto_panel != null, "normal product UI exposes automatic paid ordering")
|
|
if auto_panel == null: _finish(); return
|
|
auto_panel.auto_purchase_enabled.button_pressed = true
|
|
auto_panel.auto_purchase_target.value = 20 # Explicit player-input fixture; not a default balance value.
|
|
auto_panel.auto_purchase_button.pressed.emit()
|
|
_expect(_runtime.auto_purchase_plans.get_plan(warehouse.instance_id, SupplyCatalog.RAW_FOOD)[&"enabled"], "normal product controls arm player-selected target20")
|
|
_command(&"set_kitchen_production", {&"facility_id": _ids[&"kitchen"], &"enabled": true, &"target_stock": 8})
|
|
await _advance(120)
|
|
_expect(_saw_movement and _saw_loaded, "an employed porter walks the real route and carries purchased food")
|
|
_expect(_saw_batch, "the kitchen starts autonomous production without a guest request")
|
|
_expect(_save_checked, "a mid-delivery disk save restores ownership and replays exactly")
|
|
_expect(_completed_deliveries(SupplyCatalog.RAW_FOOD) > 0 and _completed_deliveries(SupplyCatalog.BASIC_MEAL) > 0, "both warehouse-to-kitchen and kitchen-to-cafeteria deliveries complete")
|
|
_expect(_sales(&"cafeteria") > 0, "natural guests pay for meals made from purchased food")
|
|
await _capture()
|
|
if OS.get_environment("THE_TREE_SUPPLY_PREVIEW") == "1":
|
|
_start_preview()
|
|
return
|
|
if OS.get_environment("THE_TREE_SUPPLY_CAPTURE_ONLY") == "1":
|
|
_finish()
|
|
return
|
|
for day: int in range(1, 4):
|
|
await _advance(360 + 1440 * day - _clock.absolute_minute)
|
|
var summaries: Array = _runtime.operations_coordinator.get_day_summaries()
|
|
_expect(summaries.size() == day and bool(summaries[-1].get(&"balanced", false)), "business day %d closes with a reconciled ledger" % day)
|
|
_expect(_state.get_resource(&"gold") >= 0, "day %d retains cash after real payroll and purchases" % day)
|
|
_expect(int(summaries[-1].get(&"arrears_total", -1)) == 0, "day %d pays all real wages without arrears" % day)
|
|
_check_auto_financial_close(summaries[-1])
|
|
_expect(_auto_order_count() >= 2, "at least two real shortages cause paid automatic orders without another manual restock")
|
|
_expect(_sales(&"standard_room") > 0, "the same inn earns lodging revenue")
|
|
_expect(_sales(&"reception") == 0, "three days do not depend on artificial reception revenue")
|
|
_expect(_runtime.restore_snapshot(_runtime.create_snapshot()), "three-day supply history remains restorable")
|
|
var evidence := {"checks": _checks, "failures": _failures, "meal_sales": _sales(&"cafeteria"), "lodging_sales": _sales(&"standard_room"), "raw_deliveries": _completed_deliveries(SupplyCatalog.RAW_FOOD), "meal_deliveries": _completed_deliveries(SupplyCatalog.BASIC_MEAL), "cash": _state.get_resource(&"gold"), "closes": _runtime.operations_coordinator.get_day_summaries(), "supply": _runtime.get_supply_management_report(), "upkeep": _runtime.upkeep_settlement.get_report(), "maintenance_expense": _runtime.economy_service.get_balance(FacilityUpkeepSettlement.EXPENSE)}
|
|
var evidence_dir := "res://artifacts/supply-auto-chain" if OS.get_environment("THE_TREE_BUSINESS_ACCEPTANCE") == "1" else "res://artifacts/supply-auto-chain"
|
|
if not OS.get_environment("THE_TREE_SUPPLY_EVIDENCE_DIR").is_empty():
|
|
evidence_dir = OS.get_environment("THE_TREE_SUPPLY_EVIDENCE_DIR")
|
|
var evidence_path := ProjectSettings.globalize_path(evidence_dir.path_join("normal-play-report.json"))
|
|
DirAccess.make_dir_recursive_absolute(evidence_path.get_base_dir())
|
|
var file := FileAccess.open(evidence_path, FileAccess.WRITE)
|
|
if file != null:
|
|
file.store_string(JSON.stringify(evidence, "\t"))
|
|
print("NORMAL_AUTO_PURCHASE_REPORT ", JSON.stringify({"cash": evidence["cash"], "meal_sales": evidence["meal_sales"], "raw_deliveries": evidence["raw_deliveries"], "meal_deliveries": evidence["meal_deliveries"]}))
|
|
_finish()
|
|
|
|
|
|
func _auto_order_count() -> int:
|
|
var count := 0
|
|
for row: Dictionary in _runtime.economy_service.create_snapshot()[&"idempotency"]:
|
|
if String(row[&"key"]).begins_with("supply.auto."): count += 1
|
|
return count
|
|
|
|
|
|
func _check_auto_financial_close(summary: Dictionary) -> void:
|
|
var day := int(summary[&"day"])
|
|
var start: int = _runtime.operations_coordinator.get_financial_period_start(day)
|
|
var end := int(summary[&"closed_at"])
|
|
var purchase_expense := 0
|
|
var signing_expense := 0
|
|
var auto_expense := 0
|
|
var cash_in := 0
|
|
var cash_out := 0
|
|
for entry: LedgerEntry in _runtime.economy_service.get_entries():
|
|
if entry.category != &"INITIALIZATION":
|
|
if entry.debit_account == &"PLAYER_CASH": cash_in += entry.amount
|
|
if entry.credit_account == &"PLAYER_CASH": cash_out += entry.amount
|
|
if entry.absolute_minute < start or entry.absolute_minute >= end: continue
|
|
if entry.category == &"STAFF_SIGNING_COST": signing_expense += entry.amount
|
|
if entry.category == &"OPERATING_EXPENSE":
|
|
purchase_expense += entry.amount
|
|
if entry.source_entity_id == _ids[&"warehouse"] and _runtime.economy_service.get_transaction_id_for_idempotency_key(
|
|
SupplyAutoPurchasePlans.order_key(_ids[&"warehouse"], SupplyCatalog.RAW_FOOD, entry.absolute_minute)) == entry.transaction_id:
|
|
auto_expense += entry.amount
|
|
var receipt: Dictionary = SupplyAutoPurchasePlans.receipt_from_ledger(_runtime.economy_service, _ids[&"warehouse"], SupplyCatalog.RAW_FOOD, entry.absolute_minute, entry.transaction_id)
|
|
_expect(not receipt.is_empty() and receipt[&"quantity"] * 3 == entry.amount and receipt[&"unit_price"] == 3 and receipt[&"total_cost"] == entry.amount, "actual automatic receipt retains current normal3G quote and unchanged cost/source/category")
|
|
_expect(summary[&"category_totals"][&"cost_of_goods"] == purchase_expense + signing_expense, "day%d actual purchase+contract cost exactly reconciles daily cost_of_goods" % day)
|
|
_expect(int(summary[&"category_totals"][&"cost_of_goods"]) >= auto_expense, "automatic payment is included in operating expense once")
|
|
_expect(_runtime.economy_service.get_balance(&"PLAYER_CASH") == 5000 + cash_in - cash_out, "real initial cash+all ledger flows exactly equals player cash")
|
|
var warehouse_row: Dictionary = summary[&"facility_profit_loss"].get(_ids[&"warehouse"], {})
|
|
_expect(not warehouse_row.is_empty() and int(warehouse_row.get(&"expenses", 0)) >= auto_expense, "actual warehouse P&L retains automatic expense attribution")
|
|
|
|
|
|
func _advance(minutes: int) -> void:
|
|
for index: int in maxi(minutes, 0):
|
|
var before: int = _clock.absolute_minute
|
|
_clock.advance_minutes(1)
|
|
if _clock.absolute_minute != before + 1:
|
|
_expect(false, "clock must progress: %s" % _runtime.get_operations_report())
|
|
return
|
|
var porter: StaffRuntime = _runtime.staff_service.get_staff(&"staff_gina")
|
|
if porter != null:
|
|
_saw_movement = _saw_movement or porter.current_cell != _last_porter_cell
|
|
_last_porter_cell = porter.current_cell
|
|
if not porter.carried_inventory_id.is_empty():
|
|
_saw_loaded = true
|
|
if not _save_checked:
|
|
_check_loaded_save()
|
|
if _ids.has(&"kitchen"):
|
|
_saw_batch = _saw_batch or not _runtime.kitchen_production.get_report(_ids[&"kitchen"]).get(&"active_batch", {}).is_empty()
|
|
if index % 120 == 119:
|
|
print("SUPPLY_PROGRESS ", _clock.formatted_time(), " meals=", _sales(&"cafeteria"))
|
|
await process_frame
|
|
|
|
|
|
func _check_loaded_save() -> void:
|
|
_save_checked = true
|
|
var saved: Dictionary = _runtime.save_to_path(_save_path)
|
|
_expect(bool(saved.get(&"ok", false)), "mid-delivery save writes to an isolated slot")
|
|
var before: Dictionary = _runtime.create_snapshot()
|
|
var loaded: Dictionary = _runtime.load_from_path(_save_path)
|
|
_expect(bool(loaded.get(&"ok", false)), "mid-delivery disk restore: %s" % loaded)
|
|
var normalized: Dictionary = _runtime.create_snapshot()
|
|
_clock.advance_minutes(5)
|
|
var expected: Dictionary = _runtime.create_snapshot()
|
|
_expect(_runtime.restore_snapshot(normalized), "mid-delivery checkpoint rewinds")
|
|
_clock.advance_minutes(5)
|
|
_expect(_runtime.create_snapshot() == expected, "mid-delivery replay preserves exact inventory, tasks, positions, and ledger")
|
|
_expect(_runtime.restore_snapshot(before), "resume the observed minute after replay verification")
|
|
|
|
|
|
func _completed_deliveries(item: StringName) -> int:
|
|
var count := 0
|
|
for request: LogisticsRequest in _runtime.logistics_requests.values():
|
|
if String(request.request_id).begins_with("supply.") and request.state == LogisticsRequest.State.COMPLETED and request.item_requirements.has(item):
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _sales(definition_id: StringName) -> int:
|
|
var total := 0
|
|
for entry: LedgerEntry in _runtime.economy_service.get_entries():
|
|
if entry.category == &"SERVICE_SALE" and entry.source_entity_id == _ids.get(definition_id, &"missing"):
|
|
total += entry.amount
|
|
return total
|
|
|
|
|
|
func _command(type: StringName, payload: Dictionary) -> bool:
|
|
var result: CommandResult = _runtime.submit_command(type, payload)
|
|
_expect(result.is_success(), "%s %s: %s" % [type, payload, result.message])
|
|
return result.is_success()
|
|
|
|
|
|
func _capture() -> void:
|
|
if OS.get_environment("THE_TREE_SUPPLY_CAPTURE") != "1":
|
|
return
|
|
await process_frame
|
|
_main.call("_activate_management_mode", &"product")
|
|
var panel := _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
|
|
for tab: StringName in [&"purchase", &"kitchen", &"delivery"]:
|
|
panel.select_tab(tab)
|
|
await process_frame
|
|
await RenderingServer.frame_post_draw
|
|
var path := ProjectSettings.globalize_path("res://artifacts/supply-auto-chain/%s.png" % tab)
|
|
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
|
root.get_texture().get_image().save_png(path)
|
|
|
|
|
|
func _expect(condition: bool, message: String) -> void:
|
|
_checks += 1
|
|
if not condition:
|
|
_failures += 1
|
|
push_error("FAIL: " + message)
|
|
|
|
|
|
func _finish() -> void:
|
|
if _finishing:
|
|
return
|
|
_finishing = true
|
|
if is_instance_valid(_main):
|
|
var final_cleanup: Dictionary = await FixtureCleanup.free_owner_and_wait(self, _main)
|
|
if not bool(final_cleanup.get(&"passed", false)):
|
|
_failures += 1
|
|
push_error("ordinary Main final audio cleanup failed")
|
|
SaveService.delete_save(_save_path)
|
|
print("Normal auto purchase chain: %d checks, %d failures" % [_checks, _failures])
|
|
quit(1 if _failures > 0 else 0)
|
|
|
|
|
|
func _start_preview() -> void:
|
|
if _failures > 0:
|
|
_finish()
|
|
return
|
|
SaveService.delete_save(_save_path)
|
|
root.title = "The Tree · 仕入れと厨房の試遊(保存なし)"
|
|
var load_menu := _main.find_child("SaveLoadMenu", true, false) as MenuButton
|
|
if load_menu != null:
|
|
load_menu.disabled = true
|
|
load_menu.tooltip_text = "この試遊は保存・読込を行いません。"
|
|
for node: Node in _main.find_children("*", "Button", true, false):
|
|
var button := node as Button
|
|
for connection: Dictionary in button.pressed.get_connections():
|
|
var callback: Callable = connection[&"callable"]
|
|
if callback.get_method() in [&"_save_manual", &"_load_manual"]:
|
|
button.disabled = true
|
|
button.tooltip_text = "この試遊は保存しません。"
|
|
if OS.get_environment("THE_TREE_BUSINESS_PREVIEW") == "1":
|
|
root.title = "The Tree · 料金と営業の試遊(保存なし)"
|
|
_main.call("_activate_management_mode", &"business")
|
|
var business := _main.find_child("BusinessManagementPanel", true, false)
|
|
business.call("select_facility", _ids[&"cafeteria"])
|
|
_main.call("_set_status", "料金と営業状態を変更できます。Spaceで再開すると、客の行動に反映されます(保存なし)。", true, true)
|
|
else:
|
|
_main.call("_activate_management_mode", &"product")
|
|
var panel := _main.find_child("SupplyManagementPanel", true, false) as SupplyManagementPanel
|
|
panel.select_tab(&"kitchen")
|
|
_main.call("_set_status", "通常予算から育てた宿の試遊です。Spaceで再開し、商品タブで仕入れ・調理・配送を確認できます(保存なし)。", true, true)
|
|
_clock.set_paused(true)
|
|
print("SUPPLY_PREVIEW_READY checks=%d failures=0" % _checks)
|