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:
copilot 2026-09-14 18:36:02 +02:00
commit 938cc4bc82
114 changed files with 4819 additions and 0 deletions

View 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)

View 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

View 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)

View 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)