Initial scaffold: Godot 4.3 scenes, systems, and placeholders.
Wire all twelve levels with billboard prefabs, autoload systems (inventory, achievements, mock shop, DE/EN/BY), and satire audio locks so the Title→End flow is playable with placeholder assets.
This commit is contained in:
commit
938cc4bc82
114 changed files with 4819 additions and 0 deletions
57
scripts/autoload/achievements.gd
Normal file
57
scripts/autoload/achievements.gd
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
extends Node
|
||||
## Achievement unlock + persistence.
|
||||
|
||||
signal unlocked(achievement_id: String, title: String)
|
||||
|
||||
var _defs: Array = []
|
||||
var _unlocked: Dictionary = {} # id -> true
|
||||
|
||||
func _ready() -> void:
|
||||
_load_defs()
|
||||
|
||||
func _load_defs() -> void:
|
||||
var file := FileAccess.open("res://data/achievements.json", FileAccess.READ)
|
||||
if file == null:
|
||||
push_error("Missing achievements.json")
|
||||
return
|
||||
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||
if typeof(parsed) == TYPE_ARRAY:
|
||||
_defs = parsed
|
||||
|
||||
func unlock(achievement_id: String) -> void:
|
||||
if _unlocked.get(achievement_id, false):
|
||||
return
|
||||
_unlocked[achievement_id] = true
|
||||
var title := achievement_id
|
||||
for entry in _defs:
|
||||
if str(entry.get("id", "")) == achievement_id:
|
||||
title = Locale.tr_key(str(entry.get("title_key", achievement_id)))
|
||||
break
|
||||
unlocked.emit(achievement_id, title)
|
||||
_check_all()
|
||||
GameState.save_game()
|
||||
|
||||
func is_unlocked(achievement_id: String) -> bool:
|
||||
return bool(_unlocked.get(achievement_id, false))
|
||||
|
||||
func get_defs() -> Array:
|
||||
return _defs
|
||||
|
||||
func _check_all() -> void:
|
||||
if _unlocked.get("all_burgers", false):
|
||||
return
|
||||
for entry in _defs:
|
||||
var id := str(entry.get("id", ""))
|
||||
if id == "all_burgers":
|
||||
continue
|
||||
if not _unlocked.get(id, false):
|
||||
return
|
||||
_unlocked["all_burgers"] = true
|
||||
var title := Locale.tr_key("ach_all_title")
|
||||
unlocked.emit("all_burgers", title)
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return {"unlocked": _unlocked.duplicate()}
|
||||
|
||||
func deserialize(data: Dictionary) -> void:
|
||||
_unlocked = data.get("unlocked", {}).duplicate()
|
||||
91
scripts/autoload/audio_director.gd
Normal file
91
scripts/autoload/audio_director.gd
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
extends Node
|
||||
## Music / SFX / Voice + Burger-Cham every 120s on title/loading.
|
||||
|
||||
signal burger_cham_triggered
|
||||
|
||||
const BURGER_INTERVAL := 120.0
|
||||
|
||||
# TODO: Replace placeholder streams with real takes (see README asset list).
|
||||
const MUSIC_TITLE := "res://assets/audio/placeholders/music_title.wav"
|
||||
const SFX_EXPLOSION := "res://assets/audio/placeholders/sfx_explosion.wav"
|
||||
const SFX_HIT := "res://assets/audio/placeholders/sfx_hit.wav"
|
||||
const SFX_BURGER := "res://assets/audio/placeholders/sfx_burger.wav"
|
||||
const VOICE_DE := "res://assets/audio/placeholders/voice_de.wav"
|
||||
const VOICE_EN := "res://assets/audio/placeholders/voice_en.wav"
|
||||
const VOICE_BY := "res://assets/audio/placeholders/voice_by.wav"
|
||||
|
||||
var _music: AudioStreamPlayer
|
||||
var _sfx: AudioStreamPlayer
|
||||
var _voice: AudioStreamPlayer
|
||||
var _burger_timer: float = 0.0
|
||||
var burger_cham_enabled: bool = true
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
_music = AudioStreamPlayer.new()
|
||||
_music.bus = "Music"
|
||||
_music.volume_db = 6.0 # intentionally loud
|
||||
add_child(_music)
|
||||
_sfx = AudioStreamPlayer.new()
|
||||
_sfx.bus = "SFX"
|
||||
add_child(_sfx)
|
||||
_voice = AudioStreamPlayer.new()
|
||||
_voice.bus = "Voice"
|
||||
add_child(_voice)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not burger_cham_enabled:
|
||||
return
|
||||
if GameState.current_scene_id != "title_screen":
|
||||
return
|
||||
_burger_timer += delta
|
||||
if _burger_timer >= BURGER_INTERVAL:
|
||||
_burger_timer = 0.0
|
||||
trigger_burger_cham()
|
||||
|
||||
func play_title_music() -> void:
|
||||
var stream := _load_stream(MUSIC_TITLE)
|
||||
if stream == null:
|
||||
return
|
||||
_music.stream = stream
|
||||
_music.volume_db = 6.0
|
||||
if not _music.playing:
|
||||
_music.play()
|
||||
|
||||
func stop_music() -> void:
|
||||
_music.stop()
|
||||
|
||||
func play_sfx(path: String) -> void:
|
||||
var stream := _load_stream(path)
|
||||
if stream == null:
|
||||
return
|
||||
_sfx.stream = stream
|
||||
_sfx.play()
|
||||
|
||||
func play_explosion() -> void:
|
||||
play_sfx(SFX_EXPLOSION)
|
||||
|
||||
func play_hit() -> void:
|
||||
play_sfx(SFX_HIT)
|
||||
|
||||
func play_language_voice(lang: String) -> void:
|
||||
var path := VOICE_DE
|
||||
match lang:
|
||||
"en":
|
||||
path = VOICE_EN
|
||||
"by":
|
||||
path = VOICE_BY
|
||||
var stream := _load_stream(path)
|
||||
if stream == null:
|
||||
return
|
||||
_voice.stream = stream
|
||||
_voice.play()
|
||||
|
||||
func trigger_burger_cham() -> void:
|
||||
play_sfx(SFX_BURGER)
|
||||
burger_cham_triggered.emit()
|
||||
|
||||
func _load_stream(path: String) -> AudioStream:
|
||||
if ResourceLoader.exists(path):
|
||||
return load(path)
|
||||
return null
|
||||
161
scripts/autoload/game_state.gd
Normal file
161
scripts/autoload/game_state.gd
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
extends Node
|
||||
## Global game flow, HP, death, playtime, save/load.
|
||||
## TODO: Expand save schema when real progression exists.
|
||||
|
||||
signal hp_changed(hp: int, max_hp: int)
|
||||
signal died(cause_key: String)
|
||||
signal scene_changed(scene_id: String)
|
||||
|
||||
const SAVE_PATH := "user://save.json"
|
||||
const SCENE_ORDER: Array[String] = [
|
||||
"title_screen",
|
||||
"intro_cutscene",
|
||||
"village",
|
||||
"shoe_boss",
|
||||
"meadow",
|
||||
"massage_studio",
|
||||
"farm_path",
|
||||
"farm",
|
||||
"moon",
|
||||
"final_boss",
|
||||
"end_screen",
|
||||
]
|
||||
|
||||
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",
|
||||
"farm_path": "res://scenes/farm_path.tscn",
|
||||
"farm": "res://scenes/farm.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",
|
||||
}
|
||||
|
||||
var current_scene_id: String = "title_screen"
|
||||
var max_hp: int = 5
|
||||
var hp: int = 5
|
||||
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 = {}
|
||||
|
||||
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")
|
||||
|
||||
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:
|
||||
var idx := SCENE_ORDER.find(current_scene_id)
|
||||
if idx < 0:
|
||||
goto_scene("village")
|
||||
return
|
||||
if idx >= SCENE_ORDER.size() - 1:
|
||||
goto_scene("end_screen")
|
||||
return
|
||||
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 start_new_game() -> void:
|
||||
hp = max_hp
|
||||
flags.clear()
|
||||
house_built = false
|
||||
goto_scene("intro_cutscene")
|
||||
|
||||
func heal_full() -> void:
|
||||
hp = max_hp
|
||||
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 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 save_game() -> void:
|
||||
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,
|
||||
"inventory": Inventory.serialize(),
|
||||
"achievements": Achievements.serialize(),
|
||||
"shop": Shop.serialize(),
|
||||
"settings": Settings.serialize(),
|
||||
}
|
||||
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
||||
if file == null:
|
||||
push_warning("Could not write save file")
|
||||
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", {})
|
||||
Inventory.deserialize(data.get("inventory", {}))
|
||||
Achievements.deserialize(data.get("achievements", {}))
|
||||
Shop.deserialize(data.get("shop", {}))
|
||||
Settings.deserialize(data.get("settings", {}))
|
||||
84
scripts/autoload/inventory.gd
Normal file
84
scripts/autoload/inventory.gd
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
extends Node
|
||||
## Array-based inventory with item IDs from data/items.json.
|
||||
|
||||
signal inventory_changed
|
||||
signal item_used(item_id: String)
|
||||
|
||||
var _items: Dictionary = {} # id -> count
|
||||
var _defs: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
_load_defs()
|
||||
|
||||
func _load_defs() -> void:
|
||||
var file := FileAccess.open("res://data/items.json", FileAccess.READ)
|
||||
if file == null:
|
||||
push_error("Missing items.json")
|
||||
return
|
||||
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||
if typeof(parsed) != TYPE_ARRAY:
|
||||
return
|
||||
for entry in parsed:
|
||||
if typeof(entry) == TYPE_DICTIONARY and entry.has("id"):
|
||||
_defs[str(entry["id"])] = entry
|
||||
|
||||
func add_item(item_id: String, amount: int = 1) -> void:
|
||||
if amount <= 0:
|
||||
return
|
||||
_items[item_id] = int(_items.get(item_id, 0)) + amount
|
||||
inventory_changed.emit()
|
||||
GameState.save_game()
|
||||
|
||||
func remove_item(item_id: String, amount: int = 1) -> bool:
|
||||
var have := int(_items.get(item_id, 0))
|
||||
if have < amount:
|
||||
return false
|
||||
have -= amount
|
||||
if have <= 0:
|
||||
_items.erase(item_id)
|
||||
else:
|
||||
_items[item_id] = have
|
||||
inventory_changed.emit()
|
||||
GameState.save_game()
|
||||
return true
|
||||
|
||||
func has_item(item_id: String, amount: int = 1) -> bool:
|
||||
return int(_items.get(item_id, 0)) >= amount
|
||||
|
||||
func get_count(item_id: String) -> int:
|
||||
return int(_items.get(item_id, 0))
|
||||
|
||||
func get_all() -> Dictionary:
|
||||
return _items.duplicate()
|
||||
|
||||
func use_item(item_id: String) -> bool:
|
||||
if not has_item(item_id):
|
||||
return false
|
||||
match item_id:
|
||||
"milk":
|
||||
remove_item("milk", 1)
|
||||
item_used.emit(item_id)
|
||||
GameState.skip_to_final_boss()
|
||||
return true
|
||||
"death_ticket":
|
||||
remove_item("death_ticket", 1)
|
||||
Achievements.unlock("eaten_to_death")
|
||||
item_used.emit(item_id)
|
||||
return true
|
||||
"tiramisu":
|
||||
remove_item("tiramisu", 1)
|
||||
var eaten := int(GameState.get_flag("tiramisu_kg", 0)) + 1
|
||||
GameState.set_flag("tiramisu_kg", eaten)
|
||||
if eaten >= 1000:
|
||||
Achievements.unlock("tiramisu_tonne")
|
||||
item_used.emit(item_id)
|
||||
return true
|
||||
_:
|
||||
return false
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return {"items": _items.duplicate()}
|
||||
|
||||
func deserialize(data: Dictionary) -> void:
|
||||
_items = data.get("items", {}).duplicate()
|
||||
inventory_changed.emit()
|
||||
35
scripts/autoload/locale.gd
Normal file
35
scripts/autoload/locale.gd
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
extends Node
|
||||
## Multilingual strings DE/EN/BY with DE fallback.
|
||||
|
||||
signal locale_changed
|
||||
|
||||
var _strings: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
reload()
|
||||
|
||||
func reload() -> void:
|
||||
_strings = _load_lang("de")
|
||||
var lang := Settings.language if Settings else "de"
|
||||
if lang != "de":
|
||||
var overlay := _load_lang(lang)
|
||||
for key in overlay.keys():
|
||||
_strings[key] = overlay[key]
|
||||
locale_changed.emit()
|
||||
|
||||
func _load_lang(lang: String) -> Dictionary:
|
||||
var path := "res://data/locales/%s.json" % lang
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
push_warning("Missing locale: %s" % path)
|
||||
return {}
|
||||
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||
if typeof(parsed) == TYPE_DICTIONARY:
|
||||
return parsed
|
||||
return {}
|
||||
|
||||
func tr_key(key: String, args: Array = []) -> String:
|
||||
var text := str(_strings.get(key, key))
|
||||
if args.is_empty():
|
||||
return text
|
||||
return text % args
|
||||
79
scripts/autoload/settings.gd
Normal file
79
scripts/autoload/settings.gd
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
extends Node
|
||||
## Persistent settings. Music cannot be turned down (satire).
|
||||
|
||||
signal settings_changed
|
||||
|
||||
const MUSIC_BUS := "Music"
|
||||
const SFX_BUS := "SFX"
|
||||
const VOICE_BUS := "Voice"
|
||||
|
||||
var language: String = "de"
|
||||
var sfx_volume: float = 1.0
|
||||
## Intentionally locked loud — slider is UI-disabled.
|
||||
var music_volume: float = 1.0
|
||||
|
||||
func _ready() -> void:
|
||||
_ensure_buses()
|
||||
apply_audio()
|
||||
# Locale reloads after both Settings + Locale exist (Locale._ready / deserialize).
|
||||
|
||||
func _ensure_buses() -> void:
|
||||
_ensure_bus(MUSIC_BUS)
|
||||
_ensure_bus(SFX_BUS)
|
||||
_ensure_bus(VOICE_BUS)
|
||||
|
||||
func _ensure_bus(bus_name: String) -> void:
|
||||
if AudioServer.get_bus_index(bus_name) != -1:
|
||||
return
|
||||
AudioServer.add_bus()
|
||||
var idx := AudioServer.bus_count - 1
|
||||
AudioServer.set_bus_name(idx, bus_name)
|
||||
AudioServer.set_bus_send(idx, "Master")
|
||||
|
||||
func set_language(lang: String) -> void:
|
||||
if lang not in ["de", "en", "by"]:
|
||||
return
|
||||
language = lang
|
||||
Locale.reload()
|
||||
AudioDirector.play_language_voice(lang)
|
||||
settings_changed.emit()
|
||||
GameState.save_game()
|
||||
|
||||
func set_sfx_volume(value: float) -> void:
|
||||
sfx_volume = clampf(value, 0.0, 1.0)
|
||||
apply_audio()
|
||||
settings_changed.emit()
|
||||
GameState.save_game()
|
||||
|
||||
func set_music_volume(_value: float) -> void:
|
||||
## Satire lock: music stays maxed.
|
||||
music_volume = 1.0
|
||||
apply_audio()
|
||||
settings_changed.emit()
|
||||
|
||||
func apply_audio() -> void:
|
||||
_set_bus_linear(MUSIC_BUS, music_volume)
|
||||
_set_bus_linear(SFX_BUS, sfx_volume)
|
||||
_set_bus_linear(VOICE_BUS, sfx_volume)
|
||||
|
||||
func _set_bus_linear(bus_name: String, linear: float) -> void:
|
||||
var idx := AudioServer.get_bus_index(bus_name)
|
||||
if idx == -1:
|
||||
return
|
||||
var db := -80.0 if linear <= 0.0001 else 20.0 * log(linear) / log(10.0)
|
||||
AudioServer.set_bus_volume_db(idx, db)
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return {
|
||||
"language": language,
|
||||
"sfx_volume": sfx_volume,
|
||||
"music_volume": 1.0,
|
||||
}
|
||||
|
||||
func deserialize(data: Dictionary) -> void:
|
||||
language = str(data.get("language", "de"))
|
||||
sfx_volume = float(data.get("sfx_volume", 1.0))
|
||||
music_volume = 1.0
|
||||
Locale.reload()
|
||||
apply_audio()
|
||||
settings_changed.emit()
|
||||
50
scripts/autoload/shop.gd
Normal file
50
scripts/autoload/shop.gd
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
extends Node
|
||||
## Mock Pay-to-Win shop. 1€ = 1 Dresslerpunkt (no real payments).
|
||||
|
||||
signal points_changed(points: int)
|
||||
signal purchase_made(item_id: String)
|
||||
|
||||
var dressler_points: int = 0
|
||||
|
||||
const CATALOG := [
|
||||
{"id": "buy_point", "cost": 0, "mock_euro": true},
|
||||
{"id": "sunglasses", "cost": 5, "gives": "sunglasses"},
|
||||
{"id": "death_ticket", "cost": 3, "gives": "death_ticket"},
|
||||
{"id": "milk", "cost": 10, "gives": "milk"},
|
||||
{"id": "tiramisu", "cost": 1, "gives": "tiramisu"},
|
||||
]
|
||||
|
||||
func buy_mock_euro_point() -> void:
|
||||
## Satire: pretend the user paid 1€.
|
||||
dressler_points += 1
|
||||
points_changed.emit(dressler_points)
|
||||
GameState.save_game()
|
||||
|
||||
func purchase(entry_id: String) -> bool:
|
||||
if entry_id == "buy_point":
|
||||
buy_mock_euro_point()
|
||||
return true
|
||||
for entry in CATALOG:
|
||||
if str(entry["id"]) != entry_id:
|
||||
continue
|
||||
var cost := int(entry.get("cost", 0))
|
||||
if dressler_points < cost:
|
||||
return false
|
||||
dressler_points -= cost
|
||||
var gives := str(entry.get("gives", ""))
|
||||
if gives != "":
|
||||
Inventory.add_item(gives, 1)
|
||||
if gives == "sunglasses":
|
||||
Achievements.unlock("cool_sunglasses")
|
||||
points_changed.emit(dressler_points)
|
||||
purchase_made.emit(entry_id)
|
||||
GameState.save_game()
|
||||
return true
|
||||
return false
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
return {"dressler_points": dressler_points}
|
||||
|
||||
func deserialize(data: Dictionary) -> void:
|
||||
dressler_points = int(data.get("dressler_points", 0))
|
||||
points_changed.emit(dressler_points)
|
||||
37
scripts/billboard/billboard_sprite.gd
Normal file
37
scripts/billboard/billboard_sprite.gd
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
extends Node3D
|
||||
class_name BillboardSprite
|
||||
## 2D sprite on a 3D plane that always faces the camera (Mario-tree style).
|
||||
## TODO: Swap placeholder textures for real pixel art.
|
||||
|
||||
@export var texture: Texture2D
|
||||
@export var pixel_size: float = 0.05
|
||||
@export var modulate_color: Color = Color.WHITE
|
||||
|
||||
@onready var _mesh_instance: MeshInstance3D = $MeshInstance3D
|
||||
|
||||
func _ready() -> void:
|
||||
_apply()
|
||||
|
||||
func set_texture(tex: Texture2D) -> void:
|
||||
texture = tex
|
||||
_apply()
|
||||
|
||||
func _apply() -> void:
|
||||
if _mesh_instance == null:
|
||||
return
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(1.0, 1.0)
|
||||
_mesh_instance.mesh = plane
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
|
||||
mat.alpha_scissor_threshold = 0.1
|
||||
mat.albedo_color = modulate_color
|
||||
if texture != null:
|
||||
mat.albedo_texture = texture
|
||||
var w := float(texture.get_width()) * pixel_size
|
||||
var h := float(texture.get_height()) * pixel_size
|
||||
plane.size = Vector2(w, h)
|
||||
_mesh_instance.material_override = mat
|
||||
25
scripts/combat/exit_trigger.gd
Normal file
25
scripts/combat/exit_trigger.gd
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
extends Area3D
|
||||
class_name ExitTrigger
|
||||
## Walk into this volume to advance or jump to a scene.
|
||||
|
||||
@export var next_scene_id: String = ""
|
||||
@export var use_goto_next: bool = true
|
||||
@export var require_flag: String = ""
|
||||
|
||||
func _ready() -> void:
|
||||
body_entered.connect(_on_body_entered)
|
||||
collision_layer = 0
|
||||
collision_mask = 2 # player layer
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if not body.is_in_group("player"):
|
||||
return
|
||||
if require_flag != "" and not GameState.get_flag(require_flag, false):
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("exit_hint") + " (?)")
|
||||
return
|
||||
if use_goto_next and next_scene_id == "":
|
||||
GameState.goto_next()
|
||||
elif next_scene_id != "":
|
||||
GameState.goto_scene(next_scene_id)
|
||||
24
scripts/combat/hazard_zone.gd
Normal file
24
scripts/combat/hazard_zone.gd
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
extends Area3D
|
||||
class_name HazardZone
|
||||
## Damages the player on enter.
|
||||
|
||||
@export var damage: int = 1
|
||||
@export var death_cause_key: String = "death_default"
|
||||
@export var cooldown: float = 1.0
|
||||
|
||||
var _ready_to_hurt: bool = true
|
||||
|
||||
func _ready() -> void:
|
||||
body_entered.connect(_on_body_entered)
|
||||
collision_layer = 0
|
||||
collision_mask = 2
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if not _ready_to_hurt:
|
||||
return
|
||||
if not body.is_in_group("player"):
|
||||
return
|
||||
_ready_to_hurt = false
|
||||
GameState.take_damage(damage, death_cause_key)
|
||||
await get_tree().create_timer(cooldown).timeout
|
||||
_ready_to_hurt = true
|
||||
43
scripts/combat/hittable.gd
Normal file
43
scripts/combat/hittable.gd
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
extends Node3D
|
||||
class_name HittableEntity
|
||||
## Generic enemy / boss stub with hit points.
|
||||
|
||||
signal defeated
|
||||
signal hit(remaining_hp: int)
|
||||
|
||||
@export var max_hp: int = 4
|
||||
@export var texture_path: String = "res://assets/sprites/placeholders/enemy.png"
|
||||
@export var defeat_flag: String = ""
|
||||
@export var achievement_on_defeat: String = ""
|
||||
|
||||
var hp: int = 4
|
||||
var _alive: bool = true
|
||||
|
||||
@onready var _billboard: BillboardSprite = $BillboardSprite
|
||||
|
||||
func _ready() -> void:
|
||||
hp = max_hp
|
||||
add_to_group("enemy")
|
||||
add_to_group("interactable")
|
||||
if _billboard:
|
||||
var tex: Texture2D = load(texture_path)
|
||||
if tex:
|
||||
_billboard.set_texture(tex)
|
||||
|
||||
func take_hit(amount: int = 1) -> void:
|
||||
if not _alive:
|
||||
return
|
||||
hp = max(0, hp - amount)
|
||||
hit.emit(hp)
|
||||
AudioDirector.play_hit()
|
||||
if hp <= 0:
|
||||
_alive = false
|
||||
if defeat_flag != "":
|
||||
GameState.set_flag(defeat_flag, true)
|
||||
if achievement_on_defeat != "":
|
||||
Achievements.unlock(achievement_on_defeat)
|
||||
defeated.emit()
|
||||
queue_free()
|
||||
|
||||
func on_interact(_player: Node) -> void:
|
||||
take_hit(1)
|
||||
43
scripts/combat/interactable_npc.gd
Normal file
43
scripts/combat/interactable_npc.gd
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
extends Node3D
|
||||
class_name InteractableNPC
|
||||
## Billboard NPC with dialog key and optional quest hook.
|
||||
|
||||
signal interacted(npc: InteractableNPC)
|
||||
|
||||
@export var dialog_key: String = "dialog_hongkong_npc"
|
||||
@export var texture_path: String = "res://assets/sprites/placeholders/npc.png"
|
||||
@export var give_item_id: String = ""
|
||||
@export var require_item_id: String = ""
|
||||
@export var consume_required: bool = false
|
||||
@export var quest_flag: String = ""
|
||||
@export var achievement_id: String = ""
|
||||
@export var on_success_goto: String = ""
|
||||
|
||||
@onready var _billboard: BillboardSprite = $BillboardSprite
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("interactable")
|
||||
if _billboard:
|
||||
var tex: Texture2D = load(texture_path)
|
||||
if tex:
|
||||
_billboard.set_texture(tex)
|
||||
|
||||
func on_interact(_player: Node) -> void:
|
||||
interacted.emit(self)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key(dialog_key))
|
||||
if require_item_id != "" and not Inventory.has_item(require_item_id):
|
||||
return
|
||||
if require_item_id != "" and consume_required:
|
||||
Inventory.remove_item(require_item_id, 1)
|
||||
if give_item_id != "":
|
||||
Inventory.add_item(give_item_id, 1)
|
||||
if quest_flag != "":
|
||||
GameState.set_flag(quest_flag, true)
|
||||
if quest_flag == "house_built":
|
||||
GameState.house_built = true
|
||||
if achievement_id != "":
|
||||
Achievements.unlock(achievement_id)
|
||||
if on_success_goto != "":
|
||||
GameState.goto_scene(on_success_goto)
|
||||
36
scripts/levels/death_screen.gd
Normal file
36
scripts/levels/death_screen.gd
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
extends Control
|
||||
## Blinky death screen with cause text + skull every 10s.
|
||||
## TODO: Final flashy death art.
|
||||
|
||||
@onready var title: Label = $Title
|
||||
@onready var cause: Label = $Cause
|
||||
@onready var cont: Button = $ContinueButton
|
||||
@onready var skull: ColorRect = $Skull
|
||||
|
||||
var _blink: float = 0.0
|
||||
var _skull_timer: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "death_screen"
|
||||
_refresh()
|
||||
Locale.locale_changed.connect(_refresh)
|
||||
skull.visible = false
|
||||
|
||||
func _refresh() -> void:
|
||||
title.text = Locale.tr_key("death_title")
|
||||
cause.text = Locale.tr_key(GameState.last_death_cause_key)
|
||||
cont.text = Locale.tr_key("death_continue")
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_blink += delta
|
||||
modulate = Color(1, 0.4 + 0.6 * abs(sin(_blink * 6.0)), 0.4 + 0.6 * abs(sin(_blink * 4.0)))
|
||||
_skull_timer += delta
|
||||
if _skull_timer >= 10.0:
|
||||
_skull_timer = 0.0
|
||||
skull.visible = true
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
skull.visible = false
|
||||
|
||||
func _on_continue_pressed() -> void:
|
||||
GameState.heal_full()
|
||||
GameState.goto_scene("title_screen")
|
||||
8
scripts/levels/end_screen.gd
Normal file
8
scripts/levels/end_screen.gd
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
extends Node3D
|
||||
## End village with house-build stub.
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "end_screen"
|
||||
GameState.heal_full()
|
||||
if GameState.get_flag("house_built", false) or GameState.house_built:
|
||||
GameState.house_built = true
|
||||
7
scripts/levels/farm.gd
Normal file
7
scripts/levels/farm.gd
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
extends Node3D
|
||||
## Level stub: farm
|
||||
## TODO: Replace placeholders with final pixel art / audio.
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "farm"
|
||||
GameState.heal_full()
|
||||
7
scripts/levels/farm_path.gd
Normal file
7
scripts/levels/farm_path.gd
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
extends Node3D
|
||||
## Level stub: farm_path
|
||||
## TODO: Replace placeholders with final pixel art / audio.
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "farm_path"
|
||||
GameState.heal_full()
|
||||
9
scripts/levels/final_boss.gd
Normal file
9
scripts/levels/final_boss.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
extends Node3D
|
||||
## Hongkong / Glitschkönig final boss.
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "final_boss"
|
||||
GameState.heal_full()
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_hongkong_boss"))
|
||||
35
scripts/levels/intro_cutscene.gd
Normal file
35
scripts/levels/intro_cutscene.gd
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
extends Control
|
||||
## Intro: desk + console + explosion.
|
||||
## TODO: Real cutscene art and epic narrator VO.
|
||||
|
||||
@onready var label: Label = $Label
|
||||
|
||||
var _step: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "intro_cutscene"
|
||||
AudioDirector.stop_music()
|
||||
_show_step()
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("skip_cutscene") or event.is_action_pressed("interact") or event.is_action_pressed("attack"):
|
||||
_advance()
|
||||
|
||||
func _show_step() -> void:
|
||||
match _step:
|
||||
0:
|
||||
label.text = Locale.tr_key("dialog_intro_1")
|
||||
1:
|
||||
label.text = Locale.tr_key("dialog_intro_2")
|
||||
2:
|
||||
AudioDirector.play_explosion()
|
||||
label.text = "BOOM"
|
||||
_:
|
||||
GameState.goto_scene("village")
|
||||
|
||||
func _advance() -> void:
|
||||
_step += 1
|
||||
_show_step()
|
||||
|
||||
func _on_skip_pressed() -> void:
|
||||
_advance()
|
||||
7
scripts/levels/massage_studio.gd
Normal file
7
scripts/levels/massage_studio.gd
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
extends Node3D
|
||||
## Level stub: massage_studio
|
||||
## TODO: Replace placeholders with final pixel art / audio.
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "massage_studio"
|
||||
GameState.heal_full()
|
||||
7
scripts/levels/meadow.gd
Normal file
7
scripts/levels/meadow.gd
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
extends Node3D
|
||||
## Level stub: meadow
|
||||
## TODO: Replace placeholders with final pixel art / audio.
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "meadow"
|
||||
GameState.heal_full()
|
||||
7
scripts/levels/moon.gd
Normal file
7
scripts/levels/moon.gd
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
extends Node3D
|
||||
## Moon cheese level.
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "moon"
|
||||
GameState.heal_full()
|
||||
Achievements.unlock("moon_cheese")
|
||||
9
scripts/levels/shoe_boss.gd
Normal file
9
scripts/levels/shoe_boss.gd
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
extends Node3D
|
||||
## Schuhmacher Reif boss under the table.
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "shoe_boss"
|
||||
GameState.heal_full()
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_reif"))
|
||||
49
scripts/levels/title_screen.gd
Normal file
49
scripts/levels/title_screen.gd
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
extends Control
|
||||
## Title screen with placeholder rain + loud music.
|
||||
## TODO: Real title art (Mario/Sonic/Burger rain, disturbed human).
|
||||
|
||||
@onready var subtitle: Label = $Subtitle
|
||||
@onready var bubble: Label = $Bubble
|
||||
@onready var start_btn: Button = $StartButton
|
||||
@onready var rain: Node2D = $Rain
|
||||
|
||||
var _rain_sprites: Array[Sprite2D] = []
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "title_screen"
|
||||
AudioDirector.burger_cham_enabled = true
|
||||
AudioDirector.play_title_music()
|
||||
_refresh()
|
||||
Locale.locale_changed.connect(_refresh)
|
||||
_spawn_rain()
|
||||
|
||||
func _refresh() -> void:
|
||||
subtitle.text = Locale.tr_key("title_subtitle")
|
||||
bubble.text = Locale.tr_key("title_bubble")
|
||||
start_btn.text = Locale.tr_key("menu_start")
|
||||
|
||||
func _spawn_rain() -> void:
|
||||
var paths = [
|
||||
"res://assets/sprites/placeholders/mario.png",
|
||||
"res://assets/sprites/placeholders/sonic.png",
|
||||
"res://assets/sprites/placeholders/burger.png",
|
||||
]
|
||||
for i in range(24):
|
||||
var s := Sprite2D.new()
|
||||
s.texture = load(paths[i % paths.size()])
|
||||
s.position = Vector2(randf() * 1280.0, randf() * -720.0)
|
||||
s.scale = Vector2(3, 3)
|
||||
s.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
rain.add_child(s)
|
||||
_rain_sprites.append(s)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
for s in _rain_sprites:
|
||||
s.position.y += (80.0 + s.get_index() * 3.0) * delta
|
||||
if s.position.y > 760.0:
|
||||
s.position.y = -40.0
|
||||
s.position.x = randf() * 1280.0
|
||||
|
||||
func _on_start_pressed() -> void:
|
||||
AudioDirector.burger_cham_enabled = false
|
||||
GameState.start_new_game()
|
||||
7
scripts/levels/village.gd
Normal file
7
scripts/levels/village.gd
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
extends Node3D
|
||||
## Level stub: village
|
||||
## TODO: Replace placeholders with final pixel art / audio.
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "village"
|
||||
GameState.heal_full()
|
||||
58
scripts/player/player_controller.gd
Normal file
58
scripts/player/player_controller.gd
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
extends CharacterBody3D
|
||||
class_name PlayerController
|
||||
## Simple top-down-ish 3D movement for billboard levels.
|
||||
|
||||
@export var move_speed: float = 6.0
|
||||
@export var attack_range: float = 1.8
|
||||
@export var attack_damage: int = 1
|
||||
|
||||
signal interacted
|
||||
signal attacked
|
||||
|
||||
@onready var _billboard: BillboardSprite = $BillboardSprite
|
||||
|
||||
var _locked: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("player")
|
||||
if _billboard and _billboard.texture == null:
|
||||
var tex: Texture2D = load("res://assets/sprites/placeholders/player.png")
|
||||
if tex:
|
||||
_billboard.set_texture(tex)
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if _locked or get_tree().paused:
|
||||
velocity = Vector3.ZERO
|
||||
return
|
||||
var input := Vector2(
|
||||
Input.get_axis("move_left", "move_right"),
|
||||
Input.get_axis("move_up", "move_down")
|
||||
)
|
||||
var dir := Vector3(input.x, 0.0, input.y)
|
||||
if dir.length() > 1.0:
|
||||
dir = dir.normalized()
|
||||
velocity = dir * move_speed
|
||||
move_and_slide()
|
||||
if Input.is_action_just_pressed("interact"):
|
||||
_try_interact()
|
||||
if Input.is_action_just_pressed("attack"):
|
||||
_try_attack()
|
||||
|
||||
func lock_input(locked: bool) -> void:
|
||||
_locked = locked
|
||||
|
||||
func _try_interact() -> void:
|
||||
interacted.emit()
|
||||
for body in get_tree().get_nodes_in_group("interactable"):
|
||||
if body is Node3D and global_position.distance_to(body.global_position) <= 2.2:
|
||||
if body.has_method("on_interact"):
|
||||
body.on_interact(self)
|
||||
return
|
||||
|
||||
func _try_attack() -> void:
|
||||
attacked.emit()
|
||||
AudioDirector.play_hit()
|
||||
for body in get_tree().get_nodes_in_group("enemy"):
|
||||
if body is Node3D and global_position.distance_to(body.global_position) <= attack_range:
|
||||
if body.has_method("take_hit"):
|
||||
body.take_hit(attack_damage)
|
||||
1117
scripts/tools/generate_scaffold.py
Normal file
1117
scripts/tools/generate_scaffold.py
Normal file
File diff suppressed because it is too large
Load diff
161
scripts/ui/ui_root.gd
Normal file
161
scripts/ui/ui_root.gd
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
extends CanvasLayer
|
||||
## Global UI: pause, inventory, shop, settings, toast, dialog, burger-cham.
|
||||
|
||||
@onready var pause_panel: PanelContainer = $PausePanel
|
||||
@onready var dialog_panel: PanelContainer = $DialogPanel
|
||||
@onready var dialog_label: Label = $DialogPanel/Margin/DialogLabel
|
||||
@onready var toast_label: Label = $ToastLabel
|
||||
@onready var burger_overlay: ColorRect = $BurgerOverlay
|
||||
@onready var burger_label: Label = $BurgerOverlay/BurgerLabel
|
||||
@onready var points_label: Label = $PausePanel/Margin/VBox/PointsLabel
|
||||
@onready var inventory_list: ItemList = $PausePanel/Margin/VBox/InventoryList
|
||||
@onready var sfx_slider: HSlider = $PausePanel/Margin/VBox/SfxSlider
|
||||
@onready var music_slider: HSlider = $PausePanel/Margin/VBox/MusicSlider
|
||||
@onready var lang_option: OptionButton = $PausePanel/Margin/VBox/LangOption
|
||||
@onready var hp_label: Label = $HUD/HpLabel
|
||||
|
||||
var _dialog_timer: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
layer = 100
|
||||
pause_panel.visible = false
|
||||
dialog_panel.visible = false
|
||||
burger_overlay.visible = false
|
||||
toast_label.visible = false
|
||||
_setup_lang_option()
|
||||
_refresh_texts()
|
||||
sfx_slider.value = Settings.sfx_volume
|
||||
music_slider.value = 1.0
|
||||
music_slider.editable = false
|
||||
Achievements.unlocked.connect(_on_achievement)
|
||||
AudioDirector.burger_cham_triggered.connect(_on_burger_cham)
|
||||
Settings.settings_changed.connect(_refresh_texts)
|
||||
Locale.locale_changed.connect(_refresh_texts)
|
||||
Inventory.inventory_changed.connect(_refresh_inventory)
|
||||
Shop.points_changed.connect(_refresh_points)
|
||||
GameState.hp_changed.connect(_on_hp)
|
||||
_on_hp(GameState.hp, GameState.max_hp)
|
||||
_refresh_inventory()
|
||||
_refresh_points(Shop.dressler_points)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _dialog_timer > 0.0:
|
||||
_dialog_timer -= delta
|
||||
if _dialog_timer <= 0.0:
|
||||
dialog_panel.visible = false
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("pause_menu"):
|
||||
toggle_pause()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func toggle_pause() -> void:
|
||||
if GameState.current_scene_id in ["title_screen", "death_screen", "intro_cutscene"]:
|
||||
return
|
||||
pause_panel.visible = not pause_panel.visible
|
||||
get_tree().paused = pause_panel.visible
|
||||
if pause_panel.visible:
|
||||
_refresh_texts()
|
||||
_refresh_inventory()
|
||||
|
||||
func show_dialog(text: String, duration: float = 3.5) -> void:
|
||||
dialog_label.text = text
|
||||
dialog_panel.visible = true
|
||||
_dialog_timer = duration
|
||||
|
||||
func _setup_lang_option() -> void:
|
||||
lang_option.clear()
|
||||
lang_option.add_item("Deutsch", 0)
|
||||
lang_option.add_item("English", 1)
|
||||
lang_option.add_item("Bayerisch", 2)
|
||||
match Settings.language:
|
||||
"en":
|
||||
lang_option.select(1)
|
||||
"by":
|
||||
lang_option.select(2)
|
||||
_:
|
||||
lang_option.select(0)
|
||||
|
||||
func _refresh_texts() -> void:
|
||||
$PausePanel/Margin/VBox/TitleLabel.text = Locale.tr_key("menu_settings")
|
||||
$PausePanel/Margin/VBox/ResumeButton.text = Locale.tr_key("menu_resume")
|
||||
$PausePanel/Margin/VBox/ShopTitle.text = Locale.tr_key("menu_shop")
|
||||
$PausePanel/Margin/VBox/BuyPointButton.text = Locale.tr_key("menu_buy_point")
|
||||
$PausePanel/Margin/VBox/BuySunglassesButton.text = Locale.tr_key("shop_sunglasses")
|
||||
$PausePanel/Margin/VBox/BuyTicketButton.text = Locale.tr_key("shop_ticket")
|
||||
$PausePanel/Margin/VBox/BuyMilkButton.text = Locale.tr_key("shop_milk")
|
||||
$PausePanel/Margin/VBox/BuyTiramisuButton.text = Locale.tr_key("shop_tiramisu")
|
||||
$PausePanel/Margin/VBox/SfxLabel.text = Locale.tr_key("menu_sfx")
|
||||
$PausePanel/Margin/VBox/MusicLabel.text = Locale.tr_key("menu_music")
|
||||
$PausePanel/Margin/VBox/LangLabel.text = Locale.tr_key("menu_language")
|
||||
$PausePanel/Margin/VBox/InventoryLabel.text = Locale.tr_key("menu_inventory")
|
||||
_refresh_points(Shop.dressler_points)
|
||||
|
||||
func _refresh_points(points: int) -> void:
|
||||
points_label.text = Locale.tr_key("menu_points", [str(points)])
|
||||
|
||||
func _refresh_inventory() -> void:
|
||||
inventory_list.clear()
|
||||
var items := Inventory.get_all()
|
||||
for id in items.keys():
|
||||
var count: int = int(items[id])
|
||||
var item_name: String = Locale.tr_key("item_" + str(id))
|
||||
inventory_list.add_item("%s x%d" % [item_name, count])
|
||||
inventory_list.set_item_metadata(inventory_list.item_count - 1, id)
|
||||
|
||||
func _on_hp(hp: int, max_hp: int) -> void:
|
||||
hp_label.text = "HP %d/%d" % [hp, max_hp]
|
||||
|
||||
func _on_achievement(_id: String, title: String) -> void:
|
||||
toast_label.text = Locale.tr_key("toast_achievement", [title])
|
||||
toast_label.visible = true
|
||||
await get_tree().create_timer(3.0).timeout
|
||||
toast_label.visible = false
|
||||
|
||||
func _on_burger_cham() -> void:
|
||||
burger_label.text = Locale.tr_key("burger_cham")
|
||||
burger_overlay.visible = true
|
||||
await get_tree().create_timer(2.5).timeout
|
||||
burger_overlay.visible = false
|
||||
|
||||
func _on_resume_pressed() -> void:
|
||||
pause_panel.visible = false
|
||||
get_tree().paused = false
|
||||
|
||||
func _on_buy_point() -> void:
|
||||
Shop.purchase("buy_point")
|
||||
|
||||
func _on_buy_sunglasses() -> void:
|
||||
Shop.purchase("sunglasses")
|
||||
|
||||
func _on_buy_ticket() -> void:
|
||||
Shop.purchase("death_ticket")
|
||||
|
||||
func _on_buy_milk() -> void:
|
||||
Shop.purchase("milk")
|
||||
|
||||
func _on_buy_tiramisu() -> void:
|
||||
Shop.purchase("tiramisu")
|
||||
|
||||
func _on_sfx_changed(value: float) -> void:
|
||||
Settings.set_sfx_volume(value)
|
||||
|
||||
func _on_music_changed(_value: float) -> void:
|
||||
Settings.set_music_volume(1.0)
|
||||
music_slider.value = 1.0
|
||||
|
||||
func _on_lang_selected(index: int) -> void:
|
||||
match index:
|
||||
1:
|
||||
Settings.set_language("en")
|
||||
2:
|
||||
Settings.set_language("by")
|
||||
_:
|
||||
Settings.set_language("de")
|
||||
|
||||
func _on_inventory_activated(index: int) -> void:
|
||||
var id: Variant = inventory_list.get_item_metadata(index)
|
||||
if typeof(id) == TYPE_STRING:
|
||||
Inventory.use_item(str(id))
|
||||
_refresh_inventory()
|
||||
Loading…
Add table
Add a link
Reference in a new issue