352 lines
15 KiB
GDScript
352 lines
15 KiB
GDScript
extends SceneTree
|
|
## Headless-тест паузы, настроек и выхода в меню на экране Main.tscn.
|
|
##
|
|
## Проверяет:
|
|
## • _pause_game / _resume_game — controller.paused и видимость PauseModal;
|
|
## • _open_settings + settings_changed — громкости Audio и спокойный фон;
|
|
## • _confirm_to_menu / _to_menu — подтверждение и главное меню;
|
|
## • Esc в фазе часа — пауза и повторное снятие.
|
|
##
|
|
## Запуск:
|
|
## godot --headless --path . --script tests/test_main_pause.gd
|
|
|
|
const MAIN_SCENE := "res://scenes/Main.tscn"
|
|
|
|
var _checks := 0
|
|
var _fails := 0
|
|
var _main: Node = null
|
|
var _controller = null
|
|
var _game = null
|
|
var _audio: Node = null
|
|
## Оригинальный user://save.json, чтобы тест не испортил сохранение игрока.
|
|
var _had_save := false
|
|
var _backup_save := ""
|
|
var _save_backed_up := false
|
|
|
|
|
|
func _init() -> void:
|
|
await process_frame
|
|
var packed := load(MAIN_SCENE) as PackedScene
|
|
if packed == null:
|
|
_fail("не удалось загрузить Main.tscn")
|
|
_finish()
|
|
return
|
|
_main = packed.instantiate()
|
|
root.add_child(_main)
|
|
await process_frame
|
|
|
|
_game = root.get_node_or_null("Game")
|
|
_audio = root.get_node_or_null("Audio")
|
|
_ok(_game != null, "autoload Game доступен")
|
|
_ok(_audio != null, "autoload Audio доступен")
|
|
_ok(_main.controller != null, "controller создан")
|
|
if _game == null or _main.controller == null:
|
|
_finish()
|
|
return
|
|
_controller = _main.controller
|
|
_had_save = SaveManager.has_save()
|
|
if _had_save:
|
|
_backup_save = FileAccess.get_file_as_string(SaveManager.SAVE_PATH)
|
|
_save_backed_up = true
|
|
|
|
# До старта забега — проверяем, что настройки из меню не пишут save.
|
|
_test_menu_settings_no_write()
|
|
|
|
_main._start_new_run()
|
|
await process_frame
|
|
_eq(_controller.phase, &"prep", "после нового забега фаза prep")
|
|
_ok(_main._settings_persist_allowed(), "в фазе prep запись настроек в save разрешена")
|
|
SaveManager.delete_save()
|
|
_main._apply_settings({"sfx": 33, "music": 44, "calm_bg": true, "fullscreen": false})
|
|
_ok(SaveManager.has_save(), "настройки в фазе prep пишут save.json")
|
|
|
|
_main._on_start_hour()
|
|
await process_frame
|
|
_eq(_controller.phase, &"hour", "фаза контроллера — hour")
|
|
_ok(_main.pause_modal != null, "PauseModal создана")
|
|
_ok(_main.settings_modal != null, "SettingsModal создана")
|
|
_ok(_main.confirm_modal != null, "ConfirmModal создана")
|
|
if _main.pause_modal == null or _main.settings_modal == null or _main.confirm_modal == null:
|
|
_finish()
|
|
return
|
|
|
|
_test_pause_resume()
|
|
_test_modal_pause()
|
|
_test_settings()
|
|
_test_confirm_to_menu()
|
|
_test_escape()
|
|
_test_run_result_settings_no_write()
|
|
|
|
await process_frame
|
|
print("")
|
|
print("Проверок: %d, провалов: %d" % [_checks, _fails])
|
|
_finish()
|
|
|
|
|
|
## В главном меню «Подтвердить» не создаёт и не перезаписывает save.json.
|
|
func _test_menu_settings_no_write() -> void:
|
|
# --- холодный старт: save.json ещё нет ---------------------------------
|
|
SaveManager.delete_save()
|
|
_main.main_menu.set_has_save(false)
|
|
_eq(_controller.phase, &"menu", "до старта забега фаза menu")
|
|
_ok(not _main._settings_persist_allowed(),
|
|
"в фазе menu запись настроек в save запрещена")
|
|
_ok(not SaveManager.has_save(), "перед проверкой меню save.json отсутствует")
|
|
|
|
_main._open_settings()
|
|
_main.settings_modal._sfx.set_value(40)
|
|
_main.settings_modal._confirm()
|
|
_ok(not _main.settings_modal.visible, "настройки из меню закрылись по «Подтвердить»")
|
|
_eq(int((_game.stats.get("settings", {}) as Dictionary).get("sfx", -1)), 40,
|
|
"настройки из меню попали в Game.stats")
|
|
_ok(not SaveManager.has_save(), "настройки в меню не создают save.json")
|
|
_eq(_main.main_menu.has_save, false, "Continue отражает отсутствие save")
|
|
_ok(bool(_main.main_menu._btn_continue.disabled),
|
|
"Continue неактивна без сохранения")
|
|
|
|
# --- существующий save.json не перезаписывается -------------------------
|
|
_ok(SaveManager.save({"money": 321, "phase": "prep"}),
|
|
"подготовлен существующий save.json")
|
|
var before_text := FileAccess.get_file_as_string(SaveManager.SAVE_PATH)
|
|
var before_mtime := FileAccess.get_modified_time(SaveManager.SAVE_PATH)
|
|
_main.main_menu.set_has_save(true)
|
|
_ok(not bool(_main.main_menu._btn_continue.disabled),
|
|
"Continue активна при существующем save")
|
|
|
|
_main._open_settings()
|
|
_main.settings_modal._sfx.set_value(55)
|
|
_main.settings_modal._confirm()
|
|
_eq(FileAccess.get_file_as_string(SaveManager.SAVE_PATH), before_text,
|
|
"существующий save.json не изменён настройками из меню")
|
|
_eq(FileAccess.get_modified_time(SaveManager.SAVE_PATH), before_mtime,
|
|
"mtime существующего save.json не изменился")
|
|
_eq(int((_game.stats.get("settings", {}) as Dictionary).get("sfx", -1)), 55,
|
|
"настройки из меню применены и при существующем save")
|
|
_ok(SaveManager.has_save(), "save.json остаётся на месте")
|
|
|
|
|
|
## SFX из user://save.json (или -1, если файла/поля нет).
|
|
func _saved_settings_sfx() -> int:
|
|
if not SaveManager.has_save():
|
|
return -1
|
|
var parsed = JSON.parse_string(FileAccess.get_file_as_string(SaveManager.SAVE_PATH))
|
|
if not (parsed is Dictionary):
|
|
return -1
|
|
var stats = (parsed as Dictionary).get("stats", {})
|
|
if not (stats is Dictionary):
|
|
return -1
|
|
var settings = (stats as Dictionary).get("settings", {})
|
|
if not (settings is Dictionary):
|
|
return -1
|
|
return int((settings as Dictionary).get("sfx", -1))
|
|
|
|
|
|
func _test_pause_resume() -> void:
|
|
_main._pause_game()
|
|
_ok(bool(_controller.paused), "_pause_game ставит controller.paused=true")
|
|
_ok(_main.pause_modal.visible, "_pause_game показывает PauseModal")
|
|
|
|
_main._resume_game()
|
|
_ok(not bool(_controller.paused), "_resume_game снимает паузу")
|
|
_ok(not _main.pause_modal.visible, "_resume_game скрывает PauseModal")
|
|
|
|
|
|
## Служебное окно (настройки) в час ставит таймер на паузу и снимает её.
|
|
func _test_modal_pause() -> void:
|
|
var director = _controller.hour_director
|
|
_ok(director != null, "hour_director доступен для проверки паузы")
|
|
if director == null:
|
|
return
|
|
_controller.set_paused(false)
|
|
_main._on_menu_pick("Настройки")
|
|
_ok(_main.settings_modal.visible, "настройки открыты из меню")
|
|
_ok(bool(_controller.paused), "открытые настройки ставят час на паузу")
|
|
var before := float(director.time_left)
|
|
_controller.tick(0.5)
|
|
_approx(float(director.time_left), before, 0.001,
|
|
"на паузе tick не уменьшает time_left")
|
|
|
|
_main.settings_modal.close()
|
|
_ok(not bool(_controller.paused), "закрытие настроек снимает автопаузу")
|
|
_controller.tick(0.2)
|
|
_ok(float(director.time_left) < before, "после закрытия время снова идёт")
|
|
|
|
# Подтверждение (About использует ту же модалку) тоже держит паузу.
|
|
_main._show_about()
|
|
_ok(bool(_controller.paused), "окно «О программе» ставит паузу")
|
|
_main.confirm_modal._answer(false)
|
|
_ok(not bool(_controller.paused), "закрытие «О программе» снимает паузу")
|
|
|
|
# Переименование папки — тоже служебное окно часа.
|
|
var fid := ""
|
|
for section in _game.registry.sections.values():
|
|
if not section.folder_ids.is_empty():
|
|
fid = str(section.folder_ids[0])
|
|
break
|
|
if not fid.is_empty():
|
|
_main._open_rename(fid)
|
|
_ok(_main.rename_modal.visible, "окно переименования открыто")
|
|
_ok(bool(_controller.paused), "окно переименования ставит паузу")
|
|
_main.rename_modal.close()
|
|
_ok(not bool(_controller.paused), "закрытие переименования снимает паузу")
|
|
|
|
|
|
func _test_settings() -> void:
|
|
_main._open_settings()
|
|
_ok(_main.settings_modal.visible, "_open_settings показывает SettingsModal")
|
|
|
|
# До «Подтвердить» изменения остаются черновиком и не применяются.
|
|
var sfx_before := float(_audio.sfx_volume)
|
|
_main.settings_modal._sfx.set_value(20)
|
|
_main.settings_modal._music.set_value(30)
|
|
_main.settings_modal._on_calm(true)
|
|
_approx(float(_audio.sfx_volume), sfx_before, 0.001,
|
|
"ползунок не применяет громкость до подтверждения")
|
|
SaveManager.delete_save()
|
|
_main.settings_modal._confirm()
|
|
_ok(not _main.settings_modal.visible, "«Подтвердить» закрывает настройки")
|
|
_approx(float(_audio.sfx_volume), 0.2, 0.001, "громкость SFX применена")
|
|
_approx(float(_audio.music_volume), 0.3, 0.001, "громкость музыки применена")
|
|
_ok(_main._settings_persist_allowed(), "в фазе hour запись настроек в save разрешена")
|
|
_ok(SaveManager.has_save(), "настройки в фазе hour пишут save.json")
|
|
_eq(_saved_settings_sfx(), 20, "save.json содержит применённые настройки")
|
|
_ok(_main._backdrop != null and bool(_main._backdrop.visible),
|
|
"спокойный фон не скрывает Backdrop")
|
|
_ok(_main._backdrop != null and bool(_main._backdrop.calm),
|
|
"спокойный фон переключает Backdrop.calm=true")
|
|
var clear = ProjectSettings.get_setting("rendering/environment/defaults/default_clear_color")
|
|
_ok(clear is Color and (clear as Color).is_equal_approx(Theme90.PAPER),
|
|
"clear color проекта — бумага, а не серый")
|
|
var saved = _game.stats.get("settings", {})
|
|
_ok(saved is Dictionary and int(saved.get("sfx", -1)) == 20,
|
|
"настройки сохранены в Game.stats")
|
|
|
|
# Отмена/X не применяют черновик и не пишут save.
|
|
_main._open_settings()
|
|
_main.settings_modal._sfx.set_value(90)
|
|
_main.settings_modal.close()
|
|
_approx(float(_audio.sfx_volume), 0.2, 0.001, "отмена не применяет громкость")
|
|
var saved_after_cancel = _game.stats.get("settings", {})
|
|
_ok(saved_after_cancel is Dictionary and int(saved_after_cancel.get("sfx", -1)) == 20,
|
|
"отмена не пишет настройки в save")
|
|
_eq(_saved_settings_sfx(), 20, "отмена не переписывает save.json")
|
|
|
|
# «Сброс» возвращает черновик к DEFAULT_STATE, но не применяет.
|
|
_main._open_settings()
|
|
_main.settings_modal._on_reset()
|
|
var draft: Dictionary = _main.settings_modal.get_state()
|
|
_eq(int(draft.get("sfx", -1)), 70, "«Сброс» вернул SFX к 70")
|
|
_approx(float(_audio.sfx_volume), 0.2, 0.001, "«Сброс» не применяет настройки")
|
|
_main.settings_modal.close()
|
|
|
|
# Выключение спокойного фона возвращает dither, Backdrop остаётся видимым.
|
|
_main.settings_modal.open({
|
|
"sfx": 20, "music": 30, "calm_bg": false, "fullscreen": false,
|
|
})
|
|
_main.settings_modal._confirm()
|
|
_ok(_main._backdrop != null and bool(_main._backdrop.visible),
|
|
"обычный фон не скрывает Backdrop")
|
|
_ok(_main._backdrop != null and not bool(_main._backdrop.calm),
|
|
"обычный фон сбрасывает Backdrop.calm=false")
|
|
|
|
|
|
func _test_confirm_to_menu() -> void:
|
|
_main._confirm_to_menu()
|
|
_ok(_main.confirm_modal.visible, "_confirm_to_menu показывает подтверждение")
|
|
_to_menu()
|
|
_ok(_main.main_menu.visible, "_to_menu показывает главное меню")
|
|
_ok(not _main.pause_modal.visible, "_to_menu скрывает PauseModal")
|
|
_ok(not _main.settings_modal.visible, "_to_menu скрывает SettingsModal")
|
|
_ok(not _main.confirm_modal.visible, "_to_menu скрывает ConfirmModal")
|
|
_ok(bool(_controller.paused), "_to_menu держит час на паузе")
|
|
|
|
|
|
func _test_escape() -> void:
|
|
_main._unhandled_input(_esc())
|
|
_ok(_main.pause_modal.visible, "Esc в фазе hour открывает паузу")
|
|
_main._unhandled_input(_esc())
|
|
_ok(not _main.pause_modal.visible, "повторный Esc снимает паузу")
|
|
_ok(not bool(_controller.paused), "после второго Esc пауза снята")
|
|
|
|
|
|
## На экране итогов настройки применяются, но results-save не перезаписывается.
|
|
func _test_run_result_settings_no_write() -> void:
|
|
_controller._finish_run("test")
|
|
_eq(_controller.phase, &"run_result", "контроллер перешёл в run_result")
|
|
_ok(not _main._settings_persist_allowed(),
|
|
"в фазе run_result запись настроек в save запрещена")
|
|
_ok(SaveManager.has_save(), "_finish_run записал results-save")
|
|
_ok(_main.run_result_modal.visible, "экран итогов показан")
|
|
var before_text := FileAccess.get_file_as_string(SaveManager.SAVE_PATH)
|
|
var before_mtime := FileAccess.get_modified_time(SaveManager.SAVE_PATH)
|
|
|
|
_main._open_settings()
|
|
_main.settings_modal._sfx.set_value(15)
|
|
_main.settings_modal._confirm()
|
|
_eq(FileAccess.get_file_as_string(SaveManager.SAVE_PATH), before_text,
|
|
"results-save не перезаписан настройками с экрана итогов")
|
|
_eq(FileAccess.get_modified_time(SaveManager.SAVE_PATH), before_mtime,
|
|
"mtime results-save не изменился")
|
|
_eq(int((_game.stats.get("settings", {}) as Dictionary).get("sfx", -1)), 15,
|
|
"настройки с экрана итогов применены в памяти")
|
|
|
|
|
|
func _to_menu() -> void:
|
|
_main._to_menu()
|
|
|
|
|
|
func _esc() -> InputEventKey:
|
|
var ev := InputEventKey.new()
|
|
ev.keycode = KEY_ESCAPE
|
|
ev.physical_keycode = KEY_ESCAPE
|
|
ev.pressed = true
|
|
return ev
|
|
|
|
|
|
func _finish() -> void:
|
|
# Возвращаем пользовательский save.json в исходное состояние.
|
|
if _save_backed_up:
|
|
if _had_save:
|
|
var file := FileAccess.open(SaveManager.SAVE_PATH, FileAccess.WRITE)
|
|
if file != null:
|
|
file.store_string(_backup_save)
|
|
file.flush()
|
|
file.close()
|
|
else:
|
|
SaveManager.delete_save()
|
|
if _fails == 0:
|
|
print("PASS")
|
|
else:
|
|
print("FAIL")
|
|
quit(0 if _fails == 0 else 1)
|
|
|
|
|
|
func _ok(condition: bool, message: String) -> void:
|
|
_checks += 1
|
|
if condition:
|
|
return
|
|
_fails += 1
|
|
print("FAIL: %s" % message)
|
|
|
|
|
|
func _eq(actual, expected, message: String) -> void:
|
|
_checks += 1
|
|
if actual == expected:
|
|
return
|
|
_fails += 1
|
|
print("FAIL: %s — получено %s, ожидалось %s" % [message, actual, expected])
|
|
|
|
|
|
func _approx(actual: float, expected: float, eps: float, message: String) -> void:
|
|
_checks += 1
|
|
if absf(float(actual) - float(expected)) <= eps:
|
|
return
|
|
_fails += 1
|
|
print("FAIL: %s — получено %.4f, ожидалось %.4f" % [message, actual, expected])
|
|
|
|
|
|
func _fail(message: String) -> void:
|
|
_checks += 1
|
|
_fails += 1
|
|
print("FAIL: %s" % message)
|