P16-01-DISMISS: 一般従業員の通常操作での解雇(退職金を含む。P16-01 の補助の作業。原単位への加算 0)

This commit is contained in:
Agent Control Runner
2026-10-25 17:01:25 +09:00
parent a073ccb59b
commit f5fe219674
4 changed files with 252 additions and 57 deletions
+218 -50
View File
@@ -48,6 +48,7 @@ func _run() -> void:
_test_defense_refusal()
_test_commit_failure_rolls_back()
_test_rescue_cleanup_failure_rolls_back()
_test_rollback_branches()
_test_old_save_gains_the_severance_account()
await _test_dismiss_from_main()
await process_frame
@@ -853,13 +854,74 @@ func _test_rescue_cleanup_failure_rolls_back() -> void:
_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.
## SPEC 4.1 d, the guarded branches the other scenes do not fail. Each changes nothing
## but the command record (5.c).
## (1) The removal contract cannot release the held Task's Reservation although the
## pre-check passes. Disclosed fixture: the release operation id is already bound
## to another inventory reservation, so InventoryService refuses the release.
## (2) The seat of a boarded rescue pair cannot be released. Disclosed fixture: the
## medic's Path request was completed beforehand through the public PathService
## call, a state the save check refuses.
## (3) The severance cannot be posted after the dismissed carrier's seat was released,
## so the Path request, the cabin seat and the pairing are restored with the rest.
func _test_rollback_branches() -> void:
var fixture := _scenes.new_fixture(16_150)
var runtime: Variant = fixture[&"runtime"]
var minute: int = fixture[&"clock"].absolute_minute
_expect(_scenes.command(fixture, &"register_staff", {&"staff_id": &"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": 2}).is_success()
and _scenes.command(fixture, &"create_staff_task", {&"task_id": &"tool_task", &"task_type": &"inspect", &"priority": 90, &"options": {
&"target_cell": Vector3i(0, 0, WorldService.LAYER_FRONT), &"required_role_tags": [&"worker"], &"required_items": {&"tool/snapshot_kit": 1},
&"task_tags": [&"work"], &"estimated_minutes": 10}}).is_success(), "a worker, two tools and a Task that needs one")
fixture[&"clock"].advance_one_minute()
minute = fixture[&"clock"].absolute_minute
var task: StaffTask = runtime.staff_service.get_task(&"tool_task")
_expect(task.state == StaffTask.State.WORKING and task.reservation_ids.size() == 1, "the worker holds the Task and its Reservation")
var held: StringName = task.reservation_ids[0] if task.reservation_ids.size() == 1 else &""
var bound: Dictionary = runtime.inventory_service.reserve_item(&"staff.supplies", &"tool/snapshot_kit", 1, &"p16.fixture", &"staff_task", minute, minute + 60,
StringName("staff.task.release.%s.%s" % [task.task_id, held]))
_expect(bool(bound.get(&"ok", false)) and bool(runtime.get_staff_dismissal_preview(&"tool_worker")[&"can_dismiss"]),
"disclosed fixture: the release operation id is bound to another reservation; the pre-check still passes")
var refused := _expect_executor_refusal(fixture, &"tool_worker", &"RESERVATION_RELEASE_FAILED", "(1) the removal contract's release fails")
_expect(refused.message == "Staff removal could not release Task Reservations." and runtime.staff_service.has_staff(&"tool_worker")
and runtime.inventory_service.get_reservation(held).is_active() and runtime.staff_service.get_task(&"tool_task").assigned_staff_id == &"tool_worker",
"(1) the existing contract's text; the employee, the Task and its Reservation are as before")
_scenes.free_fixture(fixture)
for scene: Array in [["(2) the seat release fails", &"RESCUE_SEAT_RELEASE_FAILED"], ["(3) the severance fails after the seat release", &"SEVERANCE_POST_FAILED"]]:
var rescue := _scenes.new_rescue_fixture()
var rescue_runtime: Variant = rescue[&"runtime"]
_expect(_scenes.advance_to_rescue_phase(rescue, "rescue_transport/MOVING/BOARDED/pair"), "%s: the pair rides the elevator" % scene[0])
var trip: Dictionary = rescue_runtime.elevators.get_actor_trip(&"staff:medic")
if scene[1] == &"RESCUE_SEAT_RELEASE_FAILED":
_expect(rescue_runtime.path_service.complete_elevator_request(StringName(trip.get(&"connector_id", &"")), StringName(trip.get(&"request_id", &""))),
"%s: disclosed fixture: the medic's Path request is completed beforehand" % scene[0])
else:
var next_id := int(rescue_runtime.create_snapshot()[&"systems"][&"runtime"][&"next_command_id"])
_expect(rescue_runtime.economy_service.transfer(Scenes.PAYER, Scenes.FRONT_SALES, 1, rescue[&"clock"].absolute_minute, &"TRANSFER", &"p16.fixture",
StringName("staff.severance.%d" % next_id)).is_success(), "%s: disclosed fixture: the dismissal's posting key is already used" % scene[0])
_expect_executor_refusal(rescue, &"medic", scene[1], scene[0])
var medic_trip: Dictionary = rescue_runtime.elevators.get_actor_trip(&"staff:medic")
var patient_trip: Dictionary = rescue_runtime.elevators.get_actor_trip(&"staff:patient")
_expect(rescue_runtime.staff_service.has_staff(&"medic") and medic_trip == trip and StringName(medic_trip.get(&"companion_id", &"")) == &"staff:patient"
and StringName(patient_trip.get(&"companion_id", &"")) == &"staff:medic", "%s: the medic is still aboard with the patient" % scene[0])
if scene[1] == &"SEVERANCE_POST_FAILED":
var saved: Dictionary = rescue_runtime.create_snapshot()
_expect(rescue_runtime.restore_snapshot(saved) and rescue_runtime.create_snapshot() == saved, "%s: the restored game saves and restores exactly" % scene[0])
_scenes.free_fixture(rescue)
## F9: the game's own Runtime and Main, driven by viewport input. Main's key and every
## button of the dismissal flow (the hiring buttons, 雇用済み, the roster rows, 解雇する…,
## やめる, 解雇を確定) are pressed through root.push_input in the viewport's own
## coordinates after the control is scrolled into view (_click, the form of
## tests/ui/test_business_management.gd _click_principal_button). The panel shows the
## severance, the hidden-fund top-up, the reopened Task, the rescue the command
## interrupts and the held wages before the press, dismisses, 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 when it is
## clicked (5.a, whole save bytes). This is viewport input, not OS input; the Windows
## device, IME, OS scaling and the same-candidate device check are later (SPEC 10).
func _test_dismiss_from_main() -> void:
var runtime: Variant = root.get_node("SimulationRuntime")
var clock: Variant = root.get_node("GameClock")
@@ -876,15 +938,14 @@ func _test_dismiss_from_main() -> void:
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
main.call("_unhandled_key_input", key)
await _press_key(KEY_H)
_expect((main.get("staff_candidate_panel") as Control).visible, "the H key opens the hiring panel")
for candidate: StringName in [&"staff_akane", &"staff_eri", &"heroine_anri"]:
var hire := (main.get("staff_candidate_buttons") as Dictionary).get(candidate) as Button
_expect(hire != null, "the hiring panel lists %s" % candidate)
if hire != null:
hire.pressed.emit()
await _click(hire)
_expect(runtime.staff_service.has_staff(candidate), "a click hires %s" % candidate)
_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(_scenes.command(fixture, &"create_staff_task", {&"task_id": &"ui_task", &"task_type": &"inspect", &"priority": 90, &"options": {
@@ -893,35 +954,65 @@ func _test_dismiss_from_main() -> void:
clock.advance_minutes(1)
var eri_task: StringName = runtime.staff_service.get_staff(&"staff_eri").current_task_id
_expect(eri_task == &"ui_task", "the cleaner holds the Task (%s)" % eri_task)
(main.get("staff_management_toggle_button") as Button).pressed.emit()
await _click(main.get("staff_management_toggle_button") as Button)
var panel: StaffManagementPanel = main.get("staff_management_panel")
_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")
_expect(panel.visible and panel.roster_buttons.size() == 4, "a click on 雇用済み shows the four employees")
await _click(panel.roster_buttons[&"staff_akane"])
_expect(panel.selected_staff_id == &"staff_akane" and not panel.dismiss_button.disabled and panel.dismiss_button.tooltip_text.contains("退職金 G360"),
"a click on her row offers the dismissal with severance G360")
var untouched := var_to_bytes(runtime.create_snapshot())
panel.dismiss_button.pressed.emit()
await _click(panel.dismiss_button)
_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 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()
"a click on 解雇する… shows the severance and sends nothing (5.a, bytes)")
await _click(panel.dismiss_cancel_button)
_expect(not panel.dismiss_confirm_box.visible and var_to_bytes(runtime.create_snapshot()) == untouched, "a click on やめる closes the confirmation without a command (bytes)")
await _click(panel.dismiss_button)
await _click(panel.dismiss_confirm_button)
_expect(not runtime.staff_service.has_staff(&"staff_akane") and not panel.roster_buttons.has(&"staff_akane") and panel.selected_staff_id != &"staff_akane",
"解雇を確定 dismisses her; the roster and the selection follow")
"a click on 解雇を確定 dismisses her; the roster and the selection follow")
_expect(panel.result_label.text.contains("解雇しました") and panel.result_label.text.contains("G360"), "the result shows the paid severance: %s" % panel.result_label.text)
_expect(_scenes.count_category(runtime, FinancePolicy.SEVERANCE_CATEGORY, &"staff_akane") == 1, "the panel's command paid the severance once")
(panel.roster_buttons[&"staff_eri"] as Button).pressed.emit()
panel.dismiss_button.pressed.emit()
await _click(panel.roster_buttons[&"staff_eri"])
await _click(panel.dismiss_button)
_expect(panel.dismiss_confirm_label.text.contains("持っている作業は開き直して") and panel.dismiss_confirm_label.text.contains("G270"), "the cleaner's confirmation names the reopened Task and G270")
clock.set_paused(false)
panel.dismiss_confirm_button.pressed.emit()
await _click(panel.dismiss_confirm_button, true)
_expect(runtime.staff_service.has_staff(&"staff_eri") and panel.dismiss_button.disabled and panel.result_label.text.contains("受理しました"), "with the clock running the dismissal waits for the next minute")
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("解雇しました") and panel.result_label.text.contains("持っていた作業は開き直しました"), "the queued dismissal completes and reopens her Task")
# The rescue the command interrupts (SPEC 6.2.1), on one floor: a collapsed medic (the
# patient) and a medic who carries her to the infirmary on foot.
var infirmary := _scenes.command(fixture, &"build_facility", {&"definition_id": &"infirmary", &"origin": Vector3i(8, 0, 1)})
_expect(infirmary.is_success(), "build the infirmary (%s %s)" % [infirmary.message, infirmary.details])
clock.advance_minutes(runtime.definition_registry.get_facility(&"infirmary").build_minutes)
for id: StringName in [&"patient", &"medic"]:
var options := {
&"current_cell": Vector3i(-2, 0, 1) if id == &"patient" else Vector3i(-10, 0, 1),
&"health": 0 if id == &"patient" else 100, &"state": StaffRuntime.State.AVAILABLE,
&"role_tags": [&"medic"], &"permitted_task_tags": [&"rescue"], &"daily_wage": 100,
}
_expect(_scenes.command(fixture, &"register_staff", {&"staff_id": id, &"role_id": &"medic", &"options": options}).is_success(), "register rescue employee %s" % id)
_expect(_scenes.advance_to_rescue_phase(fixture, "rescue_transport/MOVING/-/single"), "the medic carries the patient on foot (%s)" % _scenes.rescue_phase(runtime))
panel.refresh()
await _click(panel.roster_buttons[&"patient"])
var carrying := var_to_bytes(runtime.create_snapshot())
await _click(panel.dismiss_button)
_expect(panel.dismiss_confirm_label.text.contains("この従業員への救護は取り消し、救護係を外します。") and var_to_bytes(runtime.create_snapshot()) == carrying,
"the patient's confirmation says her rescue is cancelled: %s" % panel.dismiss_confirm_label.text)
await _click(panel.dismiss_cancel_button)
await _click(panel.roster_buttons[&"medic"])
await _click(panel.dismiss_button)
_expect(panel.dismiss_confirm_label.text.contains("運んでいる救護の搬送は取り消し、患者の救護をやり直します。") and not panel.dismiss_confirm_label.text.contains("開き直して"),
"the carrier's confirmation says the transport is cancelled, not reopened: %s" % panel.dismiss_confirm_label.text)
await _click(panel.dismiss_confirm_button)
_expect(not runtime.staff_service.has_staff(&"medic") and panel.result_label.text.contains("関わっていた救護の作業は取り消しました。")
and not panel.result_label.text.contains("開き直しました"), "the carrier's result names the cancelled rescue: %s" % panel.result_label.text)
await _click(panel.roster_buttons[&"patient"])
await _click(panel.dismiss_button)
await _click(panel.dismiss_confirm_button)
_expect(not runtime.staff_service.has_staff(&"patient"), "the patient is dismissed too, so the later rows are as before")
for row: Array in [[&"heroine_anri", "ユニーク:雇用・維持費無料 / 解雇・売却不可", "R1"], [&"adult_contract", "ギルドで雇った一般従業員だけを解雇できます。", "R7"]]:
_expect_refused_row(panel, runtime, row[0], row[1], row[2])
await _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])
@@ -935,7 +1026,7 @@ func _test_dismiss_from_main() -> void:
&"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")
await _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]})
@@ -961,7 +1052,7 @@ func _test_dismiss_from_main() -> void:
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")
await _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()
@@ -974,34 +1065,52 @@ func _test_dismiss_from_main() -> void:
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")
await _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()
# The hidden fund covers the part of the severance the front fund lacks (A14).
# Disclosed balance fixtures: the front fund holds G100 and a hidden-fund sale adds
# G200, so the funds return to the earlier balances after the dismissal.
for account: StringName in [Scenes.FRONT_SALES, Scenes.BACK_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(), "disclosed fixture: a payer for the balance fixtures")
_expect(_scenes.command(fixture, &"register_staff", {&"staff_id": &"ui_top_up", &"role_id": &"cleaner", &"options": {&"daily_wage": 100}}).is_success(), "an employee whose severance needs the hidden fund")
var front_before: int = runtime.economy_service.get_balance(FRONT)
var back_before: int = runtime.economy_service.get_balance(BACK)
_expect(front_before > 0 and _scenes.drain_front(fixture, "ui.top_up") and _scenes.sale(fixture, FRONT, 100, "ui.top_up.front")
and _scenes.sale(fixture, BACK, 200, "ui.top_up.back"), "disclosed fixture: front G100, hidden G%d" % (back_before + 200))
panel.refresh()
await _click(panel.roster_buttons[&"ui_top_up"])
await _click(panel.dismiss_button)
_expect(panel.dismiss_confirm_label.text.contains("退職金 G300(日給の3日分)を表金から払います。不足 G200 は裏金から補います。"),
"the confirmation shows the hidden-fund top-up: %s" % panel.dismiss_confirm_label.text)
await _click(panel.dismiss_confirm_button)
_expect(not runtime.staff_service.has_staff(&"ui_top_up") and panel.result_label.text.contains("退職金 G300 を払いました(不足 G200 は裏金から補填)"),
"the result shows the top-up: %s" % panel.result_label.text)
_expect(runtime.economy_service.get_balance(FRONT) == 0 and runtime.economy_service.get_balance(BACK) == back_before
and _scenes.sale(fixture, FRONT, front_before, "ui.top_up.restore") and runtime.economy_service.get_balance(FRONT) == front_before,
"the funds are back at G%d and G%d" % [front_before, back_before])
# 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()
await _click(panel.roster_buttons[&"ui_late"])
await _click(panel.dismiss_button)
await _click(panel.dismiss_confirm_button, true)
_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")
await _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")
_expect(_scenes.sale(fixture, FRONT, 1000, "ui.sale"), "disclosed fixture: a sale refills the front fund")
await _show_staff_panel(main, panel)
panel.refresh()
(panel.roster_buttons[&"ui_late"] as Button).pressed.emit()
panel.dismiss_button.pressed.emit()
await _click(panel.roster_buttons[&"ui_late"])
await _click(panel.dismiss_button)
_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()
await _click(panel.dismiss_confirm_button)
_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")):
@@ -1012,20 +1121,79 @@ func _test_dismiss_from_main() -> void:
_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")
await _show_staff_panel(main, panel)
await _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")
## A refusal row: a click selects the row, the button is disabled with the reason, and a
## click on the disabled button sends nothing (5.a, whole save bytes).
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()
await _click(panel.roster_buttons[staff_id])
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),
_expect(panel.selected_staff_id == staff_id and 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()
await _click(panel.dismiss_button)
_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)
"%s: a click on the disabled button sends nothing (5.a, whole save bytes)" % label)
## Viewport input (the _click_principal_button form of tests/ui/test_business_management.gd):
## the control is scrolled into view, the mouse moves to its centre and presses and
## releases there, in the viewport's own coordinates (Main is scaled from 1440x810).
## A roster row rebuilt by the panel in this frame scrolls only from the next frame
## (ScrollContainer.ensure_control_visible), and the roster's own scroll sits inside
## the side panel's scroll, so each is asked again after a frame.
## `running`: the clock runs only for the release, so a press while time runs sends
## the command for the next minute and no frame advances a minute.
func _click(control: Control, running := false) -> void:
for pass_index: int in 2:
await process_frame
var ancestor: Node = control.get_parent()
while ancestor != null:
if ancestor is ScrollContainer: ancestor.ensure_control_visible(control)
ancestor = ancestor.get_parent()
await process_frame
var clock: Variant = root.get_node("GameClock")
var point := control.get_global_rect().get_center()
var motion := InputEventMouseMotion.new()
motion.position = point
motion.global_position = point
root.push_input(motion, true)
for pressed: bool in [true, false]:
var event := InputEventMouseButton.new()
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = pressed
event.position = point
event.global_position = point
event.button_mask = MOUSE_BUTTON_MASK_LEFT if pressed else 0
if running and not pressed:
clock.set_paused(false)
root.push_input(event, true)
if running and not pressed:
clock.set_paused(true)
await process_frame
## After a 00:00 close Main shows the ledger in the side panel (_on_snapshot_published)
## and hides the staff controls; the player returns with the 従業員 navigation button.
func _show_staff_panel(main: Control, panel: StaffManagementPanel) -> void:
_expect(not panel.is_visible_in_tree(), "after the close Main shows the ledger instead of the staff panel")
await _click((main.get("navigation_buttons") as Dictionary)[&"staff"] as Button)
_expect(panel.is_visible_in_tree(), "a click on 従業員 brings the hired-staff panel back")
## A key press and release through the viewport (Main's _unhandled_key_input).
func _press_key(keycode: Key) -> void:
for pressed: bool in [true, false]:
var event := InputEventKey.new()
event.keycode = keycode
event.physical_keycode = keycode
event.pressed = pressed
root.push_input(event, true)
await process_frame