Connect bear-save, massage, farm paywall, cheese catapults, Dressler hijack, final boss phases, fat-stim UI, and scene fixes so the satire path plays through.
264 lines
7 KiB
GDScript
264 lines
7 KiB
GDScript
extends Node
|
|
## Global game flow matching DESIGN.md.
|
|
|
|
signal hp_changed(hp: int, max_hp: int)
|
|
signal died(cause_key: String)
|
|
signal scene_changed(scene_id: String)
|
|
signal status_changed
|
|
|
|
const SAVE_PATH := "user://save.json"
|
|
|
|
const SCENE_PATHS := {
|
|
"title_screen": "res://scenes/title_screen.tscn",
|
|
"intro_cutscene": "res://scenes/intro_cutscene.tscn",
|
|
"village": "res://scenes/village.tscn",
|
|
"shoe_boss": "res://scenes/shoe_boss.tscn",
|
|
"meadow": "res://scenes/meadow.tscn",
|
|
"massage_studio": "res://scenes/massage_studio.tscn",
|
|
"catscene": "res://scenes/catscene.tscn",
|
|
"farm_path": "res://scenes/farm_path.tscn",
|
|
"farm": "res://scenes/farm.tscn",
|
|
"dressler_boss": "res://scenes/dressler_boss.tscn",
|
|
"moon": "res://scenes/moon.tscn",
|
|
"final_boss": "res://scenes/final_boss.tscn",
|
|
"end_screen": "res://scenes/end_screen.tscn",
|
|
"death_screen": "res://scenes/death_screen.tscn",
|
|
}
|
|
|
|
## Default linear beats (branches override via goto_scene / flags).
|
|
const SCENE_ORDER: Array[String] = [
|
|
"title_screen",
|
|
"intro_cutscene",
|
|
"village",
|
|
"shoe_boss",
|
|
"meadow",
|
|
"massage_studio",
|
|
"catscene",
|
|
"farm_path",
|
|
"farm",
|
|
"dressler_boss",
|
|
"moon",
|
|
"final_boss",
|
|
"end_screen",
|
|
]
|
|
|
|
var current_scene_id: String = "title_screen"
|
|
var max_hp: int = 4
|
|
var hp: int = 4
|
|
var death_count: int = 0
|
|
var last_death_cause_key: String = "death_default"
|
|
var playtime_seconds: float = 0.0
|
|
var house_built: bool = false
|
|
var flags: Dictionary = {}
|
|
|
|
## Status: fat (tiramisu), glitched (3 glitzo hits), urhahn shock
|
|
var fat_time_left: float = 0.0
|
|
var glitch_hits: int = 0
|
|
var shock_time_left: float = 0.0
|
|
|
|
func _ready() -> void:
|
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
|
load_game()
|
|
|
|
func _process(delta: float) -> void:
|
|
if get_tree().paused:
|
|
return
|
|
if current_scene_id in ["title_screen", "death_screen"]:
|
|
return
|
|
playtime_seconds += delta
|
|
if playtime_seconds >= 3600.0:
|
|
Achievements.unlock("played_hour")
|
|
var dirty := false
|
|
if fat_time_left > 0.0:
|
|
fat_time_left = max(0.0, fat_time_left - delta)
|
|
dirty = true
|
|
if shock_time_left > 0.0:
|
|
shock_time_left = max(0.0, shock_time_left - delta)
|
|
dirty = true
|
|
if dirty:
|
|
status_changed.emit()
|
|
|
|
func is_fat() -> bool:
|
|
return fat_time_left > 0.0
|
|
|
|
func is_shocked() -> bool:
|
|
return shock_time_left > 0.0
|
|
|
|
func is_glitched_slow() -> bool:
|
|
return glitch_hits >= 3
|
|
|
|
func apply_fat(seconds: float = 600.0) -> void:
|
|
fat_time_left = max(fat_time_left, seconds)
|
|
Achievements.unlock("tiramisu_tonne")
|
|
status_changed.emit()
|
|
|
|
func apply_shock(seconds: float = 10.0) -> void:
|
|
shock_time_left = max(shock_time_left, seconds)
|
|
status_changed.emit()
|
|
|
|
func register_glitzo_hit() -> void:
|
|
glitch_hits += 1
|
|
status_changed.emit()
|
|
take_damage(1, "death_glitzo")
|
|
|
|
func clear_glitch() -> void:
|
|
glitch_hits = 0
|
|
status_changed.emit()
|
|
|
|
func goto_scene(scene_id: String) -> void:
|
|
if not SCENE_PATHS.has(scene_id):
|
|
push_error("Unknown scene: %s" % scene_id)
|
|
return
|
|
current_scene_id = scene_id
|
|
scene_changed.emit(scene_id)
|
|
if scene_id == "moon":
|
|
Achievements.unlock("moon_cheese")
|
|
save_game()
|
|
get_tree().change_scene_to_file(SCENE_PATHS[scene_id])
|
|
|
|
func goto_next() -> void:
|
|
match current_scene_id:
|
|
"village":
|
|
goto_scene("shoe_boss")
|
|
"shoe_boss":
|
|
goto_scene("meadow")
|
|
"meadow":
|
|
goto_scene("massage_studio")
|
|
"massage_studio":
|
|
goto_scene("catscene")
|
|
"catscene":
|
|
goto_scene("farm_path")
|
|
"farm_path":
|
|
if get_flag("planet_destroyed", false):
|
|
goto_scene("moon")
|
|
else:
|
|
goto_scene("farm")
|
|
"farm":
|
|
goto_scene("dressler_boss")
|
|
"dressler_boss":
|
|
goto_scene("moon")
|
|
"moon":
|
|
goto_scene("final_boss")
|
|
"final_boss":
|
|
goto_scene("end_screen")
|
|
_:
|
|
var idx := SCENE_ORDER.find(current_scene_id)
|
|
if idx < 0 or idx >= SCENE_ORDER.size() - 1:
|
|
goto_scene("end_screen")
|
|
else:
|
|
goto_scene(SCENE_ORDER[idx + 1])
|
|
|
|
func skip_to_final_boss() -> void:
|
|
Achievements.unlock("milk_skip")
|
|
flags["milk_skip_used"] = true
|
|
goto_scene("final_boss")
|
|
|
|
func reset_to_intro() -> void:
|
|
## Catscene skip punishment.
|
|
hp = max_hp
|
|
glitch_hits = 0
|
|
fat_time_left = 0.0
|
|
shock_time_left = 0.0
|
|
flags.clear()
|
|
Inventory.deserialize({"items": {}})
|
|
goto_scene("intro_cutscene")
|
|
|
|
func start_new_game() -> void:
|
|
hp = max_hp
|
|
glitch_hits = 0
|
|
fat_time_left = 0.0
|
|
shock_time_left = 0.0
|
|
flags.clear()
|
|
house_built = false
|
|
goto_scene("intro_cutscene")
|
|
|
|
func heal_full() -> void:
|
|
hp = max_hp
|
|
glitch_hits = 0
|
|
hp_changed.emit(hp, max_hp)
|
|
|
|
func take_damage(amount: int = 1, cause_key: String = "death_default") -> void:
|
|
hp = max(0, hp - amount)
|
|
hp_changed.emit(hp, max_hp)
|
|
if hp <= 0:
|
|
die(cause_key)
|
|
|
|
func die(cause_key: String = "death_default") -> void:
|
|
last_death_cause_key = cause_key
|
|
death_count += 1
|
|
died.emit(cause_key)
|
|
if death_count >= 100:
|
|
Achievements.unlock("died_100")
|
|
save_game()
|
|
goto_scene("death_screen")
|
|
|
|
func revive_continue() -> void:
|
|
heal_full()
|
|
## Return to last non-death gameplay scene, default village.
|
|
var back := str(flags.get("last_gameplay_scene", "village"))
|
|
if back == "death_screen" or back == "title_screen":
|
|
back = "village"
|
|
goto_scene(back)
|
|
|
|
func quit_to_title() -> void:
|
|
heal_full()
|
|
goto_scene("title_screen")
|
|
|
|
func set_flag(key: String, value: Variant = true) -> void:
|
|
flags[key] = value
|
|
save_game()
|
|
|
|
func get_flag(key: String, default_value: Variant = false) -> Variant:
|
|
return flags.get(key, default_value)
|
|
|
|
func remember_gameplay_scene() -> void:
|
|
if current_scene_id not in ["title_screen", "death_screen", "intro_cutscene", "catscene"]:
|
|
flags["last_gameplay_scene"] = current_scene_id
|
|
|
|
func save_game() -> void:
|
|
remember_gameplay_scene()
|
|
var data := {
|
|
"hp": hp,
|
|
"max_hp": max_hp,
|
|
"death_count": death_count,
|
|
"last_death_cause_key": last_death_cause_key,
|
|
"playtime_seconds": playtime_seconds,
|
|
"current_scene_id": current_scene_id,
|
|
"house_built": house_built,
|
|
"flags": flags,
|
|
"fat_time_left": fat_time_left,
|
|
"glitch_hits": glitch_hits,
|
|
"inventory": Inventory.serialize(),
|
|
"achievements": Achievements.serialize(),
|
|
"shop": Shop.serialize(),
|
|
"settings": Settings.serialize(),
|
|
}
|
|
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
|
if file == null:
|
|
return
|
|
file.store_string(JSON.stringify(data, "\t"))
|
|
|
|
func load_game() -> void:
|
|
if not FileAccess.file_exists(SAVE_PATH):
|
|
return
|
|
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
|
|
if file == null:
|
|
return
|
|
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
|
if typeof(parsed) != TYPE_DICTIONARY:
|
|
return
|
|
var data: Dictionary = parsed
|
|
hp = int(data.get("hp", max_hp))
|
|
max_hp = int(data.get("max_hp", max_hp))
|
|
death_count = int(data.get("death_count", 0))
|
|
last_death_cause_key = str(data.get("last_death_cause_key", "death_default"))
|
|
playtime_seconds = float(data.get("playtime_seconds", 0.0))
|
|
current_scene_id = str(data.get("current_scene_id", "title_screen"))
|
|
house_built = bool(data.get("house_built", false))
|
|
flags = data.get("flags", {})
|
|
fat_time_left = float(data.get("fat_time_left", 0.0))
|
|
glitch_hits = int(data.get("glitch_hits", 0))
|
|
Inventory.deserialize(data.get("inventory", {}))
|
|
Achievements.deserialize(data.get("achievements", {}))
|
|
Shop.deserialize(data.get("shop", {}))
|
|
Settings.deserialize(data.get("settings", {}))
|