|
|
|
@@ -33,18 +33,21 @@ func _run() -> void:
|
|
|
|
|
print("P16-01-DISMISS: general employee dismissal")
|
|
|
|
|
_scenes = Scenes.new(_expect)
|
|
|
|
|
_test_ledger_day()
|
|
|
|
|
_test_ledger_saves_same_process()
|
|
|
|
|
_test_top_up_and_insufficient_funds()
|
|
|
|
|
_test_category_refusals()
|
|
|
|
|
_test_queued_commands_for_the_same_employee()
|
|
|
|
|
_test_bankrupt_refusal()
|
|
|
|
|
_test_external_task_refusal()
|
|
|
|
|
_test_reserved_task()
|
|
|
|
|
_test_cleaning_task()
|
|
|
|
|
_test_facility_duty_and_service()
|
|
|
|
|
_test_walking_commute()
|
|
|
|
|
_test_rescue_matrix()
|
|
|
|
|
_test_event_actors()
|
|
|
|
|
_test_defense_refusal()
|
|
|
|
|
_test_commit_failure_rolls_back()
|
|
|
|
|
_test_rescue_cleanup_failure_rolls_back()
|
|
|
|
|
_test_old_save_gains_the_severance_account()
|
|
|
|
|
await _test_dismiss_from_main()
|
|
|
|
|
await process_frame
|
|
|
|
@@ -282,20 +285,52 @@ func _test_queued_commands_for_the_same_employee() -> void:
|
|
|
|
|
var next_minute: int = fixture[&"clock"].absolute_minute + 1
|
|
|
|
|
var assignment: CommandResult = runtime.submit_command(&"set_staff_work_assignment", {&"staff_id": &"staff_eri", &"facility_id": &"", &"template_id": &"NIGHT"},
|
|
|
|
|
&"player", &"staff_eri", 0, next_minute, &"p16.a9.assignment")
|
|
|
|
|
var second: CommandResult = runtime.submit_command(&"dismiss_staff", {&"staff_id": &"staff_eri"}, &"player", &"staff_eri", 0, next_minute, &"p16.a9.second")
|
|
|
|
|
_expect(assignment.is_success() and second.is_success() and (runtime.command_queue.create_snapshot()["pending"] as Array).size() == 2, "an assignment and a second dismissal wait for the next minute")
|
|
|
|
|
var second: CommandResult = runtime.submit_command(&"dismiss_staff", {&"staff_id": &"staff_eri"}, &"player", &"staff_eri", 0, next_minute + 1, &"p16.a9.second")
|
|
|
|
|
_expect(assignment.is_success() and second.is_success() and (runtime.command_queue.create_snapshot()["pending"] as Array).size() == 2, "an assignment and a second dismissal wait in the queue")
|
|
|
|
|
_expect(_scenes.dismiss(fixture, &"staff_eri").is_success(), "A9: the dismissal runs before the queued commands")
|
|
|
|
|
_expect_boundary(fixture, &"staff_eri", "A9")
|
|
|
|
|
fixture[&"clock"].advance_minutes(1)
|
|
|
|
|
var later: CommandResult = runtime.command_queue.get_processed_result(assignment.command_id)
|
|
|
|
|
var again: CommandResult = runtime.command_queue.get_processed_result(second.command_id)
|
|
|
|
|
_expect(later != null and not later.is_success() and StringName(later.details.get(&"reason", &"")) == &"STAFF_NOT_FOUND"
|
|
|
|
|
and runtime.staff_service.get_work_assignment(&"staff_eri").is_empty(), "A9: the queued assignment is refused as STAFF_NOT_FOUND and creates nothing")
|
|
|
|
|
_expect(again != null and not again.is_success() and StringName(again.details.get(&"reason", &"")) == &"NOT_FOUND"
|
|
|
|
|
and _scenes.count_category(runtime, FinancePolicy.SEVERANCE_CATEGORY) == 1, "A9: the queued second dismissal is NOT_FOUND and pays nothing")
|
|
|
|
|
# Each queued command is processed by the Runtime's own command phase (_process_commands),
|
|
|
|
|
# without the rest of the tick, so that its change alone is compared by SPEC 5.c.
|
|
|
|
|
for queued: Array in [[assignment, next_minute, &"STAFF_NOT_FOUND", "the queued assignment"], [second, next_minute + 1, &"NOT_FOUND", "the queued second dismissal"]]:
|
|
|
|
|
var before: Dictionary = runtime.create_snapshot()
|
|
|
|
|
runtime.call("_process_commands", queued[1])
|
|
|
|
|
var after: Dictionary = runtime.create_snapshot()
|
|
|
|
|
var processed: CommandResult = runtime.command_queue.get_processed_result((queued[0] as CommandResult).command_id)
|
|
|
|
|
_expect(processed != null and not processed.is_success() and StringName(processed.details.get(&"reason", &"")) == queued[2], "A9: %s is refused as %s" % [queued[3], queued[2]])
|
|
|
|
|
_expect(processed != null and var_to_bytes(_without_command_record(before)) == var_to_bytes(_without_command_record(after))
|
|
|
|
|
and _only_queued_command_processed(before, after, processed), "A9: %s changes nothing but its own record (5.c)" % queued[3])
|
|
|
|
|
_expect(runtime.staff_service.get_work_assignment(&"staff_eri").is_empty() and _scenes.count_category(runtime, FinancePolicy.SEVERANCE_CATEGORY) == 1,
|
|
|
|
|
"A9: nothing is assigned and the severance is paid once")
|
|
|
|
|
_scenes.free_fixture(fixture)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## SPEC 5.c for a command accepted earlier (A9): its processing takes no new command id
|
|
|
|
|
## and adds only its failed result and one COMMAND_FAILED event.
|
|
|
|
|
func _only_queued_command_processed(before: Dictionary, after: Dictionary, result: CommandResult) -> bool:
|
|
|
|
|
var b: Dictionary = before[&"systems"][&"runtime"]
|
|
|
|
|
var a: Dictionary = after[&"systems"][&"runtime"]
|
|
|
|
|
var counters: Dictionary = (b[&"id_generator"]["counters"] as Dictionary).duplicate()
|
|
|
|
|
counters["domain_event"] = int(counters.get("domain_event", 0)) + 1
|
|
|
|
|
var bq: Dictionary = b[&"command_queue"]
|
|
|
|
|
var aq: Dictionary = a[&"command_queue"]
|
|
|
|
|
var pending: Array = (bq["pending"] as Array).filter(func(row: Dictionary) -> bool: return int(row["command_id"]) != result.command_id)
|
|
|
|
|
var processed: Array = (aq["processed"] as Array).duplicate()
|
|
|
|
|
var index := processed.find(result.to_dictionary())
|
|
|
|
|
if index >= 0:
|
|
|
|
|
processed.remove_at(index)
|
|
|
|
|
var b_ids: Array = b[&"domain_events"]["published_ids"]
|
|
|
|
|
var a_ids: Array = a[&"domain_events"]["published_ids"]
|
|
|
|
|
var prior: Array = b[&"domain_events"]["recent_events"]
|
|
|
|
|
var recent: Array = a[&"domain_events"]["recent_events"]
|
|
|
|
|
var added: Dictionary = recent[-1]
|
|
|
|
|
var kept := recent.slice(0, recent.size() - 1)
|
|
|
|
|
return int(a[&"next_command_id"]) == int(b[&"next_command_id"]) and a[&"id_generator"]["counters"] == counters \
|
|
|
|
|
and (bq["pending"] as Array).size() == pending.size() + 1 and aq["pending"] == pending and aq["in_flight"] == bq["in_flight"] \
|
|
|
|
|
and aq["idempotency"] == bq["idempotency"] and aq["accepted"] == bq["accepted"] and index >= 0 and processed == bq["processed"] \
|
|
|
|
|
and a_ids.size() == b_ids.size() + 1 and added["event_type"] == "COMMAND_FAILED" and int(added["causation_command_id"]) == result.command_id \
|
|
|
|
|
and kept == prior.slice(prior.size() - kept.size())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## R5: after the game ends the command is refused before acceptance.
|
|
|
|
|
func _test_bankrupt_refusal() -> void:
|
|
|
|
|
var fixture := _scenes.new_fixture(16_050, true)
|
|
|
|
@@ -349,6 +384,20 @@ func _test_external_task_refusal() -> void:
|
|
|
|
|
clock.advance_minutes(1)
|
|
|
|
|
_expect(runtime.logistics_requests[&"supply.p16_dismiss"].state == LogisticsRequest.State.DELIVERING, "the porter carries the loaded cargo")
|
|
|
|
|
_expect_executor_refusal(fixture, &"staff_gina", &"EXTERNAL_TASK", "R2 loaded cargo")
|
|
|
|
|
var loaded: Dictionary = runtime.create_snapshot()
|
|
|
|
|
# The return stage (返送), as tests/supply/test_supply_logistics_bridge.gd reaches it:
|
|
|
|
|
# disclosed fixture splitting the cargo into two lots, one spoiling next minute.
|
|
|
|
|
var inventory: Dictionary = runtime.inventory_service.create_snapshot()
|
|
|
|
|
for owner: Dictionary in inventory[&"inventories"]:
|
|
|
|
|
if owner[&"owner_inventory_id"] == "cargo:staff_gina":
|
|
|
|
|
owner[&"stacks"][0][&"quantity"] = 4
|
|
|
|
|
_expect(runtime.inventory_service.restore_snapshot(inventory), "fixture splits the loaded cargo")
|
|
|
|
|
runtime.inventory_service.add_stack(&"cargo:staff_gina", SupplyCatalog.RAW_FOOD, 4, 100, clock.absolute_minute + 1, clock.absolute_minute)
|
|
|
|
|
clock.advance_minutes(1)
|
|
|
|
|
var porter_task: StaffTask = runtime.staff_service.get_task(runtime.staff_service.get_staff(&"staff_gina").current_task_id)
|
|
|
|
|
_expect(porter_task != null and porter_task.task_type == SupplyLogisticsBridge.RETURN_TYPE, "the porter returns the surviving cargo to its source")
|
|
|
|
|
_expect_executor_refusal(fixture, &"staff_gina", &"EXTERNAL_TASK", "R2 return")
|
|
|
|
|
_expect(runtime.restore_snapshot(loaded), "back to the loaded delivery")
|
|
|
|
|
for minute: int in 60:
|
|
|
|
|
if runtime.logistics_requests[&"supply.p16_dismiss"].state == LogisticsRequest.State.COMPLETED: break
|
|
|
|
|
clock.advance_minutes(1)
|
|
|
|
@@ -394,6 +443,37 @@ func _test_reserved_task() -> void:
|
|
|
|
|
_expect(runtime.restore_snapshot(working) and runtime.staff_service.remove_staff(&"tool_worker", &"DISMISSED", true, fixture[&"clock"].absolute_minute).is_success()
|
|
|
|
|
and runtime.staff_service.get_task(&"tool_task").create_snapshot() == dismissed_task and runtime.inventory_service.create_snapshot() == dismissed_inventory,
|
|
|
|
|
"A5: the Task and the inventory end as the existing removal contract leaves them")
|
|
|
|
|
# F7: the reopened reserved Task is taken again by another worker and completed, also when
|
|
|
|
|
# the first reservation, the dismissal and the new reservation fall in one minute.
|
|
|
|
|
var first_minute := int(working[&"clock"][&"absolute_minute"])
|
|
|
|
|
var first_reservation: StringName = reservation_id
|
|
|
|
|
_expect(runtime.restore_snapshot(working) and int(fixture[&"clock"].absolute_minute) == first_minute, "back to the Task reserved at minute %d" % first_minute)
|
|
|
|
|
_expect(_scenes.dismiss(fixture, &"tool_worker").is_success()
|
|
|
|
|
and _scenes.command(fixture, &"register_staff", {&"staff_id": &"tool_worker_2", &"role_id": &"worker", &"options": worker_options}).is_success(),
|
|
|
|
|
"dismiss the worker and register another one in the same minute")
|
|
|
|
|
runtime.staff_service.dispatch_now(first_minute)
|
|
|
|
|
task = runtime.staff_service.get_task(&"tool_task")
|
|
|
|
|
var second_reservation: StringName = task.reservation_ids[0] if task.reservation_ids.size() == 1 else &""
|
|
|
|
|
var reservation: InventoryReservation = runtime.inventory_service.get_reservation(second_reservation)
|
|
|
|
|
_expect(task.assigned_staff_id == &"tool_worker_2" and second_reservation != first_reservation and reservation != null and reservation.is_active()
|
|
|
|
|
and reservation.created_at == first_minute and runtime.inventory_service.get_reservation(first_reservation).state == InventoryReservation.State.RELEASED,
|
|
|
|
|
"F7: in the same minute the other worker reserves the tool anew; the closed reservation is not replayed")
|
|
|
|
|
fixture[&"clock"].advance_minutes(15)
|
|
|
|
|
_expect(runtime.staff_service.get_task(&"tool_task").state == StaffTask.State.COMPLETED and runtime.inventory_service.available_quantity(&"staff.supplies", &"tool/snapshot_kit") == 1,
|
|
|
|
|
"F7: the other worker completes the reopened Task and its tool is released again")
|
|
|
|
|
_expect(runtime.restore_snapshot(working), "back to the reserved Task once more")
|
|
|
|
|
_expect(_scenes.command(fixture, &"register_staff", {&"staff_id": &"tool_worker_2", &"role_id": &"worker", &"options": worker_options}).is_success(), "another worker is ready")
|
|
|
|
|
fixture[&"clock"].set_paused(false)
|
|
|
|
|
var queued: CommandResult = runtime.submit_command(&"dismiss_staff", {&"staff_id": &"tool_worker"}, &"player", &"tool_worker", 0, -1, &"p16.a5.queued")
|
|
|
|
|
fixture[&"clock"].advance_one_minute()
|
|
|
|
|
fixture[&"clock"].set_paused(true)
|
|
|
|
|
task = runtime.staff_service.get_task(&"tool_task")
|
|
|
|
|
var done: CommandResult = runtime.command_queue.get_processed_result(queued.command_id)
|
|
|
|
|
_expect(done != null and done.is_success() and task.assigned_staff_id == &"tool_worker_2" and task.reservation_ids.size() == 1
|
|
|
|
|
and runtime.inventory_service.get_reservation(task.reservation_ids[0]).created_at == int(fixture[&"clock"].absolute_minute),
|
|
|
|
|
"F7: a queued dismissal and the new reservation fall in the same tick")
|
|
|
|
|
fixture[&"clock"].advance_minutes(15)
|
|
|
|
|
_expect(runtime.staff_service.get_task(&"tool_task").state == StaffTask.State.COMPLETED, "F7: that Task is completed too")
|
|
|
|
|
_scenes.free_fixture(fixture)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -460,10 +540,48 @@ func _test_walking_commute() -> void:
|
|
|
|
|
_expect(_scenes.dismiss(fixture, &"staff_akane").is_success(), "A2: the commuting employee is dismissed")
|
|
|
|
|
_expect_boundary(fixture, &"staff_akane", "A2 walking")
|
|
|
|
|
_scenes.free_fixture(fixture)
|
|
|
|
|
_test_elevator_commute()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## A2 at the elevator, A6 the carrier and A7 the patient, through every rescue
|
|
|
|
|
## phase of the elevator rescue; a pair aboard is kept together until a landing.
|
|
|
|
|
## A2 at the elevator: a cleaner commuting to the cleaning room upstairs waits for
|
|
|
|
|
## and rides the service elevator; she is dismissed at each of those phases.
|
|
|
|
|
func _test_elevator_commute() -> void:
|
|
|
|
|
var fixture := _scenes.new_fixture(16_091)
|
|
|
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
|
|
|
var clock: Variant = fixture[&"clock"]
|
|
|
|
|
for spec: Array in [[&"service_elevator", Vector3i(0, 0, 1)], [&"cleaning_room", Vector3i(3, 1, 1)]]:
|
|
|
|
|
_expect(_scenes.command(fixture, &"build_facility", {&"definition_id": spec[0], &"origin": spec[1]}).is_success(), "build %s" % spec[0])
|
|
|
|
|
clock.advance_minutes(400)
|
|
|
|
|
var room: StringName = &""
|
|
|
|
|
for facility: FacilityInstance in runtime.facility_service.get_instances():
|
|
|
|
|
if facility.definition_id == &"cleaning_room": room = facility.instance_id
|
|
|
|
|
_expect(_scenes.command(fixture, &"hire_staff_candidate", {&"candidate_id": &"staff_eri"}).is_success()
|
|
|
|
|
and _scenes.command(fixture, &"set_staff_work_assignment", {&"staff_id": &"staff_eri", &"facility_id": room, &"template_id": &"DAY"}).is_success(),
|
|
|
|
|
"hire staff_eri and send her to the cleaning room upstairs")
|
|
|
|
|
var checkpoints := {}
|
|
|
|
|
for minute: int in 60:
|
|
|
|
|
clock.advance_minutes(1)
|
|
|
|
|
var worker: StaffRuntime = runtime.staff_service.get_staff(&"staff_eri")
|
|
|
|
|
var trip: Dictionary = runtime.elevators.get_actor_trip(&"staff:staff_eri")
|
|
|
|
|
if worker.state == StaffRuntime.State.COMMUTING and not trip.is_empty() and not checkpoints.has(trip[&"state"]):
|
|
|
|
|
checkpoints[trip[&"state"]] = runtime.create_snapshot()
|
|
|
|
|
if worker.state != StaffRuntime.State.COMMUTING:
|
|
|
|
|
break
|
|
|
|
|
_expect(checkpoints.has(PathService.ELEVATOR_REQUEST_QUEUED) and checkpoints.has(PathService.ELEVATOR_REQUEST_BOARDED), "her commute waits for and rides the elevator %s" % [checkpoints.keys()])
|
|
|
|
|
for phase: Variant in checkpoints:
|
|
|
|
|
_expect(runtime.restore_snapshot(checkpoints[phase]), "restore the commute %s" % phase)
|
|
|
|
|
_expect(runtime.staff_service.get_staff(&"staff_eri").state == StaffRuntime.State.COMMUTING, "she is commuting (%s)" % phase)
|
|
|
|
|
_expect(_scenes.dismiss(fixture, &"staff_eri").is_success(), "A2: the commuting employee is dismissed at the elevator (%s)" % phase)
|
|
|
|
|
_expect(runtime.elevators.get_report()[&"trips"].is_empty(), "A2: her elevator request ends with her (%s)" % phase)
|
|
|
|
|
_expect_boundary(fixture, &"staff_eri", "A2 elevator %s" % phase)
|
|
|
|
|
clock.advance_minutes(3)
|
|
|
|
|
_expect(runtime.restore_snapshot(runtime.create_snapshot()), "A2: the elevator keeps working after the dismissal (%s)" % phase)
|
|
|
|
|
_scenes.free_fixture(fixture)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## A6 the carrier and A7 the patient, through every rescue phase of the elevator
|
|
|
|
|
## rescue, the pair aboard included: the dismissed member leaves the cabin at the
|
|
|
|
|
## command and the other one rides on to a real landing (SPEC 6.2.1).
|
|
|
|
|
func _test_rescue_matrix() -> void:
|
|
|
|
|
var fixture := _scenes.new_rescue_fixture()
|
|
|
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
|
|
@@ -474,16 +592,17 @@ func _test_rescue_matrix() -> void:
|
|
|
|
|
if phase in RESCUE_PHASES and not checkpoints.has(phase):
|
|
|
|
|
checkpoints[phase] = runtime.create_snapshot()
|
|
|
|
|
_expect(checkpoints.size() == RESCUE_PHASES.size(), "the rescue passes every phase %s" % [checkpoints.keys()])
|
|
|
|
|
var stops: Array = runtime.elevators.get_report()[&"shafts"][0][&"stops"]
|
|
|
|
|
for phase: String in RESCUE_PHASES:
|
|
|
|
|
if not checkpoints.has(phase):
|
|
|
|
|
continue
|
|
|
|
|
for staff_id: StringName in [&"medic", &"patient"]:
|
|
|
|
|
_expect(runtime.restore_snapshot(checkpoints[phase]), "restore %s" % phase)
|
|
|
|
|
var label := "%s, dismiss the %s" % [phase, staff_id]
|
|
|
|
|
if phase.ends_with("BOARDED/pair"):
|
|
|
|
|
_expect_executor_refusal(fixture, staff_id, &"RESCUE_PAIR_ABOARD", label)
|
|
|
|
|
continue
|
|
|
|
|
var other: StringName = &"patient" if staff_id == &"medic" else &"medic"
|
|
|
|
|
var held: StringName = runtime.staff_service.get_staff(&"medic").current_task_id
|
|
|
|
|
var other_aboard_before := _aboard(runtime, other)
|
|
|
|
|
var preview: Dictionary = runtime.get_staff_dismissal_preview(staff_id)
|
|
|
|
|
var result := _scenes.dismiss(fixture, staff_id)
|
|
|
|
|
_expect(result.is_success(), "%s: dismissed (%s)" % [label, result.message])
|
|
|
|
|
_expect_boundary(fixture, staff_id, label)
|
|
|
|
@@ -491,28 +610,44 @@ func _test_rescue_matrix() -> void:
|
|
|
|
|
if staff_id == &"medic" and phase.begins_with("rescue_pickup"):
|
|
|
|
|
_expect(task.state == StaffTask.State.OPEN and task.assigned_staff_id.is_empty() and runtime.staff_service.get_staff(&"patient").state == StaffRuntime.State.INCAPACITATED,
|
|
|
|
|
"%s: the pickup is reopened for another medic (A6)" % label)
|
|
|
|
|
_expect(preview[&"reopened_task_ids"] == [held] and (preview[&"interrupted_rescue_task_ids"] as Array).is_empty() and result.details[&"reopened_task_ids"] == [held],
|
|
|
|
|
"%s: the preview and the result call the pickup reopened" % label)
|
|
|
|
|
elif staff_id == &"medic":
|
|
|
|
|
_expect(task.state == StaffTask.State.CANCELLED and task.state_reason == &"RESCUE_INTERRUPTED" and result.details[&"interrupted_rescue_task_ids"] == [held]
|
|
|
|
|
and runtime.elevators.get_report()[&"trips"].is_empty(), "%s: the transport is interrupted with its elevator requests (A6)" % label)
|
|
|
|
|
_expect(task.state == StaffTask.State.CANCELLED and task.state_reason == &"RESCUE_INTERRUPTED" and result.details[&"interrupted_rescue_task_ids"] == [held],
|
|
|
|
|
"%s: the transport is interrupted (A6)" % label)
|
|
|
|
|
_expect(preview[&"interrupted_rescue_task_ids"] == [held] and preview[&"interrupted_rescue_roles"] == [&"carrier"] and (preview[&"reopened_task_ids"] as Array).is_empty()
|
|
|
|
|
and (result.details[&"reopened_task_ids"] as Array).is_empty(), "%s: the preview and the result call the transport interrupted, not reopened" % label)
|
|
|
|
|
else:
|
|
|
|
|
_expect(task.state == StaffTask.State.CANCELLED and task.state_reason == &"RESCUE_INTERRUPTED" and runtime.staff_service.get_staff(&"medic").current_task_id.is_empty(),
|
|
|
|
|
"%s: the rescue is interrupted and the medic released (A7)" % label)
|
|
|
|
|
_expect(preview[&"interrupted_rescue_task_ids"] == [held] and preview[&"interrupted_rescue_roles"] == [&"patient"], "%s: the preview names the interrupted rescue" % label)
|
|
|
|
|
# The other one rides on to a real landing when aboard; otherwise no elevator request remains.
|
|
|
|
|
var other_trip: Dictionary = runtime.elevators.get_actor_trip(StringName("staff:" + String(other)))
|
|
|
|
|
if other_aboard_before:
|
|
|
|
|
_expect(other_trip.get(&"state", &"") == PathService.ELEVATOR_REQUEST_BOARDED and not other_trip.has(&"companion_id"),
|
|
|
|
|
"%s: the %s stays aboard alone" % [label, other])
|
|
|
|
|
for minute: int in 40:
|
|
|
|
|
if runtime.elevators.get_actor_trip(StringName("staff:" + String(other))).is_empty():
|
|
|
|
|
break
|
|
|
|
|
fixture[&"clock"].advance_minutes(1)
|
|
|
|
|
_expect(runtime.elevators.get_actor_trip(StringName("staff:" + String(other))).is_empty() and runtime.staff_service.get_staff(other).current_cell in stops,
|
|
|
|
|
"%s: the %s alights at a real landing" % [label, other])
|
|
|
|
|
else:
|
|
|
|
|
_expect(other_trip.is_empty(), "%s: no elevator request remains for the %s" % [label, other])
|
|
|
|
|
if staff_id == &"medic" and not phase.begins_with("rescue_pickup"):
|
|
|
|
|
fixture[&"clock"].advance_minutes(1)
|
|
|
|
|
var restarted: Array = runtime.staff_service.get_tasks().filter(func(t: StaffTask) -> bool: return t.task_type == &"rescue_pickup" and not t.is_terminal() and t.target_entity_id == &"patient")
|
|
|
|
|
_expect(restarted.size() == 1, "%s: the next minute starts a new rescue for the patient (existing rule)" % label)
|
|
|
|
|
else:
|
|
|
|
|
var medic: StaffRuntime = runtime.staff_service.get_staff(&"medic")
|
|
|
|
|
var aboard: bool = runtime.elevators.get_actor_trip(&"staff:medic").get(&"state", &"") == PathService.ELEVATOR_REQUEST_BOARDED
|
|
|
|
|
_expect(task.state == StaffTask.State.CANCELLED and task.state_reason == &"RESCUE_INTERRUPTED" and medic.current_task_id.is_empty(),
|
|
|
|
|
"%s: the rescue is interrupted and the medic released (A7)" % label)
|
|
|
|
|
_expect(aboard == (phase == "rescue_pickup/MOVING/BOARDED/single"), "%s: a medic riding alone rides on to a landing; otherwise no elevator request remains" % label)
|
|
|
|
|
fixture[&"clock"].advance_minutes(3)
|
|
|
|
|
var later: Dictionary = runtime.create_snapshot()
|
|
|
|
|
_expect(runtime.restore_snapshot(later) and runtime.staff_service.validate_invariants()[&"ok"], "%s: three minutes later the game still saves" % label)
|
|
|
|
|
_expect(runtime.restore_snapshot(checkpoints.get("rescue_transport/MOVING/BOARDED/pair", runtime.create_snapshot())), "back to the pair aboard")
|
|
|
|
|
_expect(_scenes.advance_to_rescue_phase(fixture, "rescue_transport/MOVING/-/single") and _scenes.dismiss(fixture, &"medic").is_success(),
|
|
|
|
|
"the pair refusal is a wait: after the landing the medic is dismissed")
|
|
|
|
|
_expect_boundary(fixture, &"medic", "after the landing")
|
|
|
|
|
_expect(restarted.size() == 1, "%s: the patient's rescue starts again (existing rule)" % label)
|
|
|
|
|
fixture[&"clock"].advance_minutes(2)
|
|
|
|
|
var later: Dictionary = runtime.create_snapshot()
|
|
|
|
|
_expect(runtime.restore_snapshot(later) and runtime.staff_service.validate_invariants()[&"ok"], "%s: later the game still saves" % label)
|
|
|
|
|
_scenes.free_fixture(fixture)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _aboard(runtime: Variant, staff_id: StringName) -> bool:
|
|
|
|
|
return runtime.elevators.get_actor_trip(StringName("staff:" + String(staff_id))).get(&"state", &"") == PathService.ELEVATOR_REQUEST_BOARDED
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## A8: the Event's existing missing actor policy handles the dismissed actor at
|
|
|
|
|
## the command boundary (CANCEL; CONTINUE_WITHOUT keeps the other actor).
|
|
|
|
|
func _test_event_actors() -> void:
|
|
|
|
@@ -608,9 +743,123 @@ func _test_old_save_gains_the_severance_account() -> void:
|
|
|
|
|
_scenes.free_fixture(fixture)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## F9: the game's own Runtime and Main. The staff panel shows the severance and
|
|
|
|
|
## the reopened Task before the press, dismisses through its buttons, refuses the
|
|
|
|
|
## UNIQUE and ADULT_CONTRACT rows with their reasons and sends nothing for them.
|
|
|
|
|
## A5 with the SPEC's example: the cleaning Task (facility_clean) a cleaner works is
|
|
|
|
|
## reopened by the dismissal, and the other cleaner takes it and completes it.
|
|
|
|
|
func _test_cleaning_task() -> void:
|
|
|
|
|
var fixture := _scenes.new_fixture(16_075)
|
|
|
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
|
|
|
var clock: Variant = fixture[&"clock"]
|
|
|
|
|
for spec: Array in [[&"cleaning_room", Vector3i(-6, 0, 1)], [&"reception", Vector3i(1, 0, 0)]]:
|
|
|
|
|
_expect(_scenes.command(fixture, &"build_facility", {&"definition_id": spec[0], &"origin": spec[1]}).is_success(), "build %s" % spec[0])
|
|
|
|
|
clock.advance_minutes(240)
|
|
|
|
|
var room: StringName = &""
|
|
|
|
|
var reception: StringName = &""
|
|
|
|
|
for facility: FacilityInstance in runtime.facility_service.get_instances():
|
|
|
|
|
if facility.definition_id == &"cleaning_room": room = facility.instance_id
|
|
|
|
|
if facility.definition_id == &"reception": reception = facility.instance_id
|
|
|
|
|
_expect(_scenes.command(fixture, &"hire_staff_candidate", {&"candidate_id": &"staff_eri"}).is_success()
|
|
|
|
|
and _scenes.command(fixture, &"hire_staff_candidate", {&"candidate_id": &"staff_akane"}).is_success()
|
|
|
|
|
and _scenes.command(fixture, &"register_staff", {&"staff_id": &"spare_cleaner", &"role_id": &"cleaner", &"options": {&"role_tags": [&"cleaner"],
|
|
|
|
|
&"permitted_task_tags": [&"cleaning", &"maintenance"], &"daily_wage": 90}}).is_success(), "two cleaners and a receptionist")
|
|
|
|
|
for row: Array in [[&"staff_eri", room], [&"spare_cleaner", room], [&"staff_akane", reception]]:
|
|
|
|
|
_expect(_scenes.command(fixture, &"set_staff_work_assignment", {&"staff_id": row[0], &"facility_id": row[1], &"template_id": &"DAY"}).is_success(), "assign %s" % row[0])
|
|
|
|
|
clock.advance_minutes(30)
|
|
|
|
|
# 25 completed guest visits soil the reception to the cleaning threshold (75).
|
|
|
|
|
var count := 0
|
|
|
|
|
for batch: int in 4:
|
|
|
|
|
for index: int in (8 if batch < 3 else 1):
|
|
|
|
|
var guest := StringName("p16_cleaning_payer_%d" % count)
|
|
|
|
|
_expect(_scenes.command(fixture, &"register_guest_wallet", {&"guest_id": guest, &"gold": 100}).is_success()
|
|
|
|
|
and _scenes.command(fixture, &"create_service_request", {&"request_id": StringName("p16_cleaning_visit_%d" % count), &"facility_id": reception,
|
|
|
|
|
&"guest_ids": [guest], &"service_type": &"check_in"}).is_success(), "a real reception visit")
|
|
|
|
|
count += 1
|
|
|
|
|
for minute: int in 25:
|
|
|
|
|
clock.advance_one_minute()
|
|
|
|
|
if int(runtime.facility_cleanliness.get_facility(reception)["current"]) == 100 - count: break
|
|
|
|
|
var task: StaffTask = null
|
|
|
|
|
for minute: int in 80:
|
|
|
|
|
for candidate: StaffTask in runtime.staff_service.get_tasks():
|
|
|
|
|
if candidate.task_type == FacilityCleaningBridge.TASK_TYPE and candidate.target_entity_id == reception and not candidate.is_terminal():
|
|
|
|
|
task = candidate
|
|
|
|
|
if task != null and task.state == StaffTask.State.WORKING: break
|
|
|
|
|
clock.advance_one_minute()
|
|
|
|
|
_expect(task != null and task.state == StaffTask.State.WORKING, "a cleaner works the reception's cleaning Task")
|
|
|
|
|
if task == null:
|
|
|
|
|
_scenes.free_fixture(fixture)
|
|
|
|
|
return
|
|
|
|
|
var task_id := task.task_id
|
|
|
|
|
var worker := task.assigned_staff_id
|
|
|
|
|
var other: StringName = &"spare_cleaner" if worker == &"staff_eri" else &"staff_eri"
|
|
|
|
|
var result := _scenes.dismiss(fixture, worker)
|
|
|
|
|
task = runtime.staff_service.get_task(task_id)
|
|
|
|
|
_expect(result.is_success() and result.details[&"reopened_task_ids"] == [task_id] and task.state == StaffTask.State.OPEN and task.assigned_staff_id.is_empty()
|
|
|
|
|
and task.state_reason == &"STAFF_REMOVED", "A5: the cleaning Task is reopened by the dismissal")
|
|
|
|
|
_expect_boundary(fixture, worker, "A5 cleaning")
|
|
|
|
|
var taker: StringName = &""
|
|
|
|
|
for minute: int in 80:
|
|
|
|
|
clock.advance_one_minute()
|
|
|
|
|
task = runtime.staff_service.get_task(task_id)
|
|
|
|
|
if not task.assigned_staff_id.is_empty(): taker = task.assigned_staff_id
|
|
|
|
|
if task.state == StaffTask.State.COMPLETED: break
|
|
|
|
|
_expect(taker == other and task.state == StaffTask.State.COMPLETED and int(runtime.facility_cleanliness.get_facility(reception)["current"]) == 100,
|
|
|
|
|
"A5: the other cleaner takes the reopened cleaning Task and completes it (cleanliness 100)")
|
|
|
|
|
_scenes.free_fixture(fixture)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## F8 in the same process: every ledger save point (before and right after the
|
|
|
|
|
## dismissal, a minute later and around the day 2 close) is written to disk, read back
|
|
|
|
|
## by this process and played to the end, exactly as without the save.
|
|
|
|
|
func _test_ledger_saves_same_process() -> void:
|
|
|
|
|
var straight := _scenes.new_ledger_fixture()
|
|
|
|
|
_scenes.play_ledger(straight, 360, Scenes.LAST_MINUTE, Callable())
|
|
|
|
|
var straight_economy := _scenes.economy_bytes(straight[&"runtime"])
|
|
|
|
|
var straight_state := _scenes.state_value(straight[&"runtime"], straight[&"clock"])
|
|
|
|
|
var saving := _scenes.new_ledger_fixture()
|
|
|
|
|
var paths := {}
|
|
|
|
|
_scenes.play_ledger(saving, 360, Scenes.LAST_MINUTE, func(minute: int, phase: String) -> void:
|
|
|
|
|
if [minute, phase] in Scenes.SAVE_POINTS:
|
|
|
|
|
var path := "user://tests/p16-same-process-%d-%s" % [minute, phase]
|
|
|
|
|
_expect(bool(saving[&"runtime"].save_to_path(path).get(&"ok", false)), "same process: save at %d %s" % [minute, phase])
|
|
|
|
|
paths["%d-%s" % [minute, phase]] = path)
|
|
|
|
|
for point: Array in Scenes.SAVE_POINTS:
|
|
|
|
|
var name := "%d-%s" % [point[0], point[1]]
|
|
|
|
|
var resumed := _scenes.new_ledger_fixture()
|
|
|
|
|
var runtime: Variant = resumed[&"runtime"]
|
|
|
|
|
_expect(bool(runtime.load_from_path(paths.get(name, "")).get(&"ok", false)) and int(resumed[&"clock"].absolute_minute) == int(point[0]),
|
|
|
|
|
"F8 same process %s: the save loads at its minute" % name)
|
|
|
|
|
_expect(runtime.staff_service.has_staff(Scenes.DISMISSED) == (name == "1500-before"), "F8 same process %s: the employee is there exactly before the command" % name)
|
|
|
|
|
_scenes.play_ledger(resumed, int(point[0]), Scenes.LAST_MINUTE, Callable())
|
|
|
|
|
_expect(_scenes.economy_bytes(runtime) == straight_economy and _scenes.state_value(runtime, resumed[&"clock"]) == straight_state,
|
|
|
|
|
"F8 same process %s: resuming gives exactly the uninterrupted ledger, employees and Tasks" % name)
|
|
|
|
|
_expect(_scenes.count_category(runtime, FinancePolicy.SEVERANCE_CATEGORY) == 1 and runtime.staff_service.get_departed_unpaid_wages(Scenes.DISMISSED) == 0
|
|
|
|
|
and _payroll_payments(runtime, 2) == {Scenes.DISMISSED: 120, Scenes.KEPT: 180}, "F8 same process %s: the severance and the held 120 are paid once" % name)
|
|
|
|
|
_scenes.free_fixture(resumed)
|
|
|
|
|
SaveService.delete_save(paths.get(name, ""))
|
|
|
|
|
_scenes.free_fixture(straight)
|
|
|
|
|
_scenes.free_fixture(saving)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## SPEC 4.1 d: a rescue cleanup that cannot complete rolls the whole dismissal back
|
|
|
|
|
## (5.c). Fixture: the medic's pickup names a Reservation that does not exist, so the
|
|
|
|
|
## existing cancellation refuses it.
|
|
|
|
|
func _test_rescue_cleanup_failure_rolls_back() -> void:
|
|
|
|
|
var fixture := _scenes.new_rescue_fixture()
|
|
|
|
|
var runtime: Variant = fixture[&"runtime"]
|
|
|
|
|
_expect(_scenes.advance_to_rescue_phase(fixture, "rescue_pickup/MOVING/-/single"), "the medic walks to the patient")
|
|
|
|
|
var task: StaffTask = runtime.staff_service.get_task(runtime.staff_service.get_staff(&"medic").current_task_id)
|
|
|
|
|
task.reservation_ids.append(&"p16.fixture.missing_reservation")
|
|
|
|
|
_expect_executor_refusal(fixture, &"patient", &"RESCUE_CLEANUP_FAILED", "rescue cleanup failure")
|
|
|
|
|
_expect(runtime.staff_service.has_staff(&"patient") and _scenes.count_category(runtime, FinancePolicy.SEVERANCE_CATEGORY) == 0, "the patient stays and no severance is posted")
|
|
|
|
|
_scenes.free_fixture(fixture)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## F9: the game's own Runtime and Main. The staff panel shows the severance, the
|
|
|
|
|
## reopened Task and the held wages before the press, dismisses through its buttons,
|
|
|
|
|
## shows the result and a refusal that arrives after the press, and for every refusal
|
|
|
|
|
## row (R1, R2, R3, R5, R7, R8, R9) keeps the button disabled with the reason and sends
|
|
|
|
|
## nothing on a forced press (5.a, whole save bytes). Input route: the Buttons' pressed
|
|
|
|
|
## signals and Main's key handler, as the other Main suites drive them; no OS or
|
|
|
|
|
## viewport input is claimed.
|
|
|
|
|
func _test_dismiss_from_main() -> void:
|
|
|
|
|
var runtime: Variant = root.get_node("SimulationRuntime")
|
|
|
|
|
var clock: Variant = root.get_node("GameClock")
|
|
|
|
@@ -626,6 +875,7 @@ func _test_dismiss_from_main() -> void:
|
|
|
|
|
await process_frame
|
|
|
|
|
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
|
|
|
clock.set_paused(true)
|
|
|
|
|
var fixture := {&"runtime": runtime, &"clock": clock, &"state": state}
|
|
|
|
|
var key := InputEventKey.new()
|
|
|
|
|
key.keycode = KEY_H
|
|
|
|
|
key.pressed = true
|
|
|
|
@@ -635,9 +885,9 @@ func _test_dismiss_from_main() -> void:
|
|
|
|
|
_expect(hire != null, "the hiring panel lists %s" % candidate)
|
|
|
|
|
if hire != null:
|
|
|
|
|
hire.pressed.emit()
|
|
|
|
|
_expect(runtime.submit_command(&"register_staff", {&"staff_id": &"adult_contract", &"role_id": &"attendant",
|
|
|
|
|
_expect(_scenes.command(fixture, &"register_staff", {&"staff_id": &"adult_contract", &"role_id": &"attendant",
|
|
|
|
|
&"options": {&"employment_kind": &"ADULT_CONTRACT", &"daily_wage": 100}}).is_success(), "an ADULT_CONTRACT fixture row")
|
|
|
|
|
_expect(runtime.submit_command(&"create_staff_task", {&"task_id": &"ui_task", &"task_type": &"inspect", &"priority": 90, &"options": {
|
|
|
|
|
_expect(_scenes.command(fixture, &"create_staff_task", {&"task_id": &"ui_task", &"task_type": &"inspect", &"priority": 90, &"options": {
|
|
|
|
|
&"target_cell": runtime.staff_service.get_staff(&"staff_eri").current_cell, &"required_role_tags": [&"cleaner"], &"task_tags": [&"cleaning"], &"estimated_minutes": 120}}).is_success(),
|
|
|
|
|
"a Task for the cleaner")
|
|
|
|
|
clock.advance_minutes(1)
|
|
|
|
@@ -648,12 +898,12 @@ func _test_dismiss_from_main() -> void:
|
|
|
|
|
_expect(panel.visible and panel.roster_buttons.size() == 4, "the hired-staff panel lists four employees")
|
|
|
|
|
(panel.roster_buttons[&"staff_akane"] as Button).pressed.emit()
|
|
|
|
|
_expect(not panel.dismiss_button.disabled and panel.dismiss_button.tooltip_text.contains("退職金 G360"), "the receptionist row offers the dismissal with severance G360")
|
|
|
|
|
var untouched: Dictionary = runtime.create_snapshot()
|
|
|
|
|
var untouched := var_to_bytes(runtime.create_snapshot())
|
|
|
|
|
panel.dismiss_button.pressed.emit()
|
|
|
|
|
_expect(panel.dismiss_confirm_box.visible and panel.dismiss_confirm_label.text.contains("退職金 G360(日給の3日分)") and runtime.create_snapshot() == untouched,
|
|
|
|
|
"pressing 解雇する… shows the severance and sends nothing (5.a)")
|
|
|
|
|
_expect(panel.dismiss_confirm_box.visible and panel.dismiss_confirm_label.text.contains("退職金 G360(日給の3日分)") and var_to_bytes(runtime.create_snapshot()) == untouched,
|
|
|
|
|
"pressing 解雇する… shows the severance and sends nothing (5.a, bytes)")
|
|
|
|
|
panel.dismiss_cancel_button.pressed.emit()
|
|
|
|
|
_expect(not panel.dismiss_confirm_box.visible and runtime.create_snapshot() == untouched, "やめる closes the confirmation without a command")
|
|
|
|
|
_expect(not panel.dismiss_confirm_box.visible and var_to_bytes(runtime.create_snapshot()) == untouched, "やめる closes the confirmation without a command (bytes)")
|
|
|
|
|
panel.dismiss_button.pressed.emit()
|
|
|
|
|
panel.dismiss_confirm_button.pressed.emit()
|
|
|
|
|
_expect(not runtime.staff_service.has_staff(&"staff_akane") and not panel.roster_buttons.has(&"staff_akane") and panel.selected_staff_id != &"staff_akane",
|
|
|
|
@@ -669,12 +919,113 @@ func _test_dismiss_from_main() -> void:
|
|
|
|
|
clock.advance_minutes(1)
|
|
|
|
|
clock.set_paused(true)
|
|
|
|
|
_expect(not runtime.staff_service.has_staff(&"staff_eri") and runtime.staff_service.get_task(&"ui_task").state == StaffTask.State.OPEN
|
|
|
|
|
and panel.result_label.text.contains("解雇しました"), "the queued dismissal completes and reopens her Task")
|
|
|
|
|
for row: Array in [[&"heroine_anri", "ユニーク:雇用・維持費無料 / 解雇・売却不可"], [&"adult_contract", "解雇不可:ギルドで雇った一般従業員だけを解雇できます。"]]:
|
|
|
|
|
(panel.roster_buttons[row[0]] as Button).pressed.emit()
|
|
|
|
|
var before: Dictionary = runtime.create_snapshot()
|
|
|
|
|
_expect(panel.dismiss_button.disabled and panel.staff_detail_label.text.contains(row[1]), "%s: the button is disabled and the reason shown" % row[0])
|
|
|
|
|
panel.dismiss_button.pressed.emit()
|
|
|
|
|
_expect(runtime.create_snapshot() == before and not panel.dismiss_confirm_box.visible and runtime.staff_service.has_staff(row[0]), "%s: a forced press sends nothing (5.a)" % row[0])
|
|
|
|
|
and panel.result_label.text.contains("解雇しました") and panel.result_label.text.contains("持っていた作業は開き直しました"), "the queued dismissal completes and reopens her Task")
|
|
|
|
|
for row: Array in [[&"heroine_anri", "ユニーク:雇用・維持費無料 / 解雇・売却不可", "R1"], [&"adult_contract", "ギルドで雇った一般従業員だけを解雇できます。", "R7"]]:
|
|
|
|
|
_expect_refused_row(panel, runtime, row[0], row[1], row[2])
|
|
|
|
|
# R9: the defender of an unfinished raid.
|
|
|
|
|
var objective_build := _scenes.command(fixture, &"build_facility", {&"definition_id": &"reception", &"origin": Vector3i(-8, 0, 0)})
|
|
|
|
|
_expect(objective_build.is_success(), "build the raid objective (%s %s)" % [objective_build.message, objective_build.details])
|
|
|
|
|
clock.advance_minutes(runtime.definition_registry.get_facility(&"reception").build_minutes)
|
|
|
|
|
var objective: StringName = &""
|
|
|
|
|
for facility: FacilityInstance in runtime.facility_service.get_instances():
|
|
|
|
|
if facility.definition_id == &"reception": objective = facility.instance_id
|
|
|
|
|
_expect(_scenes.command(fixture, &"hire_staff_candidate", {&"candidate_id": &"staff_klaus"}).is_success()
|
|
|
|
|
and runtime.submit_command(&"create_defense_raid", {&"raid_id": &"ui.raid", &"definition": {&"source_id": &"runtime_test", &"entry_points": [&"gate.north"],
|
|
|
|
|
&"objective_ids": [objective], &"round_minutes": 5, &"max_duration_minutes": 180, &"retreat_policy": &"HOLD", &"defender_retreat_threshold": 0,
|
|
|
|
|
&"waves": [{&"wave_id": &"wave.one", &"threat_strength": 15, &"retreat_threshold": 0, &"max_rounds": 1, &"entry_points": [&"gate.north"]}]}}, &"defense_controller").is_success()
|
|
|
|
|
and runtime.submit_command(&"register_defense_combatant", {&"raid_id": &"ui.raid", &"actor_id": &"staff_klaus", &"side": &"DEFENDER", &"combat": 90,
|
|
|
|
|
&"max_health": 100, &"options": {}}, &"defense_controller").is_success(), "the guard defends an unfinished raid")
|
|
|
|
|
_expect_refused_row(panel, runtime, &"staff_klaus", "襲撃の防衛に参加中は解雇できません。", "R9")
|
|
|
|
|
# R2: the porter carrying supply logistics.
|
|
|
|
|
for spec: Array in [[&"warehouse", Vector3i(-8, 0, 1)], [&"kitchen", Vector3i(4, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)]]:
|
|
|
|
|
var built := _scenes.command(fixture, &"build_facility", {&"definition_id": spec[0], &"origin": spec[1]})
|
|
|
|
|
_expect(built.is_success(), "build %s (%s %s)" % [spec[0], built.message, built.details])
|
|
|
|
|
clock.advance_minutes(180)
|
|
|
|
|
var source: StringName = &""
|
|
|
|
|
var destination: StringName = &""
|
|
|
|
|
for facility: FacilityInstance in runtime.facility_service.get_instances():
|
|
|
|
|
if facility.definition_id == &"warehouse": source = facility.instance_id
|
|
|
|
|
if facility.definition_id == &"kitchen": destination = facility.instance_id
|
|
|
|
|
_expect(not source.is_empty() and not destination.is_empty() and _scenes.command(fixture, &"hire_staff_candidate", {&"candidate_id": &"staff_gina"}).is_success()
|
|
|
|
|
and _scenes.command(fixture, &"set_staff_work_assignment", {&"staff_id": &"staff_gina", &"facility_id": source, &"template_id": &"DAY"}).is_success(), "the porter works at the warehouse")
|
|
|
|
|
if source.is_empty() or destination.is_empty():
|
|
|
|
|
var early: Dictionary = await FixtureCleanup.free_owner_and_wait(self, main)
|
|
|
|
|
_expect(bool(early.get(&"passed", false)), "ordinary Main release with audio cleanup")
|
|
|
|
|
return
|
|
|
|
|
for minute: int in 40:
|
|
|
|
|
if runtime.staff_service.get_staff(&"staff_gina").state == StaffRuntime.State.AVAILABLE: break
|
|
|
|
|
clock.advance_minutes(1)
|
|
|
|
|
_expect(_scenes.command(fixture, &"purchase_supplies", {&"warehouse_id": source, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 8}).is_success()
|
|
|
|
|
and _scenes.command(fixture, &"create_logistics_request", {&"request_id": &"supply.p16_ui", &"source_inventory_id": runtime.facility_service.get_instance(source).local_inventory_id,
|
|
|
|
|
&"destination_inventory_id": runtime.facility_service.get_instance(destination).local_inventory_id, &"item_requirements": {SupplyCatalog.RAW_FOOD: 8}}).is_success(), "a supply delivery")
|
|
|
|
|
for minute: int in 40:
|
|
|
|
|
if runtime.staff_service.is_externally_driven_task(runtime.staff_service.get_staff(&"staff_gina").current_task_id): break
|
|
|
|
|
clock.advance_minutes(1)
|
|
|
|
|
_expect_refused_row(panel, runtime, &"staff_gina", "物流の受け取り・配達・返送の途中です。", "R2")
|
|
|
|
|
# R3: a Task Reservation that cannot be released (disclosed fixture: the tool is consumed).
|
|
|
|
|
_expect(_scenes.command(fixture, &"register_staff", {&"staff_id": &"ui_tool_worker", &"role_id": &"worker", &"options": {&"state": StaffRuntime.State.AVAILABLE,
|
|
|
|
|
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT), &"role_tags": [&"worker"], &"permitted_task_tags": [&"work"], &"daily_wage": 100}}).is_success()
|
|
|
|
|
and _scenes.command(fixture, &"add_inventory_stack", {&"owner_inventory_id": &"staff.supplies", &"item_id": &"tool/snapshot_kit", &"quantity": 1}).is_success()
|
|
|
|
|
and _scenes.command(fixture, &"create_staff_task", {&"task_id": &"ui_tool_task", &"task_type": &"inspect", &"priority": 95, &"options": {
|
|
|
|
|
&"target_cell": Vector3i(0, 0, WorldService.LAYER_FRONT), &"required_role_tags": [&"worker"], &"required_items": {&"tool/snapshot_kit": 1},
|
|
|
|
|
&"task_tags": [&"work"], &"estimated_minutes": 600}}).is_success(), "a worker and a Task with a tool")
|
|
|
|
|
clock.advance_minutes(1)
|
|
|
|
|
var tool_task: StaffTask = runtime.staff_service.get_task(&"ui_tool_task")
|
|
|
|
|
var before_consume: Dictionary = runtime.create_snapshot()
|
|
|
|
|
_expect(tool_task.assigned_staff_id == &"ui_tool_worker" and tool_task.reservation_ids.size() == 1
|
|
|
|
|
and bool(runtime.inventory_service.consume_reservation(tool_task.reservation_ids[0], &"p16.ui.consume", clock.absolute_minute).get(&"ok", false)), "fixture consumes the reserved tool")
|
|
|
|
|
_expect_refused_row(panel, runtime, &"ui_tool_worker", "作業の予約を解けないため解雇できません。", "R3")
|
|
|
|
|
_expect(runtime.restore_snapshot(before_consume), "the fixture's consumption is undone after the check")
|
|
|
|
|
panel.refresh()
|
|
|
|
|
# A refusal that arrives after the press: the funds are spent before the queued command runs.
|
|
|
|
|
_expect(_scenes.command(fixture, &"register_staff", {&"staff_id": &"ui_late", &"role_id": &"cleaner", &"options": {&"daily_wage": 100}}).is_success(), "an employee to dismiss")
|
|
|
|
|
panel.refresh()
|
|
|
|
|
(panel.roster_buttons[&"ui_late"] as Button).pressed.emit()
|
|
|
|
|
panel.dismiss_button.pressed.emit()
|
|
|
|
|
clock.set_paused(false)
|
|
|
|
|
panel.dismiss_confirm_button.pressed.emit()
|
|
|
|
|
_expect(runtime.staff_service.has_staff(&"ui_late") and panel.result_label.text.contains("受理しました"), "the dismissal is accepted for the next minute")
|
|
|
|
|
_expect(_scenes.drain_front(fixture, "ui.late") and runtime.economy_service.get_balance(BACK) < 300, "disclosed fixture: the funds are spent before the command runs")
|
|
|
|
|
clock.advance_minutes(1)
|
|
|
|
|
clock.set_paused(true)
|
|
|
|
|
_expect(runtime.staff_service.has_staff(&"ui_late") and panel.result_label.text.contains("退職金 G300 を払えません"), "the result shows the refusal after the press: %s" % panel.result_label.text)
|
|
|
|
|
_expect_refused_row(panel, runtime, &"ui_late", "退職金 G300 を払えません", "R8")
|
|
|
|
|
# The held unpaid wages: the first 00:00 close finds no funds, so the day's wage stays unpaid (A09).
|
|
|
|
|
clock.advance_minutes(1441 - int(clock.absolute_minute))
|
|
|
|
|
_expect(runtime.staff_service.get_staff(&"ui_late").unpaid_wages == 100, "the first close leaves her daily wage 100 unpaid")
|
|
|
|
|
for account: StringName in [Scenes.FRONT_SALES, Scenes.PAYER]:
|
|
|
|
|
runtime.economy_service.register_account(account)
|
|
|
|
|
_expect(runtime.economy_service.initialize_account(Scenes.PAYER, 100000, clock.absolute_minute, &"p16.ui.payer").is_success()
|
|
|
|
|
and _scenes.sale(fixture, FRONT, 1000, "ui.sale"), "disclosed fixture: a sale refills the front fund")
|
|
|
|
|
panel.refresh()
|
|
|
|
|
(panel.roster_buttons[&"ui_late"] as Button).pressed.emit()
|
|
|
|
|
panel.dismiss_button.pressed.emit()
|
|
|
|
|
_expect(panel.dismiss_confirm_label.text.contains("未払い G100 は退職金と別に次の0時に精算します"), "the confirmation shows the held wages: %s" % panel.dismiss_confirm_label.text)
|
|
|
|
|
panel.dismiss_confirm_button.pressed.emit()
|
|
|
|
|
_expect(not runtime.staff_service.has_staff(&"ui_late") and panel.result_label.text.contains("未払い G100 は次の0時に精算します"), "the result shows the held wages: %s" % panel.result_label.text)
|
|
|
|
|
# R5: after the game ends (three actual operating losses with no funds).
|
|
|
|
|
if not bool(runtime.get(&"_operations_enabled")):
|
|
|
|
|
_expect(bool(runtime.enable_production_operations().get(&"ok", false)), "production Operations")
|
|
|
|
|
for minute: int in 6000:
|
|
|
|
|
if runtime.operations_coordinator.is_terminal(): break
|
|
|
|
|
if posmod(int(clock.absolute_minute), 1440) == 1439:
|
|
|
|
|
_scenes.drain_front(fixture, "ui.bankrupt.%d" % clock.absolute_minute)
|
|
|
|
|
clock.advance_minutes(1)
|
|
|
|
|
_expect(runtime.operations_coordinator.is_terminal(), "the game ends (BANKRUPT) after three actual operating losses")
|
|
|
|
|
_expect_refused_row(panel, runtime, &"ui_tool_worker", "運営終了後は解雇できません。", "R5")
|
|
|
|
|
var cleanup: Dictionary = await FixtureCleanup.free_owner_and_wait(self, main)
|
|
|
|
|
_expect(bool(cleanup.get(&"passed", false)), "ordinary Main release with audio cleanup")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _expect_refused_row(panel: StaffManagementPanel, runtime: Variant, staff_id: StringName, text: String, label: String) -> void:
|
|
|
|
|
panel.refresh()
|
|
|
|
|
_expect(panel.roster_buttons.has(staff_id), "%s: the row is listed" % label)
|
|
|
|
|
if not panel.roster_buttons.has(staff_id):
|
|
|
|
|
return
|
|
|
|
|
(panel.roster_buttons[staff_id] as Button).pressed.emit()
|
|
|
|
|
var before := var_to_bytes(runtime.create_snapshot())
|
|
|
|
|
_expect(panel.dismiss_button.disabled and panel.staff_detail_label.text.contains(text) and panel.dismiss_button.tooltip_text.contains(text),
|
|
|
|
|
"%s: the button is disabled and the reason is shown (%s)" % [label, panel.dismiss_button.tooltip_text])
|
|
|
|
|
panel.dismiss_button.pressed.emit()
|
|
|
|
|
_expect(var_to_bytes(runtime.create_snapshot()) == before and not panel.dismiss_confirm_box.visible and runtime.staff_service.has_staff(staff_id),
|
|
|
|
|
"%s: a forced press sends nothing (5.a, whole save bytes)" % label)
|
|
|
|
|