Wire quest flow and combat across all levels.
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.
This commit is contained in:
parent
938cc4bc82
commit
1d9e0a6532
51 changed files with 1899 additions and 251 deletions
|
|
@ -24,7 +24,7 @@ func _ready() -> void:
|
|||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
_music = AudioStreamPlayer.new()
|
||||
_music.bus = "Music"
|
||||
_music.volume_db = 6.0 # intentionally loud
|
||||
_music.volume_db = 12.0 # intentionally painfully loud
|
||||
add_child(_music)
|
||||
_sfx = AudioStreamPlayer.new()
|
||||
_sfx.bus = "SFX"
|
||||
|
|
@ -48,7 +48,7 @@ func play_title_music() -> void:
|
|||
if stream == null:
|
||||
return
|
||||
_music.stream = stream
|
||||
_music.volume_db = 6.0
|
||||
_music.volume_db = 12.0
|
||||
if not _music.playing:
|
||||
_music.play()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,12 @@
|
|||
extends Node
|
||||
## Global game flow, HP, death, playtime, save/load.
|
||||
## TODO: Expand save schema when real progression exists.
|
||||
## 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_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",
|
||||
|
|
@ -28,23 +15,47 @@ const SCENE_PATHS := {
|
|||
"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 = 5
|
||||
var hp: int = 5
|
||||
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()
|
||||
|
|
@ -57,6 +68,42 @@ func _process(delta: float) -> void:
|
|||
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):
|
||||
|
|
@ -70,28 +117,64 @@ func goto_scene(scene_id: String) -> void:
|
|||
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])
|
||||
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:
|
||||
|
|
@ -109,6 +192,18 @@ func die(cause_key: String = "death_default") -> void:
|
|||
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()
|
||||
|
|
@ -116,7 +211,12 @@ func set_flag(key: String, value: Variant = true) -> void:
|
|||
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,
|
||||
|
|
@ -126,6 +226,8 @@ func save_game() -> void:
|
|||
"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(),
|
||||
|
|
@ -133,7 +235,6 @@ func save_game() -> void:
|
|||
}
|
||||
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"))
|
||||
|
||||
|
|
@ -155,6 +256,8 @@ func load_game() -> void:
|
|||
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", {}))
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ extends Node
|
|||
signal inventory_changed
|
||||
signal item_used(item_id: String)
|
||||
|
||||
var _items: Dictionary = {} # id -> count
|
||||
var _items: Dictionary = {}
|
||||
var _defs: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -13,7 +13,6 @@ func _ready() -> void:
|
|||
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:
|
||||
|
|
@ -64,13 +63,30 @@ func use_item(item_id: String) -> bool:
|
|||
remove_item("death_ticket", 1)
|
||||
Achievements.unlock("eaten_to_death")
|
||||
item_used.emit(item_id)
|
||||
GameState.die("death_ticket_eaten")
|
||||
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")
|
||||
GameState.apply_fat(600.0)
|
||||
item_used.emit(item_id)
|
||||
return true
|
||||
"dressler_apple":
|
||||
remove_item("dressler_apple", 1)
|
||||
item_used.emit(item_id)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_boss_unclever"), 4.0)
|
||||
GameState.goto_scene("final_boss")
|
||||
return true
|
||||
"wedding_egg":
|
||||
remove_item("wedding_egg", 1)
|
||||
item_used.emit(item_id)
|
||||
GameState.set_flag("used_whiff", true)
|
||||
GameState.goto_scene("final_boss")
|
||||
return true
|
||||
"banana":
|
||||
remove_item("banana", 1)
|
||||
GameState.heal_full()
|
||||
item_used.emit(item_id)
|
||||
return true
|
||||
_:
|
||||
|
|
|
|||
|
|
@ -11,11 +11,14 @@ var language: String = "de"
|
|||
var sfx_volume: float = 1.0
|
||||
## Intentionally locked loud — slider is UI-disabled.
|
||||
var music_volume: float = 1.0
|
||||
var language_chosen: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
_ensure_buses()
|
||||
apply_audio()
|
||||
# Locale reloads after both Settings + Locale exist (Locale._ready / deserialize).
|
||||
|
||||
func needs_language_picker() -> bool:
|
||||
return not language_chosen
|
||||
|
||||
func _ensure_buses() -> void:
|
||||
_ensure_bus(MUSIC_BUS)
|
||||
|
|
@ -34,6 +37,7 @@ func set_language(lang: String) -> void:
|
|||
if lang not in ["de", "en", "by"]:
|
||||
return
|
||||
language = lang
|
||||
language_chosen = true
|
||||
Locale.reload()
|
||||
AudioDirector.play_language_voice(lang)
|
||||
settings_changed.emit()
|
||||
|
|
@ -46,7 +50,6 @@ func set_sfx_volume(value: float) -> void:
|
|||
GameState.save_game()
|
||||
|
||||
func set_music_volume(_value: float) -> void:
|
||||
## Satire lock: music stays maxed.
|
||||
music_volume = 1.0
|
||||
apply_audio()
|
||||
settings_changed.emit()
|
||||
|
|
@ -68,12 +71,14 @@ func serialize() -> Dictionary:
|
|||
"language": language,
|
||||
"sfx_volume": sfx_volume,
|
||||
"music_volume": 1.0,
|
||||
"language_chosen": language_chosen,
|
||||
}
|
||||
|
||||
func deserialize(data: Dictionary) -> void:
|
||||
language = str(data.get("language", "de"))
|
||||
sfx_volume = float(data.get("sfx_volume", 1.0))
|
||||
music_volume = 1.0
|
||||
language_chosen = bool(data.get("language_chosen", false))
|
||||
Locale.reload()
|
||||
apply_audio()
|
||||
settings_changed.emit()
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ 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"},
|
||||
{"id": "continue_joke", "cost": 10, "joke_continue": true},
|
||||
{"id": "death_ticket", "cost": 1, "gives": "death_ticket"},
|
||||
{"id": "sunglasses", "cost": 1, "gives": "sunglasses", "actually_free": true},
|
||||
{"id": "burger", "cost": 1, "gives": "tiramisu"},
|
||||
{"id": "next_level", "cost": 1, "unlock_next": true},
|
||||
]
|
||||
|
||||
func buy_mock_euro_point() -> void:
|
||||
## Satire: pretend the user paid 1€.
|
||||
dressler_points += 1
|
||||
points_changed.emit(dressler_points)
|
||||
GameState.save_game()
|
||||
|
|
@ -27,6 +27,25 @@ func purchase(entry_id: String) -> bool:
|
|||
for entry in CATALOG:
|
||||
if str(entry["id"]) != entry_id:
|
||||
continue
|
||||
if bool(entry.get("actually_free", false)):
|
||||
Inventory.add_item(str(entry.get("gives", "")), 1)
|
||||
if str(entry.get("gives", "")) == "sunglasses":
|
||||
Achievements.unlock("cool_sunglasses")
|
||||
purchase_made.emit(entry_id)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("shop_joke_free"), 3.0)
|
||||
GameState.save_game()
|
||||
return true
|
||||
if bool(entry.get("joke_continue", false)):
|
||||
## „kostet 10 Punkte“ → Spaß, du darfst weiter
|
||||
var ui2 := get_node_or_null("/root/UIRoot")
|
||||
if ui2 and ui2.has_method("show_dialog"):
|
||||
ui2.show_dialog(Locale.tr_key("shop_joke_continue"), 4.0)
|
||||
GameState.set_flag("paywall_cleared", true)
|
||||
purchase_made.emit(entry_id)
|
||||
GameState.save_game()
|
||||
return true
|
||||
var cost := int(entry.get("cost", 0))
|
||||
if dressler_points < cost:
|
||||
return false
|
||||
|
|
@ -34,8 +53,8 @@ func purchase(entry_id: String) -> bool:
|
|||
var gives := str(entry.get("gives", ""))
|
||||
if gives != "":
|
||||
Inventory.add_item(gives, 1)
|
||||
if gives == "sunglasses":
|
||||
Achievements.unlock("cool_sunglasses")
|
||||
if bool(entry.get("unlock_next", false)):
|
||||
GameState.set_flag("paywall_cleared", true)
|
||||
points_changed.emit(dressler_points)
|
||||
purchase_made.emit(entry_id)
|
||||
GameState.save_game()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
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.
|
||||
## Mario-tree billboard: upright Quad (perpendicular to ground), yaw-only toward camera.
|
||||
## PlaneMesh defaults to flat on XZ — that made sprites look like floor stickers.
|
||||
|
||||
@export var texture: Texture2D
|
||||
@export var pixel_size: float = 0.05
|
||||
|
|
@ -9,9 +9,14 @@ class_name BillboardSprite
|
|||
|
||||
@onready var _mesh_instance: MeshInstance3D = $MeshInstance3D
|
||||
|
||||
var _quad_height: float = 1.0
|
||||
|
||||
func _ready() -> void:
|
||||
_apply()
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_face_camera_yaw_only()
|
||||
|
||||
func set_texture(tex: Texture2D) -> void:
|
||||
texture = tex
|
||||
_apply()
|
||||
|
|
@ -19,19 +24,41 @@ func set_texture(tex: Texture2D) -> void:
|
|||
func _apply() -> void:
|
||||
if _mesh_instance == null:
|
||||
return
|
||||
var plane := PlaneMesh.new()
|
||||
plane.size = Vector2(1.0, 1.0)
|
||||
_mesh_instance.mesh = plane
|
||||
# QuadMesh is vertical (XY), facing +Z — correct default for standing sprites.
|
||||
var quad := QuadMesh.new()
|
||||
var w := 1.0
|
||||
var h := 1.0
|
||||
if texture != null:
|
||||
w = float(texture.get_width()) * pixel_size
|
||||
h = float(texture.get_height()) * pixel_size
|
||||
quad.size = Vector2(w, h)
|
||||
_quad_height = h
|
||||
_mesh_instance.mesh = quad
|
||||
# Pivot at feet: lift mesh so bottom sits on this node's origin.
|
||||
_mesh_instance.position = Vector3(0.0, h * 0.5, 0.0)
|
||||
_mesh_instance.rotation = Vector3.ZERO
|
||||
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
# No engine billboard — we yaw the Node3D ourselves so orientation stays upright.
|
||||
mat.billboard_mode = BaseMaterial3D.BILLBOARD_DISABLED
|
||||
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.cull_mode = BaseMaterial3D.CULL_DISABLED # visible from both sides
|
||||
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
|
||||
|
||||
func _face_camera_yaw_only() -> void:
|
||||
var cam := get_viewport().get_camera_3d()
|
||||
if cam == null:
|
||||
return
|
||||
var to_cam := cam.global_position - global_position
|
||||
to_cam.y = 0.0
|
||||
if to_cam.length_squared() < 0.0001:
|
||||
return
|
||||
# QuadMesh faces +Z; look_at aims -Z at the target — aim -Z away from camera
|
||||
# so +Z faces the camera.
|
||||
look_at(global_position - to_cam, Vector3.UP)
|
||||
|
|
|
|||
40
scripts/combat/cheese_projectile.gd
Normal file
40
scripts/combat/cheese_projectile.gd
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
extends Area3D
|
||||
class_name CheeseProjectile
|
||||
## Moldy cheese shot at the player. Attack the cheese (not the seller) to destroy it.
|
||||
|
||||
var velocity: Vector3 = Vector3.ZERO
|
||||
var lifetime: float = 5.0
|
||||
var hp: int = 1
|
||||
|
||||
func _ready() -> void:
|
||||
collision_layer = 4
|
||||
collision_mask = 2
|
||||
monitoring = true
|
||||
body_entered.connect(_on_body)
|
||||
add_to_group("enemy")
|
||||
add_to_group("cheese_projectile")
|
||||
var bill := get_node_or_null("BillboardSprite")
|
||||
if bill and bill.has_method("set_texture"):
|
||||
var tex: Texture2D = load("res://assets/sprites/placeholders/cheese.png")
|
||||
if tex:
|
||||
bill.set_texture(tex)
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
global_position += velocity * delta
|
||||
lifetime -= delta
|
||||
if lifetime <= 0.0:
|
||||
call_deferred("queue_free")
|
||||
|
||||
func take_hit(_amount: int = 1) -> void:
|
||||
hp -= 1
|
||||
AudioDirector.play_hit()
|
||||
if hp <= 0:
|
||||
var seller = get_meta("seller", null)
|
||||
if seller != null and is_instance_valid(seller) and seller.has_method("notify_cheese_destroyed"):
|
||||
seller.notify_cheese_destroyed()
|
||||
call_deferred("queue_free")
|
||||
|
||||
func _on_body(body: Node3D) -> void:
|
||||
if body.is_in_group("player"):
|
||||
GameState.die("death_cheese")
|
||||
call_deferred("queue_free")
|
||||
67
scripts/combat/cheese_seller.gd
Normal file
67
scripts/combat/cheese_seller.gd
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
extends Node3D
|
||||
class_name CheeseSeller
|
||||
## Places a cheese table and periodically launches moldy cheese at the player.
|
||||
## DESIGN: stab the cheese (projectile), not the seller — seller dies of grief when cheese destroyed.
|
||||
|
||||
@export var texture_path: String = "res://assets/sprites/placeholders/cheese.png"
|
||||
@export var fire_interval: float = 2.8
|
||||
@export var cheese_speed: float = 7.0
|
||||
|
||||
var _alive: bool = true
|
||||
var _timer: float = 0.0
|
||||
var _grief: int = 0
|
||||
|
||||
@onready var _billboard: BillboardSprite = $BillboardSprite
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("interactable")
|
||||
add_to_group("cheese_seller")
|
||||
_timer = randf() * fire_interval
|
||||
if _billboard:
|
||||
var tex: Texture2D = load(texture_path)
|
||||
if tex:
|
||||
_billboard.set_texture(tex)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not _alive:
|
||||
return
|
||||
_timer -= delta
|
||||
if _timer <= 0.0:
|
||||
_timer = fire_interval
|
||||
_fire()
|
||||
|
||||
func _fire() -> void:
|
||||
var player := get_tree().get_first_node_in_group("player")
|
||||
if player == null:
|
||||
return
|
||||
var proj_scene: PackedScene = load("res://scenes/prefabs/cheese_projectile.tscn")
|
||||
if proj_scene == null:
|
||||
return
|
||||
var proj: Node3D = proj_scene.instantiate()
|
||||
get_tree().current_scene.add_child(proj)
|
||||
proj.global_position = global_position + Vector3(0, 0.8, 0)
|
||||
var dir: Vector3 = (player.global_position + Vector3(0, 0.8, 0) - proj.global_position).normalized()
|
||||
proj.set("velocity", dir * cheese_speed)
|
||||
proj.add_to_group("cheese_projectile")
|
||||
proj.add_to_group("enemy")
|
||||
proj.set_meta("seller", self)
|
||||
|
||||
func notify_cheese_destroyed() -> void:
|
||||
_grief += 1
|
||||
if _grief >= 2:
|
||||
_alive = false
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_cheese"))
|
||||
queue_free()
|
||||
|
||||
func on_interact(_player: Node) -> void:
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_cheese"))
|
||||
|
||||
func take_hit(_amount: int = 1) -> void:
|
||||
## Hitting the seller does nothing useful (DESIGN).
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_cheese"))
|
||||
21
scripts/levels/catscene.gd
Normal file
21
scripts/levels/catscene.gd
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
extends Control
|
||||
## Catscene: open-world joke. Skipping resets the run.
|
||||
|
||||
@onready var label: Label = $Label
|
||||
@onready var skip_btn: Button = $SkipButton
|
||||
@onready var cont_btn: Button = $ContinueButton
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "catscene"
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
label.text = Locale.tr_key("dialog_catscene")
|
||||
skip_btn.text = "Skip Katzin / Catscene"
|
||||
cont_btn.text = Locale.tr_key("menu_continue")
|
||||
|
||||
func _on_continue_pressed() -> void:
|
||||
GameState.goto_scene("farm_path")
|
||||
|
||||
func _on_skip_pressed() -> void:
|
||||
label.text = Locale.tr_key("dialog_catscene_skip")
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
GameState.reset_to_intro()
|
||||
|
|
@ -1,17 +1,28 @@
|
|||
extends Control
|
||||
## Blinky death screen with cause text + skull every 10s.
|
||||
## TODO: Final flashy death art.
|
||||
## Blinky death screen + absurd revive (Nein=revive, Ja=quit).
|
||||
|
||||
@onready var title: Label = $Title
|
||||
@onready var cause: Label = $Cause
|
||||
@onready var cont: Button = $ContinueButton
|
||||
@onready var btn_revive: Button = $ReviveButton
|
||||
@onready var btn_no: Button = $NoButton
|
||||
@onready var btn_yes: Button = $YesButton
|
||||
@onready var hint: Label = $Hint
|
||||
@onready var skull: ColorRect = $Skull
|
||||
@onready var bg: ColorRect = $Bg
|
||||
|
||||
var _blink: float = 0.0
|
||||
var _skull_timer: float = 0.0
|
||||
var _colors: Array[Color] = [
|
||||
Color(0.05, 0.05, 0.05),
|
||||
Color(0.9, 0.1, 0.4),
|
||||
Color(0.1, 0.8, 0.3),
|
||||
Color(0.2, 0.3, 1.0),
|
||||
Color(1.0, 0.8, 0.1),
|
||||
]
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "death_screen"
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
_refresh()
|
||||
Locale.locale_changed.connect(_refresh)
|
||||
skull.visible = false
|
||||
|
|
@ -19,11 +30,15 @@ func _ready() -> void:
|
|||
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")
|
||||
btn_revive.text = Locale.tr_key("death_revive")
|
||||
btn_no.text = Locale.tr_key("death_no")
|
||||
btn_yes.text = Locale.tr_key("death_yes")
|
||||
hint.text = Locale.tr_key("death_no_hint") + "\n" + Locale.tr_key("death_yes_hint")
|
||||
|
||||
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)))
|
||||
var idx := int(_blink) % _colors.size()
|
||||
bg.color = _colors[idx]
|
||||
_skull_timer += delta
|
||||
if _skull_timer >= 10.0:
|
||||
_skull_timer = 0.0
|
||||
|
|
@ -31,6 +46,13 @@ func _process(delta: float) -> void:
|
|||
await get_tree().create_timer(1.0).timeout
|
||||
skull.visible = false
|
||||
|
||||
func _on_continue_pressed() -> void:
|
||||
GameState.heal_full()
|
||||
GameState.goto_scene("title_screen")
|
||||
func _on_revive_pressed() -> void:
|
||||
GameState.revive_continue()
|
||||
|
||||
func _on_no_pressed() -> void:
|
||||
## Satire: Nein = trotzdem wiederbeleben
|
||||
GameState.revive_continue()
|
||||
|
||||
func _on_yes_pressed() -> void:
|
||||
## Satire: Ja = Spiel beenden → Title
|
||||
GameState.quit_to_title()
|
||||
|
|
|
|||
63
scripts/levels/dressler_boss.gd
Normal file
63
scripts/levels/dressler_boss.gd
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
extends Node3D
|
||||
## Dresslermeister: Augen öffnen, Mähdrescher hijacken, Apfel dreschen.
|
||||
|
||||
var _hijacked: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "dressler_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_dressler"))
|
||||
var dressler := get_node_or_null("Dressler")
|
||||
if dressler and dressler.has_signal("defeated"):
|
||||
dressler.defeated.connect(_on_dressler_defeated)
|
||||
# Convert mower hazards into hijackables
|
||||
for name in ["Mower1", "Mower2"]:
|
||||
var m := get_node_or_null(name)
|
||||
if m:
|
||||
_make_hijackable(m)
|
||||
|
||||
func _make_hijackable(mower: Node) -> void:
|
||||
mower.set_meta("hijacked", false)
|
||||
if mower.has_signal("body_entered"):
|
||||
# Keep death if not hijacked — wrap
|
||||
pass
|
||||
# Add interactable proxy as child Area isn't enough; use nearby attack
|
||||
mower.add_to_group("hijackable_mower")
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if Input.is_action_just_pressed("interact") or Input.is_action_just_pressed("attack"):
|
||||
_try_hijack()
|
||||
|
||||
func _try_hijack() -> void:
|
||||
var player := get_tree().get_first_node_in_group("player")
|
||||
if player == null:
|
||||
return
|
||||
for mower in get_tree().get_nodes_in_group("hijackable_mower"):
|
||||
if mower is Node3D and player.global_position.distance_to(mower.global_position) <= 2.5:
|
||||
if bool(mower.get_meta("hijacked", false)):
|
||||
continue
|
||||
mower.set_meta("hijacked", true)
|
||||
_hijacked += 1
|
||||
# Disable kill: remove from monitoring / free hazard script effect
|
||||
if mower.has_method("set"):
|
||||
mower.set("monitoring", false)
|
||||
if mower is Area3D:
|
||||
(mower as Area3D).monitoring = false
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog("Fahrer rausgeworfen! Dresche den Apfel! (%d/2)" % _hijacked)
|
||||
# Bonus damage to dressler for each hijack
|
||||
var dressler := get_node_or_null("Dressler")
|
||||
if dressler and dressler.has_method("take_hit"):
|
||||
dressler.take_hit(3)
|
||||
return
|
||||
|
||||
func _on_dressler_defeated() -> void:
|
||||
Achievements.unlock("dressler_apples")
|
||||
Inventory.add_item("dressler_apple", 1)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_dressler_drop"), 4.0)
|
||||
GameState.set_flag("dressler_defeated", true)
|
||||
|
|
@ -1,8 +1,52 @@
|
|||
extends Node3D
|
||||
## End village with house-build stub.
|
||||
## Enddorf: Hausbau + Zahnionär. Kein Happy End.
|
||||
|
||||
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
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_end_angry"), 4.0)
|
||||
var house := get_node_or_null("HouseSpot")
|
||||
if house and house.has_signal("interacted"):
|
||||
house.interacted.connect(_on_house)
|
||||
var dentist := get_node_or_null("DentistHeist")
|
||||
if dentist and dentist.has_signal("interacted"):
|
||||
dentist.interacted.connect(_on_heist)
|
||||
if GameState.house_built or GameState.get_flag("house_built", false):
|
||||
_show_house()
|
||||
|
||||
func _on_house(_n: Node = null) -> void:
|
||||
GameState.house_built = true
|
||||
GameState.set_flag("house_built", true)
|
||||
_show_house()
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("build_house"))
|
||||
|
||||
func _show_house() -> void:
|
||||
var house := get_node_or_null("BuiltHouse")
|
||||
if house:
|
||||
house.visible = true
|
||||
return
|
||||
var scene: PackedScene = load("res://scenes/prefabs/npc.tscn")
|
||||
if scene == null:
|
||||
return
|
||||
var h: Node3D = scene.instantiate()
|
||||
h.name = "BuiltHouse"
|
||||
add_child(h)
|
||||
h.global_position = Vector3(3, 0.1, -2)
|
||||
h.set("texture_path", "res://assets/sprites/placeholders/house.png")
|
||||
h.set("dialog_key", "build_house")
|
||||
var bill = h.get_node_or_null("BillboardSprite")
|
||||
if bill and bill.has_method("set_texture"):
|
||||
var tex: Texture2D = load("res://assets/sprites/placeholders/house.png")
|
||||
if tex:
|
||||
bill.set_texture(tex)
|
||||
|
||||
func _on_heist(_n: Node = null) -> void:
|
||||
Inventory.add_item("tooth", 100)
|
||||
Achievements.unlock("tooth_millionaire")
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_toothionaire"), 4.0)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,81 @@
|
|||
extends Node3D
|
||||
## Level stub: farm
|
||||
## TODO: Replace placeholders with final pixel art / audio.
|
||||
## Bayerischer Hof: 3 Quests → Paywall-Joke → Exit zu Dressler. Urhahn-Schock.
|
||||
|
||||
var _fed_pigs: bool = false
|
||||
var _milked: bool = false
|
||||
var _chickens: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "farm"
|
||||
GameState.heal_full()
|
||||
_fed_pigs = bool(GameState.get_flag("pigs_fed", false))
|
||||
_milked = bool(GameState.get_flag("cows_milked", false))
|
||||
_chickens = bool(GameState.get_flag("chickens_taken", false))
|
||||
var mood := str(GameState.get_flag("farmer_mood", "direct"))
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
if mood == "detour":
|
||||
ui.show_dialog(Locale.tr_key("dialog_farmer_detour"), 4.0)
|
||||
else:
|
||||
ui.show_dialog(Locale.tr_key("dialog_farmer_direct"), 3.5)
|
||||
await get_tree().create_timer(1.2).timeout
|
||||
ui.show_dialog(Locale.tr_key("dialog_farmer_quest"), 4.0)
|
||||
var urhahn := get_node_or_null("Urhahn")
|
||||
if urhahn and urhahn.has_signal("defeated"):
|
||||
urhahn.defeated.connect(_on_urhahn_down)
|
||||
var hazard := get_node_or_null("RoosterHazard")
|
||||
if hazard and hazard.has_signal("body_entered"):
|
||||
# Extra: shock before death for flavor when contacting hazard
|
||||
if not hazard.body_entered.is_connected(_on_rooster_touch):
|
||||
hazard.body_entered.connect(_on_rooster_touch)
|
||||
var exit := get_node_or_null("Exit")
|
||||
if exit:
|
||||
exit.set("require_flag", "paywall_cleared")
|
||||
var ticket := get_node_or_null("TicketVendor")
|
||||
if ticket and ticket.has_signal("interacted"):
|
||||
ticket.interacted.connect(_on_ticket)
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if GameState.get_flag("pigs_fed", false) and not _fed_pigs:
|
||||
complete_pig()
|
||||
if GameState.get_flag("cows_milked", false) and not _milked:
|
||||
complete_cow()
|
||||
if GameState.get_flag("chickens_taken", false) and not _chickens:
|
||||
complete_chicken()
|
||||
|
||||
func complete_pig() -> void:
|
||||
_fed_pigs = true
|
||||
_check_quests()
|
||||
|
||||
func complete_cow() -> void:
|
||||
_milked = true
|
||||
_check_quests()
|
||||
|
||||
func complete_chicken() -> void:
|
||||
_chickens = true
|
||||
if not Inventory.has_item("lava_chicken"):
|
||||
Inventory.add_item("lava_chicken", 1)
|
||||
_check_quests()
|
||||
|
||||
func _check_quests() -> void:
|
||||
if not (_fed_pigs and _milked and _chickens):
|
||||
return
|
||||
GameState.set_flag("farm_quests_done", true)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_paywall"), 4.0)
|
||||
Shop.purchase("continue_joke")
|
||||
GameState.set_flag("paywall_cleared", true)
|
||||
|
||||
func _on_urhahn_down() -> void:
|
||||
GameState.set_flag("urhahn_defeated", true)
|
||||
|
||||
func _on_rooster_touch(body: Node3D) -> void:
|
||||
if body.is_in_group("player") and not GameState.get_flag("urhahn_defeated", false):
|
||||
GameState.apply_shock(10.0)
|
||||
|
||||
func _on_ticket(_n: Node = null) -> void:
|
||||
Inventory.add_item("death_ticket", 1)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("item_death_ticket"))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,52 @@
|
|||
extends Node3D
|
||||
## Level stub: farm_path
|
||||
## TODO: Replace placeholders with final pixel art / audio.
|
||||
## Enger Weg + Gabelung. Rechts = Magier (Knockback / Planet destroy).
|
||||
|
||||
var _went_right: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "farm_path"
|
||||
GameState.heal_full()
|
||||
_went_right = bool(GameState.get_flag("went_right_first", false))
|
||||
var left := get_node_or_null("LeftExit")
|
||||
if left:
|
||||
left.set("use_goto_next", false)
|
||||
left.set("next_scene_id", "")
|
||||
if left.has_signal("body_entered") and not left.body_entered.is_connected(_on_left_body):
|
||||
left.body_entered.connect(_on_left_body)
|
||||
var right := get_node_or_null("RightZone")
|
||||
if right:
|
||||
right.set("use_goto_next", false)
|
||||
right.set("next_scene_id", "")
|
||||
if right.has_signal("body_entered") and not right.body_entered.is_connected(_on_right_body):
|
||||
right.body_entered.connect(_on_right_body)
|
||||
|
||||
func _on_left_body(body: Node3D) -> void:
|
||||
if not body.is_in_group("player"):
|
||||
return
|
||||
if _went_right or GameState.get_flag("went_right_first", false):
|
||||
GameState.set_flag("farmer_mood", "detour")
|
||||
else:
|
||||
GameState.set_flag("farmer_mood", "direct")
|
||||
GameState.set_flag("planet_destroyed", false)
|
||||
GameState.goto_scene("farm")
|
||||
|
||||
func _on_right_body(body: Node3D) -> void:
|
||||
if not body.is_in_group("player"):
|
||||
return
|
||||
_went_right = true
|
||||
GameState.set_flag("went_right_first", true)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
# Knockback toward left path
|
||||
if body is CharacterBody3D:
|
||||
body.global_position += Vector3(-4, 0, 0)
|
||||
if GameState.get_flag("called_black_mage", false):
|
||||
GameState.set_flag("mage_angered", true)
|
||||
GameState.set_flag("planet_destroyed", true)
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_mage_angry"), 4.0)
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
GameState.goto_scene("moon")
|
||||
else:
|
||||
GameState.set_flag("called_black_mage", true)
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_mage_push"))
|
||||
|
|
|
|||
|
|
@ -1,9 +1,91 @@
|
|||
extends Node3D
|
||||
## Hongkong / Glitschkönig final boss.
|
||||
## Glitschkönig: Reveal, Quirxels, Split-Phasen, Flucht des letzten Pixels.
|
||||
|
||||
enum Phase { FULL, SPLIT, FLEE, DONE }
|
||||
|
||||
var _phase: Phase = Phase.FULL
|
||||
var _revealed: bool = false
|
||||
var _splits_left: int = 2
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "final_boss"
|
||||
GameState.heal_full()
|
||||
_revealed = bool(GameState.get_flag("glitsch_revealed", false))
|
||||
if not _revealed:
|
||||
_do_reveal()
|
||||
var boss := get_node_or_null("Hongkong")
|
||||
if boss:
|
||||
boss.set("max_hp", 12)
|
||||
if boss.has_signal("hit"):
|
||||
boss.hit.connect(_on_boss_hit)
|
||||
if boss.has_signal("defeated"):
|
||||
boss.defeated.connect(_on_boss_defeated)
|
||||
var t := Timer.new()
|
||||
t.wait_time = 2.8
|
||||
t.autostart = true
|
||||
t.timeout.connect(_spawn_quirxel)
|
||||
add_child(t)
|
||||
|
||||
func _do_reveal() -> void:
|
||||
GameState.set_flag("glitsch_revealed", true)
|
||||
Inventory.add_item("banana", 1)
|
||||
Inventory.add_item("wartchen", 1)
|
||||
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"))
|
||||
ui.show_dialog(Locale.tr_key("dialog_reveal"), 5.5)
|
||||
|
||||
func _on_boss_hit(remaining: int) -> void:
|
||||
if _phase == Phase.FULL and remaining <= 6 and _splits_left > 0:
|
||||
_phase = Phase.SPLIT
|
||||
_splits_left -= 1
|
||||
_spawn_split_clone()
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog("Er teilt sich wie Minecraft-Schleim!", 2.5)
|
||||
elif remaining <= 2 and _phase != Phase.FLEE and _phase != Phase.DONE:
|
||||
_phase = Phase.FLEE
|
||||
var boss := get_node_or_null("Hongkong")
|
||||
if boss is Node3D:
|
||||
boss.global_position += Vector3(randf_range(-4, 4), 0, -3)
|
||||
|
||||
func _spawn_split_clone() -> void:
|
||||
var scene: PackedScene = load("res://scenes/prefabs/enemy.tscn")
|
||||
if scene == null:
|
||||
return
|
||||
var c: Node3D = scene.instantiate()
|
||||
c.name = "GlitschSplit"
|
||||
add_child(c)
|
||||
c.global_position = Vector3(randf_range(-3, 3), 0.1, -2)
|
||||
c.set("max_hp", 4)
|
||||
c.set("texture_path", "res://assets/sprites/placeholders/hongkong.png")
|
||||
var bill = c.get_node_or_null("BillboardSprite")
|
||||
if bill and bill.has_method("set_texture"):
|
||||
var tex: Texture2D = load("res://assets/sprites/placeholders/hongkong.png")
|
||||
if tex:
|
||||
bill.set_texture(tex)
|
||||
|
||||
func _spawn_quirxel() -> void:
|
||||
if _phase == Phase.DONE or GameState.get_flag("hongkong_defeated", false):
|
||||
return
|
||||
var scene: PackedScene = load("res://scenes/prefabs/hazard.tscn")
|
||||
if scene == null:
|
||||
return
|
||||
var h: Node3D = scene.instantiate()
|
||||
add_child(h)
|
||||
h.global_position = Vector3(randf_range(-6, 6), 0.1, randf_range(-6, 0))
|
||||
h.set("death_cause_key", "death_quirxel")
|
||||
await get_tree().create_timer(5.0).timeout
|
||||
if is_instance_valid(h):
|
||||
h.queue_free()
|
||||
|
||||
func _on_boss_defeated() -> void:
|
||||
_phase = Phase.DONE
|
||||
Achievements.unlock("beat_hongkong")
|
||||
GameState.set_flag("hongkong_defeated", true)
|
||||
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"), 3.0)
|
||||
await get_tree().create_timer(2.5).timeout
|
||||
ui.show_dialog(Locale.tr_key("dialog_bear_end"), 5.0)
|
||||
await get_tree().create_timer(3.5).timeout
|
||||
GameState.goto_scene("end_screen")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
extends Control
|
||||
## Intro: desk + console + explosion.
|
||||
## TODO: Real cutscene art and epic narrator VO.
|
||||
## Intro: Tisch, Konsole, Sprecher, Explosion.
|
||||
|
||||
@onready var label: Label = $Label
|
||||
|
||||
|
|
@ -8,6 +7,7 @@ var _step: int = 0
|
|||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "intro_cutscene"
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
AudioDirector.stop_music()
|
||||
_show_step()
|
||||
|
||||
|
|
@ -20,10 +20,10 @@ func _show_step() -> void:
|
|||
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"
|
||||
2:
|
||||
label.text = Locale.tr_key("dialog_intro_2")
|
||||
_:
|
||||
GameState.goto_scene("village")
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,53 @@
|
|||
extends Node3D
|
||||
## Level stub: massage_studio
|
||||
## TODO: Replace placeholders with final pixel art / audio.
|
||||
## Massage: Schlange massieren → Bear-Save; oder Secret-Milch.
|
||||
|
||||
@onready var choice_ui: CanvasLayer = $ChoiceUI
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "massage_studio"
|
||||
GameState.heal_full()
|
||||
choice_ui.visible = false
|
||||
var cow := get_node_or_null("CowMasseur")
|
||||
if cow and cow.has_signal("interacted"):
|
||||
cow.interacted.connect(_on_cow)
|
||||
if cow:
|
||||
cow.require_item_id = ""
|
||||
cow.achievement_id = ""
|
||||
cow.quest_flag = ""
|
||||
cow.consume_required = false
|
||||
cow.give_item_id = ""
|
||||
|
||||
func _on_cow(_npc: Node = null) -> void:
|
||||
choice_ui.visible = true
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
$ChoiceUI/Panel/Margin/VBox/Label.text = Locale.tr_key("dialog_cow")
|
||||
$ChoiceUI/Panel/Margin/VBox/MassageBtn.text = Locale.tr_key("choice_massage_now")
|
||||
$ChoiceUI/Panel/Margin/VBox/DrinkBtn.text = Locale.tr_key("choice_drink_first")
|
||||
|
||||
func _close_choices() -> void:
|
||||
choice_ui.visible = false
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
func _on_massage_pressed() -> void:
|
||||
_close_choices()
|
||||
if not Inventory.has_item("snake"):
|
||||
var ui0 := get_node_or_null("/root/UIRoot")
|
||||
if ui0 and ui0.has_method("show_dialog"):
|
||||
ui0.show_dialog(Locale.tr_key("dialog_villager_snake"))
|
||||
return
|
||||
Inventory.remove_item("snake", 1)
|
||||
GameState.set_flag("snake_massaged", true)
|
||||
Achievements.unlock("snake_massage")
|
||||
if not GameState.get_flag("bear_saved", false):
|
||||
Inventory.add_item("tooth", 1)
|
||||
GameState.set_flag("bear_saved", true)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_cow_massage") + "\n" + Locale.tr_key("dialog_bear_saved"), 5.0)
|
||||
|
||||
func _on_drink_pressed() -> void:
|
||||
_close_choices()
|
||||
Inventory.add_item("milk", 1)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_cow_drink"), 4.0)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,129 @@
|
|||
extends Node3D
|
||||
## Level stub: meadow
|
||||
## TODO: Replace placeholders with final pixel art / audio.
|
||||
## Wiese: Eisbär retten (Schlangen holen → Massage ODER hier abgeben), Glitzos.
|
||||
|
||||
var _glitzo_timer: float = 0.0
|
||||
var _bear_saved: bool = false
|
||||
var _attack_cd: float = 0.0
|
||||
var _first_glitzo_done: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "meadow"
|
||||
GameState.heal_full()
|
||||
_bear_saved = bool(GameState.get_flag("bear_saved", false))
|
||||
_first_glitzo_done = Achievements.is_unlocked("first_glitzo")
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
if _bear_saved:
|
||||
ui.show_dialog(Locale.tr_key("dialog_bear_saved"))
|
||||
else:
|
||||
ui.show_dialog(Locale.tr_key("dialog_bear_help"))
|
||||
var bear := get_node_or_null("Bear")
|
||||
if bear and bear.has_signal("interacted"):
|
||||
bear.interacted.connect(_on_bear)
|
||||
var snake := get_node_or_null("SnakePickup")
|
||||
if snake and snake.has_signal("interacted"):
|
||||
snake.interacted.connect(_on_snake_pickup)
|
||||
# Disable mower kill if snakes already bagged? Keep hazard until massage done.
|
||||
if GameState.get_flag("snake_massaged", false):
|
||||
_disable_mower()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_glitzo_timer += delta
|
||||
if _glitzo_timer >= 3.5:
|
||||
_glitzo_timer = 0.0
|
||||
_spawn_glitzo()
|
||||
_tick_glitzo_attacks(delta)
|
||||
_cull_glitzos_off_meadow()
|
||||
|
||||
func _tick_glitzo_attacks(delta: float) -> void:
|
||||
_attack_cd = max(0.0, _attack_cd - delta)
|
||||
if _attack_cd > 0.0:
|
||||
return
|
||||
var player := get_tree().get_first_node_in_group("player")
|
||||
if player == null:
|
||||
return
|
||||
for child in get_children():
|
||||
if not child.is_in_group("enemy"):
|
||||
continue
|
||||
if not str(child.get("texture_path")).ends_with("glitzo.png") and not child.name.begins_with("Glitzo"):
|
||||
continue
|
||||
if child is Node3D and player.global_position.distance_to(child.global_position) < 1.6:
|
||||
_attack_cd = 1.15
|
||||
GameState.register_glitzo_hit()
|
||||
return
|
||||
|
||||
func _cull_glitzos_off_meadow() -> void:
|
||||
## DESIGN: leave the meadow → they shed pixels and die.
|
||||
for child in get_children():
|
||||
if child is Node3D and (child.name.begins_with("Glitzo") or str(child.get("texture_path")).ends_with("glitzo.png")):
|
||||
var p: Vector3 = child.global_position
|
||||
if abs(p.x) > 18.0 or abs(p.z) > 18.0:
|
||||
child.queue_free()
|
||||
|
||||
func _spawn_glitzo() -> void:
|
||||
var scene: PackedScene = load("res://scenes/prefabs/enemy.tscn")
|
||||
if scene == null:
|
||||
return
|
||||
var g: Node3D = scene.instantiate()
|
||||
g.name = "GlitzoSpawn_%d" % Time.get_ticks_msec()
|
||||
g.set("max_hp", 4)
|
||||
g.set("texture_path", "res://assets/sprites/placeholders/glitzo.png")
|
||||
if not _first_glitzo_done:
|
||||
g.set("achievement_on_defeat", "first_glitzo")
|
||||
add_child(g)
|
||||
g.global_position = Vector3(randf_range(-8, 8), 0.1, randf_range(-6, 2))
|
||||
var bill: Node = g.get_node_or_null("BillboardSprite")
|
||||
if bill and bill.has_method("set_texture"):
|
||||
var tex: Texture2D = load("res://assets/sprites/placeholders/glitzo.png")
|
||||
if tex:
|
||||
bill.set_texture(tex)
|
||||
if g.has_signal("defeated"):
|
||||
g.defeated.connect(_on_glitzo_killed)
|
||||
|
||||
func _on_glitzo_killed() -> void:
|
||||
_first_glitzo_done = true
|
||||
|
||||
func _on_bear(_npc: Node = null) -> void:
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if _bear_saved:
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_bear_saved"))
|
||||
return
|
||||
# Need snakes rescued via inventory + preferably massaged; tooth if massaged or handing snakes in after pickup
|
||||
if Inventory.has_item("snake") and GameState.get_flag("snake_massaged", false):
|
||||
_save_bear()
|
||||
return
|
||||
if Inventory.has_item("snake"):
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_villager_snake"))
|
||||
return
|
||||
# First talk: point at snakes / give hint, auto-pickup one for convenience
|
||||
Inventory.add_item("snake", 1)
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_snake_pain") + "\n" + Locale.tr_key("dialog_bear_help"))
|
||||
|
||||
func _on_snake_pickup(_npc: Node = null) -> void:
|
||||
if not Inventory.has_item("snake"):
|
||||
Inventory.add_item("snake", 1)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_snake_pain"))
|
||||
|
||||
func _save_bear() -> void:
|
||||
_bear_saved = true
|
||||
GameState.set_flag("bear_saved", true)
|
||||
if not Inventory.has_item("tooth"):
|
||||
Inventory.add_item("tooth", 1)
|
||||
_disable_mower()
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_bear_saved"), 4.5)
|
||||
|
||||
func _disable_mower() -> void:
|
||||
var mower := get_node_or_null("MowerHazard")
|
||||
if mower:
|
||||
mower.queue_free()
|
||||
|
||||
func notify_snake_massaged() -> void:
|
||||
if not _bear_saved:
|
||||
_save_bear()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,91 @@
|
|||
extends Node3D
|
||||
## Moon cheese level.
|
||||
## Mond: Käsekatapulte, Schwert, Sonnenbrille, Fake-Bowser Hammer-Fight.
|
||||
|
||||
@onready var hammer_ui: CanvasLayer = $HammerUI
|
||||
var _correct_hammer: int = 1
|
||||
var _hits_needed: int = 3
|
||||
var _hits: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "moon"
|
||||
GameState.heal_full()
|
||||
Achievements.unlock("moon_cheese")
|
||||
hammer_ui.visible = false
|
||||
_spawn_cheese_sellers()
|
||||
var bowser := get_node_or_null("FakeBowser")
|
||||
if bowser and bowser.has_signal("interacted"):
|
||||
bowser.interacted.connect(_start_bowser_fight)
|
||||
var sword := get_node_or_null("SwordStone")
|
||||
if sword:
|
||||
sword.give_item_id = "moon_sword"
|
||||
sword.dialog_key = "dialog_cheese"
|
||||
var shades := get_node_or_null("MoonResearcher")
|
||||
if shades and shades.has_signal("interacted"):
|
||||
shades.interacted.connect(_on_shades)
|
||||
# Convert old CheeseSeller NPC if present into shooter
|
||||
var old := get_node_or_null("CheeseSeller")
|
||||
if old and not old.has_method("notify_cheese_destroyed"):
|
||||
old.queue_free()
|
||||
|
||||
func _spawn_cheese_sellers() -> void:
|
||||
var scene: PackedScene = load("res://scenes/prefabs/cheese_seller.tscn")
|
||||
if scene == null:
|
||||
return
|
||||
for i in range(4):
|
||||
var s: Node3D = scene.instantiate()
|
||||
add_child(s)
|
||||
s.global_position = Vector3(-6.0 + i * 4.0, 0.1, -2.0 - (i % 2) * 2.0)
|
||||
|
||||
func _on_shades(_n: Node = null) -> void:
|
||||
Shop.purchase("sunglasses")
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_sunglasses_free"))
|
||||
|
||||
func _start_bowser_fight(_n: Node = null) -> void:
|
||||
if GameState.get_flag("bowser_defeated", false):
|
||||
Inventory.add_item("wedding_egg", 1)
|
||||
return
|
||||
hammer_ui.visible = true
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
_hits = 0
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_bowser"))
|
||||
_refresh_hammers()
|
||||
|
||||
func _refresh_hammers() -> void:
|
||||
_correct_hammer = randi_range(1, 3)
|
||||
$HammerUI/Panel/Margin/VBox/Label.text = Locale.tr_key("dialog_bowser") + " [%d/%d]" % [_hits, _hits_needed]
|
||||
$HammerUI/Panel/Margin/VBox/H1.text = Locale.tr_key("choice_hammer_1")
|
||||
$HammerUI/Panel/Margin/VBox/H2.text = Locale.tr_key("choice_hammer_2")
|
||||
$HammerUI/Panel/Margin/VBox/H3.text = Locale.tr_key("choice_hammer_3")
|
||||
|
||||
func _pick(hammer: int) -> void:
|
||||
if hammer == _correct_hammer:
|
||||
_hits += 1
|
||||
AudioDirector.play_hit()
|
||||
if _hits >= _hits_needed:
|
||||
hammer_ui.visible = false
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
GameState.set_flag("bowser_defeated", true)
|
||||
Achievements.unlock("beat_bowser")
|
||||
Inventory.add_item("chicken", 1)
|
||||
Inventory.add_item("wedding_egg", 1)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog("Hochzeitsei! Im Inventar benutzen = Whiff zum Endboss.", 4.0)
|
||||
else:
|
||||
_refresh_hammers()
|
||||
else:
|
||||
hammer_ui.visible = false
|
||||
GameState.die("death_bowser")
|
||||
|
||||
func _on_h1() -> void:
|
||||
_pick(1)
|
||||
|
||||
func _on_h2() -> void:
|
||||
_pick(2)
|
||||
|
||||
func _on_h3() -> void:
|
||||
_pick(3)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
extends Node3D
|
||||
## Schuhmacher Reif boss under the table.
|
||||
## Schuhmacher Reif. Sieg → Tonne Tiramisu + Fett-Debuff.
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "shoe_boss"
|
||||
|
|
@ -7,3 +7,14 @@ func _ready() -> void:
|
|||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_reif"))
|
||||
var reif := get_node_or_null("Reif")
|
||||
if reif and reif.has_signal("defeated"):
|
||||
reif.defeated.connect(_on_reif_defeated)
|
||||
|
||||
func _on_reif_defeated() -> void:
|
||||
Inventory.add_item("tiramisu", 1000)
|
||||
GameState.apply_fat(600.0)
|
||||
var ui := get_node_or_null("/root/UIRoot")
|
||||
if ui and ui.has_method("show_dialog"):
|
||||
ui.show_dialog(Locale.tr_key("dialog_tiramisu"), 5.0)
|
||||
GameState.set_flag("reif_defeated", true)
|
||||
|
|
|
|||
|
|
@ -1,26 +1,35 @@
|
|||
extends Control
|
||||
## Title screen with placeholder rain + loud music.
|
||||
## TODO: Real title art (Mario/Sonic/Burger rain, disturbed human).
|
||||
## Title: pixel rain, Pay2Win, loud music, Burger-Cham, first-run language.
|
||||
|
||||
@onready var subtitle: Label = $Subtitle
|
||||
@onready var bubble: Label = $Bubble
|
||||
@onready var start_btn: Button = $StartButton
|
||||
@onready var rain: Node2D = $Rain
|
||||
@onready var lang_panel: PanelContainer = $LangPanel
|
||||
@onready var music_hint: Label = $MusicHint
|
||||
|
||||
var _rain_sprites: Array[Sprite2D] = []
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "title_screen"
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
AudioDirector.burger_cham_enabled = true
|
||||
AudioDirector.play_title_music()
|
||||
_refresh()
|
||||
Locale.locale_changed.connect(_refresh)
|
||||
_spawn_rain()
|
||||
lang_panel.visible = Settings.needs_language_picker()
|
||||
start_btn.visible = not lang_panel.visible
|
||||
|
||||
func _refresh() -> void:
|
||||
subtitle.text = Locale.tr_key("title_subtitle")
|
||||
subtitle.text = Locale.tr_key("title_pay2win")
|
||||
bubble.text = Locale.tr_key("title_bubble")
|
||||
start_btn.text = Locale.tr_key("menu_start")
|
||||
music_hint.text = Locale.tr_key("title_music_hint")
|
||||
$LangPanel/Margin/VBox/Title.text = Locale.tr_key("lang_pick_title")
|
||||
$LangPanel/Margin/VBox/BtnDE.text = Locale.tr_key("lang_de")
|
||||
$LangPanel/Margin/VBox/BtnEN.text = Locale.tr_key("lang_en")
|
||||
$LangPanel/Margin/VBox/BtnBY.text = Locale.tr_key("lang_by")
|
||||
|
||||
func _spawn_rain() -> void:
|
||||
var paths = [
|
||||
|
|
@ -28,7 +37,7 @@ func _spawn_rain() -> void:
|
|||
"res://assets/sprites/placeholders/sonic.png",
|
||||
"res://assets/sprites/placeholders/burger.png",
|
||||
]
|
||||
for i in range(24):
|
||||
for i in range(28):
|
||||
var s := Sprite2D.new()
|
||||
s.texture = load(paths[i % paths.size()])
|
||||
s.position = Vector2(randf() * 1280.0, randf() * -720.0)
|
||||
|
|
@ -39,11 +48,29 @@ func _spawn_rain() -> void:
|
|||
|
||||
func _process(delta: float) -> void:
|
||||
for s in _rain_sprites:
|
||||
s.position.y += (80.0 + s.get_index() * 3.0) * delta
|
||||
s.position.y += (90.0 + s.get_index() * 2.5) * delta
|
||||
if s.position.y > 760.0:
|
||||
s.position.y = -40.0
|
||||
s.position.x = randf() * 1280.0
|
||||
|
||||
func _pick_lang(lang: String) -> void:
|
||||
Settings.set_language(lang)
|
||||
lang_panel.visible = false
|
||||
start_btn.visible = true
|
||||
_refresh()
|
||||
|
||||
func _on_lang_de() -> void:
|
||||
_pick_lang("de")
|
||||
|
||||
func _on_lang_en() -> void:
|
||||
_pick_lang("en")
|
||||
|
||||
func _on_lang_by() -> void:
|
||||
_pick_lang("by")
|
||||
|
||||
func _on_start_pressed() -> void:
|
||||
if Settings.needs_language_picker():
|
||||
lang_panel.visible = true
|
||||
return
|
||||
AudioDirector.burger_cham_enabled = false
|
||||
GameState.start_new_game()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,34 @@
|
|||
extends Node3D
|
||||
## Level stub: village
|
||||
## TODO: Replace placeholders with final pixel art / audio.
|
||||
## Kaputtes AC-Dorf: fliehende NPCs, Tisch-Knopf → Reif.
|
||||
|
||||
var _flee_dirs: Dictionary = {}
|
||||
|
||||
func _ready() -> void:
|
||||
GameState.current_scene_id = "village"
|
||||
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_villager_run"))
|
||||
# Strip wrong early toothionaire / snake gift
|
||||
var dentist := get_node_or_null("Dentist")
|
||||
if dentist:
|
||||
dentist.give_item_id = ""
|
||||
dentist.achievement_id = ""
|
||||
dentist.quest_flag = ""
|
||||
dentist.dialog_key = "dialog_villager_snake"
|
||||
var fan := get_node_or_null("NPC_HongkongFan")
|
||||
if fan:
|
||||
fan.give_item_id = ""
|
||||
fan.dialog_key = "dialog_villager_run"
|
||||
for n in get_children():
|
||||
if n.is_in_group("interactable") and n.name != "TableButton" and n is Node3D:
|
||||
_flee_dirs[n] = Vector3(randf_range(-1, 1), 0, randf_range(-1, 1)).normalized()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
for n in _flee_dirs.keys():
|
||||
if not is_instance_valid(n):
|
||||
continue
|
||||
var node: Node3D = n
|
||||
node.global_position += _flee_dirs[n] * 1.8 * delta
|
||||
if node.global_position.length() > 22.0:
|
||||
_flee_dirs[n] = -_flee_dirs[n]
|
||||
|
|
|
|||
|
|
@ -1,38 +1,92 @@
|
|||
extends CharacterBody3D
|
||||
class_name PlayerController
|
||||
## Simple top-down-ish 3D movement for billboard levels.
|
||||
## Third-person controller: camera follows player, movement is camera-relative.
|
||||
## Billboard sprites face the camera (Mario-tree style).
|
||||
|
||||
@export var move_speed: float = 6.0
|
||||
@export var attack_range: float = 1.8
|
||||
@export var attack_damage: int = 1
|
||||
@export var mouse_sensitivity: float = 0.0025
|
||||
@export var min_pitch_deg: float = -35.0
|
||||
@export var max_pitch_deg: float = 55.0
|
||||
@export var spring_length: float = 5.5
|
||||
@export var camera_height: float = 1.4
|
||||
|
||||
signal interacted
|
||||
signal attacked
|
||||
|
||||
@onready var _billboard: BillboardSprite = $BillboardSprite
|
||||
@onready var _yaw_pivot: Node3D = $CameraYaw
|
||||
@onready var _spring: SpringArm3D = $CameraYaw/SpringArm3D
|
||||
@onready var _camera: Camera3D = $CameraYaw/SpringArm3D/Camera3D
|
||||
|
||||
var _locked: bool = false
|
||||
var _pitch: float = deg_to_rad(-18.0)
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("player")
|
||||
if _spring:
|
||||
_spring.spring_length = spring_length
|
||||
_spring.collision_mask = 1
|
||||
_spring.position = Vector3(0.0, camera_height, 0.0)
|
||||
if _camera:
|
||||
_camera.current = true
|
||||
_camera.fov = 70.0
|
||||
_apply_pitch()
|
||||
_set_mouse_captured(true)
|
||||
if _billboard and _billboard.texture == null:
|
||||
var tex: Texture2D = load("res://assets/sprites/placeholders/player.png")
|
||||
if tex:
|
||||
_billboard.set_texture(tex)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_set_mouse_captured(false)
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if _locked or get_tree().paused:
|
||||
return
|
||||
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
var motion := event as InputEventMouseMotion
|
||||
_yaw_pivot.rotate_y(-motion.relative.x * mouse_sensitivity)
|
||||
_pitch = clampf(
|
||||
_pitch - motion.relative.y * mouse_sensitivity,
|
||||
deg_to_rad(min_pitch_deg),
|
||||
deg_to_rad(max_pitch_deg)
|
||||
)
|
||||
_apply_pitch()
|
||||
elif event is InputEventMouseButton and event.pressed:
|
||||
_set_mouse_captured(true)
|
||||
|
||||
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)
|
||||
var basis_yaw := _yaw_pivot.global_transform.basis
|
||||
var forward := -basis_yaw.z
|
||||
forward.y = 0.0
|
||||
forward = forward.normalized()
|
||||
var right := basis_yaw.x
|
||||
right.y = 0.0
|
||||
right = right.normalized()
|
||||
var dir := (right * input.x) + (forward * -input.y)
|
||||
if dir.length() > 1.0:
|
||||
dir = dir.normalized()
|
||||
velocity = dir * move_speed
|
||||
var speed := move_speed
|
||||
if GameState.is_fat():
|
||||
speed *= 0.15
|
||||
elif GameState.is_glitched_slow():
|
||||
speed *= 0.55
|
||||
if GameState.is_shocked():
|
||||
velocity = Vector3.ZERO
|
||||
return
|
||||
velocity = dir * speed
|
||||
move_and_slide()
|
||||
|
||||
if Input.is_action_just_pressed("interact"):
|
||||
_try_interact()
|
||||
if Input.is_action_just_pressed("attack"):
|
||||
|
|
@ -40,6 +94,28 @@ func _physics_process(_delta: float) -> void:
|
|||
|
||||
func lock_input(locked: bool) -> void:
|
||||
_locked = locked
|
||||
if locked:
|
||||
_set_mouse_captured(false)
|
||||
elif not get_tree().paused:
|
||||
_set_mouse_captured(true)
|
||||
|
||||
func set_gameplay_focused(focused: bool) -> void:
|
||||
## Called by pause UI: release mouse in menus, recapture in gameplay.
|
||||
if focused and not _locked:
|
||||
_set_mouse_captured(true)
|
||||
else:
|
||||
_set_mouse_captured(false)
|
||||
|
||||
func _apply_pitch() -> void:
|
||||
if _spring == null:
|
||||
return
|
||||
_spring.rotation.x = _pitch
|
||||
|
||||
func _set_mouse_captured(captured: bool) -> void:
|
||||
if captured:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
else:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
func _try_interact() -> void:
|
||||
interacted.emit()
|
||||
|
|
@ -52,7 +128,14 @@ func _try_interact() -> void:
|
|||
func _try_attack() -> void:
|
||||
attacked.emit()
|
||||
AudioDirector.play_hit()
|
||||
var dmg := attack_damage
|
||||
if Inventory.has_item("tooth"):
|
||||
dmg = max(dmg, 2)
|
||||
if Inventory.has_item("moon_sword"):
|
||||
dmg = max(dmg, 99)
|
||||
if Inventory.has_item("wartchen"):
|
||||
dmg = max(dmg, 3)
|
||||
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)
|
||||
body.take_hit(dmg)
|
||||
|
|
|
|||
|
|
@ -96,6 +96,9 @@ def player_scene() -> str:
|
|||
radius = 0.35
|
||||
height = 1.0
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_spring"]
|
||||
radius = 0.2
|
||||
|
||||
[node name="Player" type="CharacterBody3D"]
|
||||
collision_layer = 2
|
||||
collision_mask = 1
|
||||
|
|
@ -106,7 +109,20 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
|
|||
shape = SubResource("CapsuleShape3D_1")
|
||||
|
||||
[node name="BillboardSprite" parent="." instance=ExtResource("2")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||
|
||||
[node name="CameraYaw" type="Node3D" parent="."]
|
||||
|
||||
[node name="SpringArm3D" type="SpringArm3D" parent="CameraYaw"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4, 0)
|
||||
spring_length = 5.5
|
||||
collision_mask = 1
|
||||
margin = 0.2
|
||||
shape = SubResource("SphereShape3D_spring")
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="CameraYaw/SpringArm3D"]
|
||||
current = true
|
||||
fov = 70.0
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -120,7 +136,7 @@ def entity_scene(name: str, script: str, extras: str = "") -> str:
|
|||
script = ExtResource("1")
|
||||
{extras}
|
||||
[node name="BillboardSprite" parent="." instance=ExtResource("2")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -373,11 +389,6 @@ script = ExtResource("1")
|
|||
transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 8, 0)
|
||||
shadow_enabled = false
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 12, 12)
|
||||
current = true
|
||||
fov = 50.0
|
||||
|
||||
[node name="Ground" type="StaticBody3D" parent="."]
|
||||
collision_layer = 1
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@ extends CanvasLayer
|
|||
@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
|
||||
@onready var fat_stim: ColorRect = $FatStim
|
||||
@onready var fat_label: Label = $FatStim/FatLabel
|
||||
|
||||
var _dialog_timer: float = 0.0
|
||||
var _fat_blink: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
|
|
@ -35,6 +38,7 @@ func _ready() -> void:
|
|||
Inventory.inventory_changed.connect(_refresh_inventory)
|
||||
Shop.points_changed.connect(_refresh_points)
|
||||
GameState.hp_changed.connect(_on_hp)
|
||||
GameState.status_changed.connect(func(): _on_hp(GameState.hp, GameState.max_hp))
|
||||
_on_hp(GameState.hp, GameState.max_hp)
|
||||
_refresh_inventory()
|
||||
_refresh_points(Shop.dressler_points)
|
||||
|
|
@ -44,6 +48,17 @@ func _process(delta: float) -> void:
|
|||
_dialog_timer -= delta
|
||||
if _dialog_timer <= 0.0:
|
||||
dialog_panel.visible = false
|
||||
_update_fat_stim(delta)
|
||||
|
||||
func _update_fat_stim(delta: float) -> void:
|
||||
if fat_stim == null:
|
||||
return
|
||||
var fat := GameState.is_fat()
|
||||
fat_stim.visible = fat and GameState.current_scene_id not in ["title_screen", "death_screen", "intro_cutscene", "catscene"]
|
||||
if fat:
|
||||
_fat_blink += delta
|
||||
fat_label.text = Locale.tr_key("dialog_tiramisu")
|
||||
fat_stim.color = Color(0.05, 0.05, 0.08, 0.35 + 0.25 * abs(sin(_fat_blink * 3.0)))
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("pause_menu"):
|
||||
|
|
@ -51,14 +66,20 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
get_viewport().set_input_as_handled()
|
||||
|
||||
func toggle_pause() -> void:
|
||||
if GameState.current_scene_id in ["title_screen", "death_screen", "intro_cutscene"]:
|
||||
if GameState.current_scene_id in ["title_screen", "death_screen", "intro_cutscene", "catscene"]:
|
||||
return
|
||||
pause_panel.visible = not pause_panel.visible
|
||||
get_tree().paused = pause_panel.visible
|
||||
_sync_player_focus(not pause_panel.visible)
|
||||
if pause_panel.visible:
|
||||
_refresh_texts()
|
||||
_refresh_inventory()
|
||||
|
||||
func _sync_player_focus(focused: bool) -> void:
|
||||
for node in get_tree().get_nodes_in_group("player"):
|
||||
if node.has_method("set_gameplay_focused"):
|
||||
node.set_gameplay_focused(focused)
|
||||
|
||||
func show_dialog(text: String, duration: float = 3.5) -> void:
|
||||
dialog_label.text = text
|
||||
dialog_panel.visible = true
|
||||
|
|
@ -84,8 +105,8 @@ func _refresh_texts() -> void:
|
|||
$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/BuyMilkButton.text = Locale.tr_key("shop_continue_joke")
|
||||
$PausePanel/Margin/VBox/BuyTiramisuButton.text = Locale.tr_key("shop_next_level")
|
||||
$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")
|
||||
|
|
@ -105,7 +126,14 @@ func _refresh_inventory() -> void:
|
|||
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]
|
||||
var extra := ""
|
||||
if GameState.is_fat():
|
||||
extra += " | " + Locale.tr_key("status_fat", [str(int(GameState.fat_time_left))])
|
||||
if GameState.is_shocked():
|
||||
extra += " | " + Locale.tr_key("status_shock")
|
||||
if GameState.is_glitched_slow():
|
||||
extra += " | " + Locale.tr_key("status_glitch")
|
||||
hp_label.text = "HP %d/%d%s" % [hp, max_hp, extra]
|
||||
|
||||
func _on_achievement(_id: String, title: String) -> void:
|
||||
toast_label.text = Locale.tr_key("toast_achievement", [title])
|
||||
|
|
@ -122,6 +150,7 @@ func _on_burger_cham() -> void:
|
|||
func _on_resume_pressed() -> void:
|
||||
pause_panel.visible = false
|
||||
get_tree().paused = false
|
||||
_sync_player_focus(true)
|
||||
|
||||
func _on_buy_point() -> void:
|
||||
Shop.purchase("buy_point")
|
||||
|
|
@ -133,10 +162,10 @@ func _on_buy_ticket() -> void:
|
|||
Shop.purchase("death_ticket")
|
||||
|
||||
func _on_buy_milk() -> void:
|
||||
Shop.purchase("milk")
|
||||
Shop.purchase("continue_joke")
|
||||
|
||||
func _on_buy_tiramisu() -> void:
|
||||
Shop.purchase("tiramisu")
|
||||
Shop.purchase("next_level")
|
||||
|
||||
func _on_sfx_changed(value: float) -> void:
|
||||
Settings.set_sfx_volume(value)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue