feat(poc): implement Sprint 1 proof of concept
- project.godot with autoload configuration - Reception room with placeholder visuals - Draggable Character with DragDropComponent - Interactive flower object with bounce animation - GameState, SaveManager, AudioManager, InputManager autoloads - HUD with back button and music toggle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,6 @@
|
|||||||
; Engine configuration file.
|
; Engine configuration file.
|
||||||
; It's best edited using the editor UI and not directly,
|
; It's best edited using the editor UI and not directly,
|
||||||
; since the parameters that go here are not all obvious.
|
; but this is the source of truth for engine configuration.
|
||||||
;
|
|
||||||
; Format:
|
|
||||||
; [section] ; section goes here
|
|
||||||
; param=value ; parameter goes here
|
|
||||||
|
|
||||||
config_version=5
|
config_version=5
|
||||||
|
|
||||||
@@ -13,7 +9,16 @@ config_version=5
|
|||||||
config/name="Cozypaw Hospital"
|
config/name="Cozypaw Hospital"
|
||||||
config/description="Werbefrei. Offline. Für Kinder."
|
config/description="Werbefrei. Offline. Für Kinder."
|
||||||
config/version="0.1.0"
|
config/version="0.1.0"
|
||||||
|
run/main_scene="res://scenes/main/Main.tscn"
|
||||||
config/features=PackedStringArray("4.6", "Mobile")
|
config/features=PackedStringArray("4.6", "Mobile")
|
||||||
|
boot_splash/show_image=false
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
GameState="*res://scripts/autoload/GameState.gd"
|
||||||
|
SaveManager="*res://scripts/autoload/SaveManager.gd"
|
||||||
|
AudioManager="*res://scripts/autoload/AudioManager.gd"
|
||||||
|
InputManager="*res://scripts/autoload/InputManager.gd"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
|
|||||||
46
scenes/characters/Character.tscn
Normal file
46
scenes/characters/Character.tscn
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://cozypaw_char"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/characters/character.gd" id="1_char"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/systems/drag_drop_component.gd" id="2_drag"]
|
||||||
|
|
||||||
|
[node name="Character" type="Node2D"]
|
||||||
|
script = ExtResource("1_char")
|
||||||
|
character_id = "bunny_01"
|
||||||
|
display_name = "Bunny"
|
||||||
|
|
||||||
|
[node name="Visual" type="ColorRect" parent="."]
|
||||||
|
color = Color(0.95, 0.85, 0.9, 1)
|
||||||
|
size = Vector2(64, 80)
|
||||||
|
position = Vector2(-32, -80)
|
||||||
|
|
||||||
|
[node name="Eyes" type="Node2D" parent="Visual"]
|
||||||
|
|
||||||
|
[node name="EyeLeft" type="ColorRect" parent="Visual/Eyes"]
|
||||||
|
color = Color(0.1, 0.1, 0.1, 1)
|
||||||
|
size = Vector2(8, 8)
|
||||||
|
position = Vector2(14, 20)
|
||||||
|
|
||||||
|
[node name="EyeRight" type="ColorRect" parent="Visual/Eyes"]
|
||||||
|
color = Color(0.1, 0.1, 0.1, 1)
|
||||||
|
size = Vector2(8, 8)
|
||||||
|
position = Vector2(42, 20)
|
||||||
|
|
||||||
|
[node name="Ears" type="Node2D" parent="."]
|
||||||
|
|
||||||
|
[node name="EarLeft" type="ColorRect" parent="Ears"]
|
||||||
|
color = Color(0.95, 0.85, 0.9, 1)
|
||||||
|
size = Vector2(18, 36)
|
||||||
|
position = Vector2(-22, -114)
|
||||||
|
|
||||||
|
[node name="EarRight" type="ColorRect" parent="Ears"]
|
||||||
|
color = Color(0.95, 0.85, 0.9, 1)
|
||||||
|
size = Vector2(18, 36)
|
||||||
|
position = Vector2(4, -114)
|
||||||
|
|
||||||
|
[node name="DragDropComponent" type="Node" parent="."]
|
||||||
|
script = ExtResource("2_drag")
|
||||||
|
|
||||||
|
[node name="CollisionArea" type="Area2D" parent="."]
|
||||||
|
input_pickable = true
|
||||||
|
|
||||||
|
[node name="CollisionShape" type="CollisionShape2D" parent="CollisionArea"]
|
||||||
23
scenes/main/Main.tscn
Normal file
23
scenes/main/Main.tscn
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[gd_scene load_steps=4 format=3 uid="uid://cozypaw_main"]
|
||||||
|
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/rooms/floor0/Reception.tscn" id="1_reception"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/characters/Character.tscn" id="2_char"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/ui/HUD.tscn" id="3_hud"]
|
||||||
|
|
||||||
|
[node name="Main" type="Node2D"]
|
||||||
|
|
||||||
|
[node name="Hospital" type="Node2D" parent="."]
|
||||||
|
|
||||||
|
[node name="Floor0" type="Node2D" parent="Hospital"]
|
||||||
|
|
||||||
|
[node name="Reception" parent="Hospital/Floor0" instance=ExtResource("1_reception")]
|
||||||
|
position = Vector2(0, 0)
|
||||||
|
|
||||||
|
[node name="Characters" type="Node2D" parent="."]
|
||||||
|
|
||||||
|
[node name="Bunny1" parent="Characters" instance=ExtResource("2_char")]
|
||||||
|
position = Vector2(300, 400)
|
||||||
|
|
||||||
|
[node name="UI" type="CanvasLayer" parent="."]
|
||||||
|
|
||||||
|
[node name="HUD" parent="UI" instance=ExtResource("3_hud")]
|
||||||
28
scenes/objects/InteractiveObject.tscn
Normal file
28
scenes/objects/InteractiveObject.tscn
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://cozypaw_iobj"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/objects/interactive_object.gd" id="1_iobj"]
|
||||||
|
[ext_resource type="Script" path="res://scripts/systems/drag_drop_component.gd" id="2_drag"]
|
||||||
|
|
||||||
|
[node name="InteractiveObject" type="Node2D"]
|
||||||
|
script = ExtResource("1_iobj")
|
||||||
|
object_id = "flower_01"
|
||||||
|
|
||||||
|
[node name="Visual" type="Node2D" parent="."]
|
||||||
|
|
||||||
|
[node name="Stem" type="ColorRect" parent="Visual"]
|
||||||
|
color = Color(0.2, 0.7, 0.2, 1)
|
||||||
|
size = Vector2(8, 40)
|
||||||
|
position = Vector2(-4, -40)
|
||||||
|
|
||||||
|
[node name="Bloom" type="ColorRect" parent="Visual"]
|
||||||
|
color = Color(1.0, 0.4, 0.6, 1)
|
||||||
|
size = Vector2(32, 32)
|
||||||
|
position = Vector2(-16, -72)
|
||||||
|
|
||||||
|
[node name="DragDropComponent" type="Node" parent="."]
|
||||||
|
script = ExtResource("2_drag")
|
||||||
|
|
||||||
|
[node name="CollisionArea" type="Area2D" parent="."]
|
||||||
|
input_pickable = true
|
||||||
|
|
||||||
|
[node name="CollisionShape" type="CollisionShape2D" parent="CollisionArea"]
|
||||||
41
scenes/rooms/floor0/Reception.tscn
Normal file
41
scenes/rooms/floor0/Reception.tscn
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://cozypaw_reception"]
|
||||||
|
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"]
|
||||||
|
|
||||||
|
[node name="Reception" type="Node2D"]
|
||||||
|
|
||||||
|
[node name="Background" type="ColorRect" parent="."]
|
||||||
|
color = Color(0.78, 0.94, 0.80, 1)
|
||||||
|
size = Vector2(1280, 720)
|
||||||
|
position = Vector2(0, 0)
|
||||||
|
|
||||||
|
[node name="Counter" type="ColorRect" parent="."]
|
||||||
|
color = Color(0.55, 0.35, 0.18, 1)
|
||||||
|
size = Vector2(300, 80)
|
||||||
|
position = Vector2(490, 610)
|
||||||
|
|
||||||
|
[node name="CounterTop" type="ColorRect" parent="."]
|
||||||
|
color = Color(0.70, 0.50, 0.28, 1)
|
||||||
|
size = Vector2(300, 12)
|
||||||
|
position = Vector2(490, 598)
|
||||||
|
|
||||||
|
[node name="Floor" type="ColorRect" parent="."]
|
||||||
|
color = Color(0.88, 0.80, 0.68, 1)
|
||||||
|
size = Vector2(1280, 100)
|
||||||
|
position = Vector2(0, 620)
|
||||||
|
|
||||||
|
[node name="WallLeft" type="ColorRect" parent="."]
|
||||||
|
color = Color(0.92, 0.88, 0.82, 1)
|
||||||
|
size = Vector2(40, 620)
|
||||||
|
position = Vector2(0, 0)
|
||||||
|
|
||||||
|
[node name="WallRight" type="ColorRect" parent="."]
|
||||||
|
color = Color(0.92, 0.88, 0.82, 1)
|
||||||
|
size = Vector2(40, 620)
|
||||||
|
position = Vector2(1240, 0)
|
||||||
|
|
||||||
|
[node name="CharacterSpawn" type="Marker2D" parent="."]
|
||||||
|
position = Vector2(300, 520)
|
||||||
|
|
||||||
|
[node name="Flower" parent="." instance=ExtResource("1_iobj")]
|
||||||
|
position = Vector2(200, 560)
|
||||||
35
scenes/ui/HUD.tscn
Normal file
35
scenes/ui/HUD.tscn
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://cozypaw_hud"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/systems/hud.gd" id="1_hud"]
|
||||||
|
|
||||||
|
[node name="HUD" type="CanvasLayer"]
|
||||||
|
script = ExtResource("1_hud")
|
||||||
|
|
||||||
|
[node name="BackButton" type="Button" parent="."]
|
||||||
|
anchors_preset = 0
|
||||||
|
anchor_left = 0.0
|
||||||
|
anchor_top = 0.0
|
||||||
|
anchor_right = 0.0
|
||||||
|
anchor_bottom = 0.0
|
||||||
|
offset_left = 16.0
|
||||||
|
offset_top = 16.0
|
||||||
|
offset_right = 80.0
|
||||||
|
offset_bottom = 80.0
|
||||||
|
text = "←"
|
||||||
|
flat = false
|
||||||
|
|
||||||
|
[node name="MusicToggle" type="Button" parent="."]
|
||||||
|
anchors_preset = 1
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_top = 0.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 0.0
|
||||||
|
offset_left = -80.0
|
||||||
|
offset_top = 16.0
|
||||||
|
offset_right = -16.0
|
||||||
|
offset_bottom = 80.0
|
||||||
|
text = "♪"
|
||||||
|
flat = false
|
||||||
|
|
||||||
|
[connection signal="pressed" from="BackButton" to="." method="_on_back_button_pressed"]
|
||||||
|
[connection signal="pressed" from="MusicToggle" to="." method="_on_music_toggle_pressed"]
|
||||||
63
scripts/autoload/AudioManager.gd
Normal file
63
scripts/autoload/AudioManager.gd
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
## AudioManager — music playback with cross-fade, SFX playback, volume control.
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const DEFAULT_MUSIC_VOLUME: float = 0.6
|
||||||
|
const CROSSFADE_DURATION: float = 1.0
|
||||||
|
|
||||||
|
var _music_player_a: AudioStreamPlayer
|
||||||
|
var _music_player_b: AudioStreamPlayer
|
||||||
|
var _active_player: AudioStreamPlayer
|
||||||
|
var _sfx_player: AudioStreamPlayer
|
||||||
|
var _music_volume: float = DEFAULT_MUSIC_VOLUME
|
||||||
|
var _sfx_volume: float = 1.0
|
||||||
|
var _is_fading: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_music_player_a = AudioStreamPlayer.new()
|
||||||
|
_music_player_b = AudioStreamPlayer.new()
|
||||||
|
_sfx_player = AudioStreamPlayer.new()
|
||||||
|
add_child(_music_player_a)
|
||||||
|
add_child(_music_player_b)
|
||||||
|
add_child(_sfx_player)
|
||||||
|
_active_player = _music_player_a
|
||||||
|
_apply_music_volume()
|
||||||
|
|
||||||
|
|
||||||
|
func play_music(stream: AudioStream) -> void:
|
||||||
|
if _active_player.stream == stream and _active_player.playing:
|
||||||
|
return
|
||||||
|
var next_player: AudioStreamPlayer = _music_player_b if _active_player == _music_player_a else _music_player_a
|
||||||
|
next_player.stream = stream
|
||||||
|
next_player.volume_db = linear_to_db(0.0)
|
||||||
|
next_player.play()
|
||||||
|
var tween: Tween = create_tween()
|
||||||
|
tween.set_parallel(true)
|
||||||
|
tween.tween_property(_active_player, "volume_db", linear_to_db(0.0), CROSSFADE_DURATION)
|
||||||
|
tween.tween_property(next_player, "volume_db", linear_to_db(_music_volume), CROSSFADE_DURATION)
|
||||||
|
var prev_player: AudioStreamPlayer = _active_player
|
||||||
|
tween.tween_callback(prev_player.stop).set_delay(CROSSFADE_DURATION)
|
||||||
|
_active_player = next_player
|
||||||
|
|
||||||
|
|
||||||
|
func play_sfx(stream: AudioStream) -> void:
|
||||||
|
_sfx_player.stream = stream
|
||||||
|
_sfx_player.volume_db = linear_to_db(_sfx_volume)
|
||||||
|
_sfx_player.play()
|
||||||
|
|
||||||
|
|
||||||
|
func set_music_volume(value: float) -> void:
|
||||||
|
_music_volume = clampf(value, 0.0, 1.0)
|
||||||
|
_apply_music_volume()
|
||||||
|
|
||||||
|
|
||||||
|
func set_sfx_volume(value: float) -> void:
|
||||||
|
_sfx_volume = clampf(value, 0.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
func get_music_volume() -> float:
|
||||||
|
return _music_volume
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_music_volume() -> void:
|
||||||
|
_active_player.volume_db = linear_to_db(_music_volume)
|
||||||
45
scripts/autoload/GameState.gd
Normal file
45
scripts/autoload/GameState.gd
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
## GameState — global game state: character positions, object states, current room.
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal state_changed
|
||||||
|
signal character_moved(character_id: String, position: Vector2)
|
||||||
|
|
||||||
|
var _character_positions: Dictionary = {}
|
||||||
|
var _object_states: Dictionary = {}
|
||||||
|
var current_room: String = "reception"
|
||||||
|
|
||||||
|
|
||||||
|
func get_character_position(id: String) -> Vector2:
|
||||||
|
return _character_positions.get(id, Vector2.ZERO)
|
||||||
|
|
||||||
|
|
||||||
|
func set_character_position(id: String, pos: Vector2) -> void:
|
||||||
|
_character_positions[id] = pos
|
||||||
|
character_moved.emit(id, pos)
|
||||||
|
state_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func get_object_state(id: String) -> String:
|
||||||
|
return _object_states.get(id, "idle")
|
||||||
|
|
||||||
|
|
||||||
|
func set_object_state(id: String, state: String) -> void:
|
||||||
|
_object_states[id] = state
|
||||||
|
state_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func get_save_data() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"character_positions": _character_positions,
|
||||||
|
"object_states": _object_states,
|
||||||
|
"current_room": current_room,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func apply_save_data(data: Dictionary) -> void:
|
||||||
|
if data.has("character_positions"):
|
||||||
|
_character_positions = data["character_positions"]
|
||||||
|
if data.has("object_states"):
|
||||||
|
_object_states = data["object_states"]
|
||||||
|
if data.has("current_room"):
|
||||||
|
current_room = data["current_room"]
|
||||||
30
scripts/autoload/InputManager.gd
Normal file
30
scripts/autoload/InputManager.gd
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
## InputManager — abstracts touch and mouse input into unified drag signals.
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal drag_started(position: Vector2)
|
||||||
|
signal drag_moved(position: Vector2)
|
||||||
|
signal drag_ended(position: Vector2)
|
||||||
|
|
||||||
|
var _is_dragging: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventScreenTouch:
|
||||||
|
if event.pressed:
|
||||||
|
_is_dragging = true
|
||||||
|
drag_started.emit(event.position)
|
||||||
|
else:
|
||||||
|
_is_dragging = false
|
||||||
|
drag_ended.emit(event.position)
|
||||||
|
elif event is InputEventScreenDrag:
|
||||||
|
if _is_dragging:
|
||||||
|
drag_moved.emit(event.position)
|
||||||
|
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
if event.pressed:
|
||||||
|
_is_dragging = true
|
||||||
|
drag_started.emit(event.position)
|
||||||
|
else:
|
||||||
|
_is_dragging = false
|
||||||
|
drag_ended.emit(event.position)
|
||||||
|
elif event is InputEventMouseMotion and _is_dragging:
|
||||||
|
drag_moved.emit(event.position)
|
||||||
40
scripts/autoload/SaveManager.gd
Normal file
40
scripts/autoload/SaveManager.gd
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
## SaveManager — persists game state as JSON to user://savegame.json, auto-saves on state_changed.
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const SAVE_PATH: String = "user://savegame.json"
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
GameState.state_changed.connect(_on_state_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func save_game() -> void:
|
||||||
|
var data: Dictionary = GameState.get_save_data()
|
||||||
|
var file: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
||||||
|
if file == null:
|
||||||
|
return
|
||||||
|
file.store_string(JSON.stringify(data))
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
|
||||||
|
func load_game() -> void:
|
||||||
|
if not FileAccess.file_exists(SAVE_PATH):
|
||||||
|
return
|
||||||
|
var file: FileAccess = FileAccess.open(SAVE_PATH, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
return
|
||||||
|
var raw: String = file.get_as_text()
|
||||||
|
file.close()
|
||||||
|
var parsed = JSON.parse_string(raw)
|
||||||
|
if parsed is Dictionary:
|
||||||
|
GameState.apply_save_data(parsed)
|
||||||
|
|
||||||
|
|
||||||
|
func reset_game() -> void:
|
||||||
|
if FileAccess.file_exists(SAVE_PATH):
|
||||||
|
DirAccess.remove_absolute(SAVE_PATH)
|
||||||
|
GameState.apply_save_data({})
|
||||||
|
|
||||||
|
|
||||||
|
func _on_state_changed() -> void:
|
||||||
|
save_game()
|
||||||
28
scripts/characters/character.gd
Normal file
28
scripts/characters/character.gd
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
## Character — base class for all playable figures (bunny, cat, etc.).
|
||||||
|
class_name Character extends Node2D
|
||||||
|
|
||||||
|
signal character_picked_up(character: Character)
|
||||||
|
signal character_placed(character: Character, position: Vector2)
|
||||||
|
|
||||||
|
@export var character_id: String = ""
|
||||||
|
@export var display_name: String = ""
|
||||||
|
|
||||||
|
var _is_held: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var drag: DragDropComponent = get_node_or_null("DragDropComponent") as DragDropComponent
|
||||||
|
if drag != null:
|
||||||
|
drag.drag_picked_up.connect(_on_drag_picked_up)
|
||||||
|
drag.drag_released.connect(_on_drag_released)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_drag_picked_up(_pos: Vector2) -> void:
|
||||||
|
_is_held = true
|
||||||
|
character_picked_up.emit(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_drag_released(pos: Vector2) -> void:
|
||||||
|
_is_held = false
|
||||||
|
GameState.set_character_position(character_id, global_position)
|
||||||
|
character_placed.emit(self, pos)
|
||||||
57
scripts/objects/interactive_object.gd
Normal file
57
scripts/objects/interactive_object.gd
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
## InteractiveObject — base class for all interactive room objects (flowers, equipment, etc.).
|
||||||
|
class_name InteractiveObject extends Node2D
|
||||||
|
|
||||||
|
signal object_interacted(object: InteractiveObject)
|
||||||
|
|
||||||
|
enum State { IDLE, ACTIVE, RETURNING }
|
||||||
|
|
||||||
|
@export var object_id: String = ""
|
||||||
|
|
||||||
|
var _current_state: State = State.IDLE
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var drag: DragDropComponent = get_node_or_null("DragDropComponent") as DragDropComponent
|
||||||
|
if drag != null:
|
||||||
|
drag.drag_picked_up.connect(_on_drag_picked_up)
|
||||||
|
drag.drag_released.connect(_on_drag_released)
|
||||||
|
var area: Area2D = get_node_or_null("CollisionArea") as Area2D
|
||||||
|
if area != null:
|
||||||
|
area.input_event.connect(_on_area_input_event)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_drag_picked_up(_pos: Vector2) -> void:
|
||||||
|
_set_state(State.ACTIVE)
|
||||||
|
object_interacted.emit(self)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_drag_released(_pos: Vector2) -> void:
|
||||||
|
_set_state(State.RETURNING)
|
||||||
|
GameState.set_object_state(object_id, "idle")
|
||||||
|
_play_bounce_animation()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_area_input_event(_viewport: Node, event: InputEvent, _shape_idx: int) -> void:
|
||||||
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
_trigger_interaction()
|
||||||
|
elif event is InputEventScreenTouch and event.pressed:
|
||||||
|
_trigger_interaction()
|
||||||
|
|
||||||
|
|
||||||
|
func _trigger_interaction() -> void:
|
||||||
|
_set_state(State.ACTIVE)
|
||||||
|
object_interacted.emit(self)
|
||||||
|
GameState.set_object_state(object_id, "active")
|
||||||
|
_play_bounce_animation()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_state(new_state: State) -> void:
|
||||||
|
_current_state = new_state
|
||||||
|
|
||||||
|
|
||||||
|
func _play_bounce_animation() -> void:
|
||||||
|
var tween: Tween = create_tween()
|
||||||
|
tween.tween_property(self, "scale", Vector2(1.2, 1.2), 0.1)
|
||||||
|
tween.tween_property(self, "scale", Vector2(0.9, 0.9), 0.1)
|
||||||
|
tween.tween_property(self, "scale", Vector2(1.0, 1.0), 0.1)
|
||||||
|
tween.tween_callback(func() -> void: _set_state(State.IDLE))
|
||||||
80
scripts/systems/drag_drop_component.gd
Normal file
80
scripts/systems/drag_drop_component.gd
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
## DragDropComponent — reusable drag-and-drop node; attach to any Character or InteractiveObject.
|
||||||
|
class_name DragDropComponent extends Node
|
||||||
|
|
||||||
|
signal drag_picked_up(global_position: Vector2)
|
||||||
|
signal drag_released(global_position: Vector2)
|
||||||
|
|
||||||
|
const DRAG_Z_INDEX: int = 10
|
||||||
|
const DRAG_SCALE: float = 1.1
|
||||||
|
|
||||||
|
@export var drag_target: Node2D
|
||||||
|
|
||||||
|
var _is_dragging: bool = false
|
||||||
|
var _drag_offset: Vector2 = Vector2.ZERO
|
||||||
|
var _original_z_index: int = 0
|
||||||
|
var _original_scale: Vector2 = Vector2.ONE
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if drag_target == null:
|
||||||
|
drag_target = get_parent() as Node2D
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventScreenTouch:
|
||||||
|
_handle_press(event.pressed, event.position)
|
||||||
|
elif event is InputEventScreenDrag and _is_dragging:
|
||||||
|
_move_to(event.position)
|
||||||
|
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
_handle_press(event.pressed, event.position)
|
||||||
|
elif event is InputEventMouseMotion and _is_dragging:
|
||||||
|
_move_to(event.position)
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_press(pressed: bool, screen_pos: Vector2) -> void:
|
||||||
|
if pressed and _is_position_over_target(screen_pos):
|
||||||
|
_start_drag(screen_pos)
|
||||||
|
elif not pressed and _is_dragging:
|
||||||
|
_end_drag(screen_pos)
|
||||||
|
|
||||||
|
|
||||||
|
func _start_drag(screen_pos: Vector2) -> void:
|
||||||
|
_is_dragging = true
|
||||||
|
var world_pos: Vector2 = _screen_to_world(screen_pos)
|
||||||
|
_drag_offset = drag_target.global_position - world_pos
|
||||||
|
_original_z_index = drag_target.z_index
|
||||||
|
_original_scale = drag_target.scale
|
||||||
|
drag_target.z_index = DRAG_Z_INDEX
|
||||||
|
drag_target.scale = _original_scale * DRAG_SCALE
|
||||||
|
drag_picked_up.emit(drag_target.global_position)
|
||||||
|
|
||||||
|
|
||||||
|
func _end_drag(_screen_pos: Vector2) -> void:
|
||||||
|
_is_dragging = false
|
||||||
|
drag_target.z_index = _original_z_index
|
||||||
|
drag_target.scale = _original_scale
|
||||||
|
drag_released.emit(drag_target.global_position)
|
||||||
|
|
||||||
|
|
||||||
|
func _move_to(screen_pos: Vector2) -> void:
|
||||||
|
var world_pos: Vector2 = _screen_to_world(screen_pos)
|
||||||
|
drag_target.global_position = world_pos + _drag_offset
|
||||||
|
|
||||||
|
|
||||||
|
func _screen_to_world(screen_pos: Vector2) -> Vector2:
|
||||||
|
var canvas_transform: Transform2D = drag_target.get_viewport().get_canvas_transform()
|
||||||
|
return canvas_transform.affine_inverse() * screen_pos
|
||||||
|
|
||||||
|
|
||||||
|
func _is_position_over_target(screen_pos: Vector2) -> bool:
|
||||||
|
if drag_target == null:
|
||||||
|
return false
|
||||||
|
var world_pos: Vector2 = _screen_to_world(screen_pos)
|
||||||
|
var local_pos: Vector2 = drag_target.to_local(world_pos)
|
||||||
|
var area: Area2D = drag_target.get_node_or_null("CollisionArea") as Area2D
|
||||||
|
if area == null:
|
||||||
|
return local_pos.length() < 64.0
|
||||||
|
for child in area.get_children():
|
||||||
|
if child is CollisionShape2D and child.shape != null:
|
||||||
|
return child.shape.get_rect().has_point(local_pos)
|
||||||
|
return local_pos.length() < 64.0
|
||||||
21
scripts/systems/hud.gd
Normal file
21
scripts/systems/hud.gd
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## HUD — heads-up display with back button and music toggle.
|
||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
var _music_enabled: bool = true
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func _on_back_button_pressed() -> void:
|
||||||
|
get_tree().quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_music_toggle_pressed() -> void:
|
||||||
|
_music_enabled = not _music_enabled
|
||||||
|
var volume: float = AudioManager.DEFAULT_MUSIC_VOLUME if _music_enabled else 0.0
|
||||||
|
AudioManager.set_music_volume(volume)
|
||||||
|
var btn: Button = get_node_or_null("MusicToggle") as Button
|
||||||
|
if btn != null:
|
||||||
|
btn.text = "♪" if _music_enabled else "✕"
|
||||||
Reference in New Issue
Block a user