31 Commits

Author SHA1 Message Date
Steven Wroblewski
e4ecae376e feat(floor1): wire up all four first-floor rooms with horizontal navigation arrows 2026-04-17 14:33:55 +02:00
Steven Wroblewski
9b91e78c20 feat(patientroom): add Patient Room with two beds, TV, and bedside table 2026-04-17 14:32:07 +02:00
Steven Wroblewski
8773ecfa6a feat(lab): add Lab room with bench, sink, and interactive lab equipment 2026-04-17 14:30:31 +02:00
Steven Wroblewski
c1f742ae61 feat(pharmacy): add Pharmacy room with double medicine shelf and positive medicine objects 2026-04-17 14:28:48 +02:00
Steven Wroblewski
e14303da9b feat(xray): add XRay Room scene with exam table and XRayMachine 2026-04-17 14:26:14 +02:00
Steven Wroblewski
5f33e16165 feat(xray): add XRayMachine component with plate slide-in animation 2026-04-17 14:25:04 +02:00
Steven Wroblewski
353efc100f docs: mark Sprint 5-7 as complete 2026-04-17 14:14:39 +02:00
Steven Wroblewski
c4e33aef62 fix(emergency): explicitly set ambulance trigger_floor and trigger_room to prevent silent failure on room reorder 2026-04-17 14:01:56 +02:00
Steven Wroblewski
c0c86b0998 feat(floor0): wire up all four ground-floor rooms with horizontal navigation arrows 2026-04-17 13:59:01 +02:00
Steven Wroblewski
cc44519d62 feat(emergency): add Emergency Room scene with ambulance bay, medical table, and interactive objects 2026-04-17 13:57:30 +02:00
Steven Wroblewski
045aa8c3b6 feat(ambulance): add Ambulance component with drive-in/out animation triggered by room navigation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 13:56:11 +02:00
Steven Wroblewski
b1ea85a449 feat(restaurant): add Restaurant room with three tables and interactive food objects 2026-04-17 13:54:35 +02:00
Steven Wroblewski
35ab8ffeb3 feat(giftshop): add Gift Shop room with shelf, counter, and interactive gift objects 2026-04-17 13:53:40 +02:00
Steven Wroblewski
a48539d410 feat(reception): complete Reception with waiting benches, bell, and potted plant 2026-04-17 13:52:42 +02:00
Steven Wroblewski
130317b516 feat(navigation): add NavigationArrow component for within-floor room navigation 2026-04-17 13:51:32 +02:00
Steven Wroblewski
903df578e2 feat(navigation): extend RoomNavigator for horizontal room-within-floor navigation 2026-04-17 13:50:00 +02:00
Steven Wroblewski
2bb73c905c fix(ui): sync settings sliders to saved values when menu opens
show_menu() now calls set_value_no_signal() to reflect the current
GameState volume values without re-triggering value_changed handlers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 13:28:57 +02:00
Steven Wroblewski
6bc199f6c1 fix(core): correct camera Y offset in RoomNavigator floor navigation
Floor N spans from N*-720 to (N-1)*-720. The camera must center at the
floor midpoint, so target_y = floor_index * -720 + 360 (half floor height).
Previous formula placed the camera at the floor boundary, showing content
split between two floors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 13:26:03 +02:00
Steven Wroblewski
f4611e2443 fix(core): serialize character positions as [x,y] arrays for JSON compatibility
Vector2 is not a JSON-native type — JSON.stringify converts it to a string,
which cannot be assigned back to a Vector2 return type on load. Positions are
now saved as [x, y] float arrays and reconstructed as Vector2 in apply_save_data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 13:22:56 +02:00
Steven Wroblewski
22f4b23897 fix(objects): replace Area2D.input_event with _input() for reliable Android touch
Area2D.input_event is unreliable on Android with gl_compatibility renderer.
Switched to manual _input() hit detection using canvas_transform coordinate
conversion, consistent with the DragDropComponent approach already used in
this project.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 13:20:14 +02:00
Steven Wroblewski
51bfd1718b fix(core): enforce CharacterData compile order and remove ClassDB type lookup on export
character.gd referenced CharacterData.State as a compile-time constant dict,
causing export to fail when character.gd was compiled before character_data.gd
(alphabetical order). The preload forces character_data.gd to be compiled first.
Changing SubResource type to "Resource" removes the ClassDB lookup for the
custom class name during scene export, relying on the explicit script reference
instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 13:11:30 +02:00
Steven Wroblewski
8f416b8999 fix(core): add explicit script reference for CharacterData SubResource in Main.tscn
Godot 4 Android export cannot resolve custom class names from the global
script cache during scene export. Adding an explicit ExtResource reference
to character_data.gd ensures the type is resolved at load time without
relying on the class cache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 12:57:39 +02:00
Steven Wroblewski
19d1ec1fe5 fix(core): remove class_name from autoload scripts to prevent singleton conflict
Godot 4 throws "Class hides an autoload singleton" when a script declares
class_name with the same name as its registered autoload. Removed class_name
from AudioManager and RoomNavigator — both are accessible globally via their
autoload name without it. Also ignores Godot-generated *.uid files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 12:44:31 +02:00
Steven Wroblewski
6b0c41bbfd fix(core): resolve review findings in Sprint 3-4
- Fix SaveManager reset_game to use DirAccess.remove correctly
- Add null check for FileAccess.open in save_game
- Fix AudioManager crossfade tween callback chain
- Replace fragile absolute HUD path with relative onready
- Guard character position save against empty id
- Add GameState.has_character_position helper
- Emit room_changed signal after tween completes
- Add target_floor validation in ElevatorButton
- Persist audio settings via GameState
- Add class_name to RoomNavigator, AudioManager, HUD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 12:22:27 +02:00
Steven Wroblewski
13db45bb04 feat(core): implement room navigation, character states, save/load and settings menu
- RoomNavigator autoload: smooth camera pan between floors
- Floor1 and Floor2 placeholder rooms with elevator buttons
- CharacterData Resource with State enum (HEALTHY/SICK/SLEEPING/TIRED)
- Character visual state feedback via ColorRect color
- Main scene loads saved state on startup
- SettingsMenu with music/sfx sliders and game reset
- HUD settings button to open SettingsMenu

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 12:17:56 +02:00
Steven Wroblewski
ace7d722ed chore: enable ETC2/ASTC texture compression for Android export 2026-04-17 11:53:04 +02:00
Steven Wroblewski
f7c8a7ec03 fix(poc): resolve evaluator and review findings
- Add RectangleShape2D to Character and InteractiveObject collision areas
- Fix HUD button signal connections in _ready()
- Fix character_placed signal emitting global_position
- Extract DEFAULT_DRAG_RADIUS constant in DragDropComponent
- Type Variant on JSON parsed variable in SaveManager
- Extract music symbol constants in HUD
- Refactor duplicated drag input code in InputManager

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 11:16:05 +02:00
Steven Wroblewski
9a1e30d808 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>
2026-04-17 11:16:04 +02:00
Steven Wroblewski
c2028edb2f chore(setup): update Godot version to 4.6 in project.godot 2026-04-17 11:12:38 +02:00
Steven Wroblewski
9daf06c9b1 chore(setup): add minimal project.godot for Godot 4 2026-04-17 11:06:56 +02:00
Steven Wroblewski
2ebed0a511 chore(setup): add editorconfig and smoke-test checklist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 11:05:18 +02:00
38 changed files with 2073 additions and 5 deletions

23
.editorconfig Normal file
View File

@@ -0,0 +1,23 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.gd]
indent_style = tab
indent_size = 4
[*.{tscn,tres,godot,cfg}]
indent_style = tab
[*.md]
trim_trailing_whitespace = false
indent_style = space
indent_size = 2
[*.json]
indent_style = space
indent_size = 2

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
# Godot generated files # Godot generated files
.godot/ .godot/
.import/ .import/
*.uid
# Godot-specific ignores # Godot-specific ignores
export.cfg export.cfg

View File

@@ -254,11 +254,11 @@ Realistisch für einen Vollzeit-Entwickler mit Familie und Side-Projekten. Kürz
- [ ] Settings-Menü (Lautstärke, Reset) - [ ] Settings-Menü (Lautstärke, Reset)
- [ ] Character-State-System (gesund, krank, schläft) - [ ] Character-State-System (gesund, krank, schläft)
### Sprint 5-7: Erdgeschoss (Woche 6-8) ### Sprint 5-7: Erdgeschoss (Woche 6-8)
- [ ] Empfang komplett - [x] Empfang komplett
- [ ] Geschenke-Shop - [x] Geschenke-Shop
- [ ] Restaurant - [x] Restaurant
- [ ] Notaufnahme mit Krankenwagen-Animation - [x] Notaufnahme mit Krankenwagen-Animation
### Sprint 8-10: 1. Obergeschoss (Woche 9-11) ### Sprint 8-10: 1. Obergeschoss (Woche 9-11)
- [ ] Röntgen mit Slide-Animation - [ ] Röntgen mit Slide-Animation

58
docs/smoke-tests.md Normal file
View File

@@ -0,0 +1,58 @@
# Smoke-Test-Checkliste
> Smoke Tests werden nach jedem Sprint manuell auf einem echten Android-Tablet durchgeführt.
> Ziel ist kein vollständiger Test, sondern ein schnelles "Geht es grundlegend?" vor dem Zeigen an die Kinder.
> Neue Sprint-Abschnitte werden jeweils zu Beginn des Sprints ergänzt.
---
## Basis-Tests (jeder Sprint)
Diese Tests werden bei jedem Sprint-Abschluss durchgeführt, unabhängig vom Inhalt.
- [ ] App startet ohne Absturz
- [ ] Kein Freeze oder schwarzer Screen in den ersten 5 Sekunden
- [ ] Musik spielt beim Start (ca. 60% Lautstärke)
- [ ] HUD: Back-Button sichtbar und funktionsfähig
- [ ] HUD: Musik-Toggle funktioniert (an/aus)
- [ ] Performance: Keine sichtbaren Ruckler beim Scrollen/Navigieren
- [ ] Touch-Targets: Alle interaktiven Elemente mit Kinderfingern erreichbar (min. 48dp)
- [ ] Offline: Spiel funktioniert vollständig ohne WLAN/Mobilfunk
- [ ] Kein Netzwerk-Request im Hintergrund (DevTools / Android Profiler prüfen)
---
## Sprint 1 — Proof of Concept
Ziel: Erster spielbarer Raum (Empfang/Reception), Drag & Drop einer Figur, ein interaktives Objekt.
- [ ] Empfangs-Raum lädt ohne Fehler
- [ ] Figur (Häschen oder Kätzchen) ist auf dem Bildschirm sichtbar
- [ ] Figur lässt sich per Touch aufheben (Drag-Start)
- [ ] Figur lässt sich an beliebiger Stelle loslassen (Drop)
- [ ] Figur kehrt nicht automatisch zur Startposition zurück
- [ ] Interaktives Objekt (Blume) reagiert auf Touch (Animation oder Sound)
- [ ] Position der Figur bleibt nach App-Neustart erhalten (Save/Load)
- [ ] Keine GDScript-Fehler in der Godot-Konsole
---
## Sprint 2 — (wird ergänzt)
---
## Sprint 3 — (wird ergänzt)
---
## Sprint 4 — (wird ergänzt)
---
## Sprint 5 — (wird ergänzt)
---
## Notizen
_Auffälligkeiten, Geräte-spezifische Bugs oder UAT-Feedback der Kinder hier festhalten._

43
project.godot Normal file
View File

@@ -0,0 +1,43 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Cozypaw Hospital"
config/description="Werbefrei. Offline. Für Kinder."
config/version="0.1.0"
run/main_scene="res://scenes/main/Main.tscn"
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"
RoomNavigator="*res://scripts/systems/room_navigator.gd"
[display]
window/size/viewport_width=1280
window/size/viewport_height=720
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
[input_devices]
pointing/emulate_touch_from_mouse=true
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
textures/vram_compression/import_etc2_astc=true

View File

@@ -0,0 +1,51 @@
[gd_scene load_steps=4 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"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_char"]
size = Vector2(64, 80)
[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"]
shape = SubResource("RectangleShape2D_char")
position = Vector2(0, -40)

186
scenes/main/Main.tscn Normal file
View File

@@ -0,0 +1,186 @@
[gd_scene load_steps=17 format=3 uid="uid://cozypaw_main"]
[ext_resource type="Script" path="res://scripts/main/main.gd" id="1_main"]
[ext_resource type="PackedScene" path="res://scenes/rooms/floor0/Reception.tscn" id="2_reception"]
[ext_resource type="PackedScene" path="res://scenes/characters/Character.tscn" id="3_char"]
[ext_resource type="PackedScene" path="res://scenes/ui/HUD.tscn" id="4_hud"]
[ext_resource type="PackedScene" path="res://scenes/ui/SettingsMenu.tscn" id="5_settings"]
[ext_resource type="PackedScene" path="res://scenes/objects/ElevatorButton.tscn" id="6_elevbtn"]
[ext_resource type="Script" path="res://scripts/characters/character_data.gd" id="7_chardata"]
[ext_resource type="PackedScene" path="res://scenes/rooms/floor0/GiftShop.tscn" id="8_giftshop"]
[ext_resource type="PackedScene" path="res://scenes/rooms/floor0/Restaurant.tscn" id="9_restaurant"]
[ext_resource type="PackedScene" path="res://scenes/rooms/floor0/EmergencyRoom.tscn" id="10_emergency"]
[ext_resource type="PackedScene" path="res://scenes/objects/NavigationArrow.tscn" id="11_navarrow"]
[ext_resource type="PackedScene" path="res://scenes/rooms/floor1/XRay.tscn" id="12_xray"]
[ext_resource type="PackedScene" path="res://scenes/rooms/floor1/Pharmacy.tscn" id="13_pharmacy"]
[ext_resource type="PackedScene" path="res://scenes/rooms/floor1/Lab.tscn" id="14_lab"]
[ext_resource type="PackedScene" path="res://scenes/rooms/floor1/PatientRoom.tscn" id="15_patientroom"]
[sub_resource type="Resource" id="CharacterData_bunny1"]
script = ExtResource("7_chardata")
id = "bunny_01"
display_name = "Bunny"
species = 0
state = 0
current_floor = 0
position = Vector2(0, 0)
[node name="Main" type="Node2D"]
script = ExtResource("1_main")
[node name="Camera2D" type="Camera2D" parent="."]
position = Vector2(640, 360)
zoom = Vector2(1, 1)
[node name="Hospital" type="Node2D" parent="."]
[node name="Floor0" type="Node2D" parent="Hospital"]
position = Vector2(0, 0)
[node name="Reception" parent="Hospital/Floor0" instance=ExtResource("2_reception")]
position = Vector2(0, 0)
[node name="GiftShop" parent="Hospital/Floor0" instance=ExtResource("8_giftshop")]
position = Vector2(1280, 0)
[node name="Restaurant" parent="Hospital/Floor0" instance=ExtResource("9_restaurant")]
position = Vector2(2560, 0)
[node name="EmergencyRoom" parent="Hospital/Floor0" instance=ExtResource("10_emergency")]
position = Vector2(3840, 0)
[node name="ElevatorUp0" parent="Hospital/Floor0" instance=ExtResource("6_elevbtn")]
position = Vector2(1100, 160)
target_floor = 1
[node name="NavRight0to1" parent="Hospital/Floor0" instance=ExtResource("11_navarrow")]
position = Vector2(1200, 480)
target_floor = 0
target_room = 1
label_text = "→"
[node name="NavLeft1to0" parent="Hospital/Floor0" instance=ExtResource("11_navarrow")]
position = Vector2(1340, 480)
target_floor = 0
target_room = 0
label_text = "←"
[node name="NavRight1to2" parent="Hospital/Floor0" instance=ExtResource("11_navarrow")]
position = Vector2(2480, 480)
target_floor = 0
target_room = 2
label_text = "→"
[node name="NavLeft2to1" parent="Hospital/Floor0" instance=ExtResource("11_navarrow")]
position = Vector2(2600, 480)
target_floor = 0
target_room = 1
label_text = "←"
[node name="NavRight2to3" parent="Hospital/Floor0" instance=ExtResource("11_navarrow")]
position = Vector2(3760, 480)
target_floor = 0
target_room = 3
label_text = "→"
[node name="NavLeft3to2" parent="Hospital/Floor0" instance=ExtResource("11_navarrow")]
position = Vector2(3920, 480)
target_floor = 0
target_room = 2
label_text = "←"
[node name="Floor1" type="Node2D" parent="Hospital"]
position = Vector2(0, -720)
[node name="XRay" parent="Hospital/Floor1" instance=ExtResource("12_xray")]
position = Vector2(0, 0)
[node name="Pharmacy" parent="Hospital/Floor1" instance=ExtResource("13_pharmacy")]
position = Vector2(1280, 0)
[node name="Lab" parent="Hospital/Floor1" instance=ExtResource("14_lab")]
position = Vector2(2560, 0)
[node name="PatientRoom" parent="Hospital/Floor1" instance=ExtResource("15_patientroom")]
position = Vector2(3840, 0)
[node name="ElevatorDown1" parent="Hospital/Floor1" instance=ExtResource("6_elevbtn")]
position = Vector2(1100, 540)
target_floor = 0
[node name="ElevatorUp1" parent="Hospital/Floor1" instance=ExtResource("6_elevbtn")]
position = Vector2(1100, 180)
target_floor = 2
[node name="NavRight0to1_F1" parent="Hospital/Floor1" instance=ExtResource("11_navarrow")]
position = Vector2(1200, 480)
target_floor = 1
target_room = 1
label_text = "→"
[node name="NavLeft1to0_F1" parent="Hospital/Floor1" instance=ExtResource("11_navarrow")]
position = Vector2(1340, 480)
target_floor = 1
target_room = 0
label_text = "←"
[node name="NavRight1to2_F1" parent="Hospital/Floor1" instance=ExtResource("11_navarrow")]
position = Vector2(2480, 480)
target_floor = 1
target_room = 2
label_text = "→"
[node name="NavLeft2to1_F1" parent="Hospital/Floor1" instance=ExtResource("11_navarrow")]
position = Vector2(2600, 480)
target_floor = 1
target_room = 1
label_text = "←"
[node name="NavRight2to3_F1" parent="Hospital/Floor1" instance=ExtResource("11_navarrow")]
position = Vector2(3760, 480)
target_floor = 1
target_room = 3
label_text = "→"
[node name="NavLeft3to2_F1" parent="Hospital/Floor1" instance=ExtResource("11_navarrow")]
position = Vector2(3920, 480)
target_floor = 1
target_room = 2
label_text = "←"
[node name="Floor2" type="Node2D" parent="Hospital"]
position = Vector2(0, -1440)
[node name="Background" type="ColorRect" parent="Hospital/Floor2"]
color = Color(0.96, 0.88, 0.96, 1)
size = Vector2(1280, 720)
position = Vector2(0, 0)
[node name="FloorLabel" type="Label" parent="Hospital/Floor2"]
offset_left = 560.0
offset_top = 300.0
offset_right = 720.0
offset_bottom = 360.0
text = "2. OG"
theme_override_font_sizes/font_size = 32
[node name="FloorRect" type="ColorRect" parent="Hospital/Floor2"]
color = Color(0.88, 0.80, 0.68, 1)
size = Vector2(1280, 100)
position = Vector2(0, 620)
[node name="ElevatorDown2" parent="Hospital/Floor2" instance=ExtResource("6_elevbtn")]
position = Vector2(1200, 360)
target_floor = 1
[node name="Characters" type="Node2D" parent="."]
[node name="Bunny1" parent="Characters" instance=ExtResource("3_char")]
position = Vector2(300, 400)
data = SubResource("CharacterData_bunny1")
[node name="UI" type="CanvasLayer" parent="."]
[node name="HUD" parent="UI" instance=ExtResource("4_hud")]
[node name="SettingsMenu" parent="UI" instance=ExtResource("5_settings")]

View File

@@ -0,0 +1,41 @@
[gd_scene load_steps=2 format=3 uid="uid://cozypaw_ambulance"]
[ext_resource type="Script" path="res://scripts/objects/ambulance.gd" id="1_ambulance"]
[node name="Ambulance" type="Node2D"]
script = ExtResource("1_ambulance")
[node name="Body" type="ColorRect" parent="."]
color = Color(1.0, 1.0, 1.0, 1)
size = Vector2(180, 100)
position = Vector2(-90, -50)
[node name="RedStripe" type="ColorRect" parent="."]
color = Color(0.90, 0.15, 0.15, 1)
size = Vector2(180, 20)
position = Vector2(-90, -15)
[node name="Cabin" type="ColorRect" parent="."]
color = Color(0.82, 0.85, 0.90, 1)
size = Vector2(70, 60)
position = Vector2(-90, -50)
[node name="WindowFront" type="ColorRect" parent="."]
color = Color(0.60, 0.85, 1.0, 1)
size = Vector2(50, 38)
position = Vector2(-82, -46)
[node name="WheelFront" type="ColorRect" parent="."]
color = Color(0.15, 0.15, 0.15, 1)
size = Vector2(30, 30)
position = Vector2(-75, 48)
[node name="WheelBack" type="ColorRect" parent="."]
color = Color(0.15, 0.15, 0.15, 1)
size = Vector2(30, 30)
position = Vector2(50, 48)
[node name="Siren" type="ColorRect" parent="."]
color = Color(0.10, 0.40, 1.0, 1)
size = Vector2(40, 18)
position = Vector2(-20, -68)

View File

@@ -0,0 +1,29 @@
[gd_scene load_steps=3 format=3 uid="uid://cozypaw_elevator_btn"]
[ext_resource type="Script" path="res://scripts/objects/elevator_button.gd" id="1_elevbtn"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_elevbtn"]
size = Vector2(80, 80)
[node name="ElevatorButton" type="Node2D"]
script = ExtResource("1_elevbtn")
[node name="Visual" type="ColorRect" parent="."]
color = Color(0.95, 0.75, 0.3, 1)
size = Vector2(80, 80)
position = Vector2(-40, -40)
[node name="Label" type="Label" parent="Visual"]
offset_left = 0.0
offset_top = 0.0
offset_right = 80.0
offset_bottom = 80.0
horizontal_alignment = 1
vertical_alignment = 1
text = "▲"
[node name="CollisionArea" type="Area2D" parent="."]
input_pickable = true
[node name="CollisionShape" type="CollisionShape2D" parent="CollisionArea"]
shape = SubResource("RectangleShape2D_elevbtn")

View File

@@ -0,0 +1,32 @@
[gd_scene load_steps=4 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"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_obj"]
size = Vector2(48, 48)
[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"]
shape = SubResource("RectangleShape2D_obj")

View File

@@ -0,0 +1,21 @@
[gd_scene load_steps=2 format=3 uid="uid://cozypaw_navarrow"]
[ext_resource type="Script" path="res://scripts/objects/navigation_arrow.gd" id="1_navarrow"]
[node name="NavigationArrow" type="Node2D"]
script = ExtResource("1_navarrow")
[node name="Body" type="ColorRect" parent="."]
color = Color(0.30, 0.60, 1.0, 0.85)
size = Vector2(96, 96)
position = Vector2(-48, -48)
[node name="Label" type="Label" parent="."]
offset_left = -24.0
offset_top = -18.0
offset_right = 24.0
offset_bottom = 18.0
text = "→"
theme_override_font_sizes/font_size = 36
horizontal_alignment = 1
vertical_alignment = 1

View File

@@ -0,0 +1,34 @@
[gd_scene load_steps=2 format=3 uid="uid://cozypaw_xraymachine"]
[ext_resource type="Script" path="res://scripts/objects/xray_machine.gd" id="1_xraymachine"]
[node name="XRayMachine" type="Node2D"]
script = ExtResource("1_xraymachine")
[node name="MachineBody" type="ColorRect" parent="."]
color = Color(0.68, 0.70, 0.76, 1)
size = Vector2(80, 200)
position = Vector2(-40, -200)
[node name="MachineArm" type="ColorRect" parent="."]
color = Color(0.62, 0.64, 0.70, 1)
size = Vector2(220, 24)
position = Vector2(-40, -180)
[node name="ViewerFrame" type="ColorRect" parent="."]
color = Color(0.20, 0.22, 0.26, 1)
size = Vector2(80, 60)
position = Vector2(-40, -195)
[node name="Viewer" type="ColorRect" parent="."]
color = Color(0.06, 0.08, 0.12, 1)
size = Vector2(66, 46)
position = Vector2(-33, -189)
[node name="Plate" type="Node2D" parent="."]
position = Vector2(-50, 20)
[node name="PlateBody" type="ColorRect" parent="Plate"]
color = Color(0.28, 0.32, 0.38, 1)
size = Vector2(200, 14)
position = Vector2(0, -7)

View File

@@ -0,0 +1,75 @@
[gd_scene load_steps=3 format=3 uid="uid://cozypaw_emergency"]
[ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"]
[ext_resource type="PackedScene" path="res://scenes/objects/Ambulance.tscn" id="2_ambulance"]
[node name="EmergencyRoom" type="Node2D"]
[node name="Background" type="ColorRect" parent="."]
color = Color(0.88, 0.94, 1.0, 1)
size = Vector2(1280, 720)
position = Vector2(0, 0)
[node name="Floor" type="ColorRect" parent="."]
color = Color(0.72, 0.74, 0.80, 1)
size = Vector2(1280, 100)
position = Vector2(0, 620)
[node name="WallLeft" type="ColorRect" parent="."]
color = Color(0.90, 0.92, 0.96, 1)
size = Vector2(40, 620)
position = Vector2(0, 0)
[node name="WallRight" type="ColorRect" parent="."]
color = Color(0.90, 0.92, 0.96, 1)
size = Vector2(40, 620)
position = Vector2(1240, 0)
[node name="AmbulanceBay" type="ColorRect" parent="."]
color = Color(0.58, 0.60, 0.68, 1)
size = Vector2(380, 120)
position = Vector2(860, 500)
[node name="AmbulanceBayLine" type="ColorRect" parent="."]
color = Color(0.90, 0.80, 0.10, 1)
size = Vector2(380, 6)
position = Vector2(860, 500)
[node name="MedicalTable" type="ColorRect" parent="."]
color = Color(0.85, 0.85, 0.92, 1)
size = Vector2(260, 30)
position = Vector2(180, 490)
[node name="MedicalTableLeg1" type="ColorRect" parent="."]
color = Color(0.68, 0.68, 0.76, 1)
size = Vector2(16, 130)
position = Vector2(200, 520)
[node name="MedicalTableLeg2" type="ColorRect" parent="."]
color = Color(0.68, 0.68, 0.76, 1)
size = Vector2(16, 130)
position = Vector2(408, 520)
[node name="IVPole" type="ColorRect" parent="."]
color = Color(0.78, 0.78, 0.84, 1)
size = Vector2(10, 220)
position = Vector2(545, 350)
[node name="IVBag" type="ColorRect" parent="."]
color = Color(0.55, 0.85, 0.95, 1)
size = Vector2(50, 70)
position = Vector2(525, 280)
[node name="CharacterSpawn" type="Marker2D" parent="."]
position = Vector2(310, 490)
[node name="Stretcher" parent="." instance=ExtResource("1_iobj")]
position = Vector2(680, 490)
[node name="IVStand" parent="." instance=ExtResource("1_iobj")]
position = Vector2(550, 440)
[node name="Ambulance" parent="." instance=ExtResource("2_ambulance")]
position = Vector2(500, 570)
trigger_floor = 0
trigger_room = 3

View File

@@ -0,0 +1,65 @@
[gd_scene load_steps=2 format=3 uid="uid://cozypaw_giftshop"]
[ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"]
[node name="GiftShop" type="Node2D"]
[node name="Background" type="ColorRect" parent="."]
color = Color(1.0, 0.88, 0.92, 1)
size = Vector2(1280, 720)
position = Vector2(0, 0)
[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.96, 0.90, 0.90, 1)
size = Vector2(40, 620)
position = Vector2(0, 0)
[node name="WallRight" type="ColorRect" parent="."]
color = Color(0.96, 0.90, 0.90, 1)
size = Vector2(40, 620)
position = Vector2(1240, 0)
[node name="ShelfBoard" type="ColorRect" parent="."]
color = Color(0.75, 0.50, 0.25, 1)
size = Vector2(800, 20)
position = Vector2(240, 360)
[node name="ShelfLegLeft" type="ColorRect" parent="."]
color = Color(0.60, 0.38, 0.18, 1)
size = Vector2(16, 240)
position = Vector2(240, 380)
[node name="ShelfLegRight" type="ColorRect" parent="."]
color = Color(0.60, 0.38, 0.18, 1)
size = Vector2(16, 240)
position = Vector2(1024, 380)
[node name="Counter" type="ColorRect" parent="."]
color = Color(0.60, 0.38, 0.18, 1)
size = Vector2(240, 80)
position = Vector2(520, 540)
[node name="CounterTop" type="ColorRect" parent="."]
color = Color(0.75, 0.50, 0.25, 1)
size = Vector2(240, 12)
position = Vector2(520, 528)
[node name="CharacterSpawn" type="Marker2D" parent="."]
position = Vector2(300, 520)
[node name="FlowerBouquet" parent="." instance=ExtResource("1_iobj")]
position = Vector2(380, 330)
[node name="PlushToy" parent="." instance=ExtResource("1_iobj")]
position = Vector2(640, 330)
[node name="GiftCard" parent="." instance=ExtResource("1_iobj")]
position = Vector2(900, 330)
[node name="GiftBox" parent="." instance=ExtResource("1_iobj")]
position = Vector2(640, 510)

View File

@@ -0,0 +1,87 @@
[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="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="Counter" type="ColorRect" parent="."]
color = Color(0.55, 0.35, 0.18, 1)
size = Vector2(340, 80)
position = Vector2(460, 540)
[node name="CounterTop" type="ColorRect" parent="."]
color = Color(0.70, 0.50, 0.28, 1)
size = Vector2(340, 12)
position = Vector2(460, 528)
[node name="NumberDisplayFrame" type="ColorRect" parent="."]
color = Color(0.20, 0.20, 0.20, 1)
size = Vector2(120, 80)
position = Vector2(570, 445)
[node name="NumberDisplayScreen" type="ColorRect" parent="."]
color = Color(0.10, 0.80, 0.20, 1)
size = Vector2(100, 60)
position = Vector2(580, 455)
[node name="BenchLeft" type="ColorRect" parent="."]
color = Color(0.60, 0.40, 0.20, 1)
size = Vector2(180, 40)
position = Vector2(100, 578)
[node name="BenchLeftLeg1" type="ColorRect" parent="."]
color = Color(0.45, 0.28, 0.12, 1)
size = Vector2(16, 32)
position = Vector2(112, 618)
[node name="BenchLeftLeg2" type="ColorRect" parent="."]
color = Color(0.45, 0.28, 0.12, 1)
size = Vector2(16, 32)
position = Vector2(252, 618)
[node name="BenchRight" type="ColorRect" parent="."]
color = Color(0.60, 0.40, 0.20, 1)
size = Vector2(180, 40)
position = Vector2(940, 578)
[node name="BenchRightLeg1" type="ColorRect" parent="."]
color = Color(0.45, 0.28, 0.12, 1)
size = Vector2(16, 32)
position = Vector2(952, 618)
[node name="BenchRightLeg2" type="ColorRect" parent="."]
color = Color(0.45, 0.28, 0.12, 1)
size = Vector2(16, 32)
position = Vector2(1092, 618)
[node name="CharacterSpawn" type="Marker2D" parent="."]
position = Vector2(300, 520)
[node name="Flower" parent="." instance=ExtResource("1_iobj")]
position = Vector2(200, 560)
[node name="Bell" parent="." instance=ExtResource("1_iobj")]
position = Vector2(530, 510)
[node name="PottedPlant" parent="." instance=ExtResource("1_iobj")]
position = Vector2(1180, 560)

View File

@@ -0,0 +1,95 @@
[gd_scene load_steps=2 format=3 uid="uid://cozypaw_restaurant"]
[ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"]
[node name="Restaurant" type="Node2D"]
[node name="Background" type="ColorRect" parent="."]
color = Color(1.0, 0.95, 0.82, 1)
size = Vector2(1280, 720)
position = Vector2(0, 0)
[node name="Floor" type="ColorRect" parent="."]
color = Color(0.75, 0.65, 0.50, 1)
size = Vector2(1280, 100)
position = Vector2(0, 620)
[node name="WallLeft" type="ColorRect" parent="."]
color = Color(0.96, 0.92, 0.80, 1)
size = Vector2(40, 620)
position = Vector2(0, 0)
[node name="WallRight" type="ColorRect" parent="."]
color = Color(0.96, 0.92, 0.80, 1)
size = Vector2(40, 620)
position = Vector2(1240, 0)
[node name="KitchenCounter" type="ColorRect" parent="."]
color = Color(0.55, 0.35, 0.18, 1)
size = Vector2(360, 80)
position = Vector2(460, 240)
[node name="KitchenCounterTop" type="ColorRect" parent="."]
color = Color(0.70, 0.50, 0.28, 1)
size = Vector2(360, 12)
position = Vector2(460, 228)
[node name="Table1" type="ColorRect" parent="."]
color = Color(0.65, 0.42, 0.20, 1)
size = Vector2(200, 20)
position = Vector2(120, 520)
[node name="Table1Leg1" type="ColorRect" parent="."]
color = Color(0.50, 0.32, 0.15, 1)
size = Vector2(16, 100)
position = Vector2(140, 540)
[node name="Table1Leg2" type="ColorRect" parent="."]
color = Color(0.50, 0.32, 0.15, 1)
size = Vector2(16, 100)
position = Vector2(304, 540)
[node name="Table2" type="ColorRect" parent="."]
color = Color(0.65, 0.42, 0.20, 1)
size = Vector2(200, 20)
position = Vector2(540, 520)
[node name="Table2Leg1" type="ColorRect" parent="."]
color = Color(0.50, 0.32, 0.15, 1)
size = Vector2(16, 100)
position = Vector2(560, 540)
[node name="Table2Leg2" type="ColorRect" parent="."]
color = Color(0.50, 0.32, 0.15, 1)
size = Vector2(16, 100)
position = Vector2(724, 540)
[node name="Table3" type="ColorRect" parent="."]
color = Color(0.65, 0.42, 0.20, 1)
size = Vector2(200, 20)
position = Vector2(960, 520)
[node name="Table3Leg1" type="ColorRect" parent="."]
color = Color(0.50, 0.32, 0.15, 1)
size = Vector2(16, 100)
position = Vector2(980, 540)
[node name="Table3Leg2" type="ColorRect" parent="."]
color = Color(0.50, 0.32, 0.15, 1)
size = Vector2(16, 100)
position = Vector2(1144, 540)
[node name="CharacterSpawn" type="Marker2D" parent="."]
position = Vector2(300, 490)
[node name="SoupBowl" parent="." instance=ExtResource("1_iobj")]
position = Vector2(220, 490)
[node name="Sandwich" parent="." instance=ExtResource("1_iobj")]
position = Vector2(640, 490)
[node name="JuiceGlass" parent="." instance=ExtResource("1_iobj")]
position = Vector2(1060, 490)
[node name="CashRegister" parent="." instance=ExtResource("1_iobj")]
position = Vector2(640, 210)

View File

@@ -0,0 +1,70 @@
[gd_scene load_steps=2 format=3 uid="uid://cozypaw_lab"]
[ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"]
[node name="Lab" type="Node2D"]
[node name="Background" type="ColorRect" parent="."]
color = Color(0.90, 0.94, 0.96, 1)
size = Vector2(1280, 720)
position = Vector2(0, 0)
[node name="Floor" type="ColorRect" parent="."]
color = Color(0.78, 0.82, 0.86, 1)
size = Vector2(1280, 100)
position = Vector2(0, 620)
[node name="WallLeft" type="ColorRect" parent="."]
color = Color(0.88, 0.92, 0.94, 1)
size = Vector2(40, 620)
position = Vector2(0, 0)
[node name="WallRight" type="ColorRect" parent="."]
color = Color(0.88, 0.92, 0.94, 1)
size = Vector2(40, 620)
position = Vector2(1240, 0)
[node name="LabBench" type="ColorRect" parent="."]
color = Color(0.88, 0.88, 0.92, 1)
size = Vector2(800, 40)
position = Vector2(240, 480)
[node name="LabBenchFront" type="ColorRect" parent="."]
color = Color(0.70, 0.72, 0.78, 1)
size = Vector2(800, 16)
position = Vector2(240, 520)
[node name="LabBenchLegLeft" type="ColorRect" parent="."]
color = Color(0.70, 0.72, 0.78, 1)
size = Vector2(16, 140)
position = Vector2(260, 536)
[node name="LabBenchLegRight" type="ColorRect" parent="."]
color = Color(0.70, 0.72, 0.78, 1)
size = Vector2(16, 140)
position = Vector2(1008, 536)
[node name="Sink" type="ColorRect" parent="."]
color = Color(0.75, 0.80, 0.84, 1)
size = Vector2(100, 50)
position = Vector2(1060, 430)
[node name="SinkBase" type="ColorRect" parent="."]
color = Color(0.60, 0.64, 0.68, 1)
size = Vector2(100, 190)
position = Vector2(1060, 480)
[node name="CharacterSpawn" type="Marker2D" parent="."]
position = Vector2(200, 490)
[node name="Microscope" parent="." instance=ExtResource("1_iobj")]
position = Vector2(380, 450)
[node name="TestTubeRack" parent="." instance=ExtResource("1_iobj")]
position = Vector2(600, 450)
[node name="ReagentBottle" parent="." instance=ExtResource("1_iobj")]
position = Vector2(820, 450)
[node name="PetriDish" parent="." instance=ExtResource("1_iobj")]
position = Vector2(490, 450)

View File

@@ -0,0 +1,84 @@
[gd_scene load_steps=2 format=3 uid="uid://cozypaw_patientroom"]
[ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"]
[node name="PatientRoom" type="Node2D"]
[node name="Background" type="ColorRect" parent="."]
color = Color(0.96, 0.94, 0.88, 1)
size = Vector2(1280, 720)
position = Vector2(0, 0)
[node name="Floor" type="ColorRect" parent="."]
color = Color(0.88, 0.82, 0.72, 1)
size = Vector2(1280, 100)
position = Vector2(0, 620)
[node name="WallLeft" type="ColorRect" parent="."]
color = Color(0.94, 0.92, 0.86, 1)
size = Vector2(40, 620)
position = Vector2(0, 0)
[node name="WallRight" type="ColorRect" parent="."]
color = Color(0.94, 0.92, 0.86, 1)
size = Vector2(40, 620)
position = Vector2(1240, 0)
[node name="Bed1Frame" type="ColorRect" parent="."]
color = Color(0.85, 0.85, 0.90, 1)
size = Vector2(260, 50)
position = Vector2(120, 490)
[node name="Bed1Pillow" type="ColorRect" parent="."]
color = Color(0.96, 0.96, 1.0, 1)
size = Vector2(80, 50)
position = Vector2(120, 440)
[node name="Bed1Leg1" type="ColorRect" parent="."]
color = Color(0.70, 0.70, 0.76, 1)
size = Vector2(14, 130)
position = Vector2(136, 540)
[node name="Bed1Leg2" type="ColorRect" parent="."]
color = Color(0.70, 0.70, 0.76, 1)
size = Vector2(14, 130)
position = Vector2(350, 540)
[node name="Bed2Frame" type="ColorRect" parent="."]
color = Color(0.85, 0.85, 0.90, 1)
size = Vector2(260, 50)
position = Vector2(680, 490)
[node name="Bed2Pillow" type="ColorRect" parent="."]
color = Color(0.96, 0.96, 1.0, 1)
size = Vector2(80, 50)
position = Vector2(680, 440)
[node name="Bed2Leg1" type="ColorRect" parent="."]
color = Color(0.70, 0.70, 0.76, 1)
size = Vector2(14, 130)
position = Vector2(696, 540)
[node name="Bed2Leg2" type="ColorRect" parent="."]
color = Color(0.70, 0.70, 0.76, 1)
size = Vector2(14, 130)
position = Vector2(910, 540)
[node name="TVFrame" type="ColorRect" parent="."]
color = Color(0.18, 0.18, 0.20, 1)
size = Vector2(200, 130)
position = Vector2(1000, 200)
[node name="TVScreen" type="ColorRect" parent="."]
color = Color(0.08, 0.10, 0.14, 1)
size = Vector2(184, 114)
position = Vector2(1008, 208)
[node name="CharacterSpawn" type="Marker2D" parent="."]
position = Vector2(480, 490)
[node name="TV" parent="." instance=ExtResource("1_iobj")]
position = Vector2(1100, 265)
[node name="BedsideTable" parent="." instance=ExtResource("1_iobj")]
position = Vector2(500, 540)

View File

@@ -0,0 +1,70 @@
[gd_scene load_steps=2 format=3 uid="uid://cozypaw_pharmacy"]
[ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"]
[node name="Pharmacy" type="Node2D"]
[node name="Background" type="ColorRect" parent="."]
color = Color(0.96, 0.94, 0.82, 1)
size = Vector2(1280, 720)
position = Vector2(0, 0)
[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.96, 0.94, 0.86, 1)
size = Vector2(40, 620)
position = Vector2(0, 0)
[node name="WallRight" type="ColorRect" parent="."]
color = Color(0.96, 0.94, 0.86, 1)
size = Vector2(40, 620)
position = Vector2(1240, 0)
[node name="MedicineShelf" type="ColorRect" parent="."]
color = Color(0.75, 0.55, 0.30, 1)
size = Vector2(700, 20)
position = Vector2(290, 300)
[node name="MedicineShelfLegLeft" type="ColorRect" parent="."]
color = Color(0.62, 0.44, 0.22, 1)
size = Vector2(16, 320)
position = Vector2(290, 300)
[node name="MedicineShelfLegRight" type="ColorRect" parent="."]
color = Color(0.62, 0.44, 0.22, 1)
size = Vector2(16, 320)
position = Vector2(974, 300)
[node name="MedicineShelf2" type="ColorRect" parent="."]
color = Color(0.75, 0.55, 0.30, 1)
size = Vector2(700, 20)
position = Vector2(290, 460)
[node name="Counter" type="ColorRect" parent="."]
color = Color(0.62, 0.44, 0.22, 1)
size = Vector2(300, 80)
position = Vector2(490, 540)
[node name="CounterTop" type="ColorRect" parent="."]
color = Color(0.78, 0.62, 0.36, 1)
size = Vector2(300, 12)
position = Vector2(490, 528)
[node name="CharacterSpawn" type="Marker2D" parent="."]
position = Vector2(200, 520)
[node name="VitaminBottle" parent="." instance=ExtResource("1_iobj")]
position = Vector2(380, 270)
[node name="BandageRoll" parent="." instance=ExtResource("1_iobj")]
position = Vector2(640, 270)
[node name="SyrupBottle" parent="." instance=ExtResource("1_iobj")]
position = Vector2(900, 270)
[node name="MedicineBox" parent="." instance=ExtResource("1_iobj")]
position = Vector2(640, 430)

View File

@@ -0,0 +1,55 @@
[gd_scene load_steps=3 format=3 uid="uid://cozypaw_xray"]
[ext_resource type="PackedScene" path="res://scenes/objects/InteractiveObject.tscn" id="1_iobj"]
[ext_resource type="PackedScene" path="res://scenes/objects/XRayMachine.tscn" id="2_xraymachine"]
[node name="XRay" type="Node2D"]
[node name="Background" type="ColorRect" parent="."]
color = Color(0.88, 0.92, 0.96, 1)
size = Vector2(1280, 720)
position = Vector2(0, 0)
[node name="Floor" type="ColorRect" parent="."]
color = Color(0.78, 0.80, 0.86, 1)
size = Vector2(1280, 100)
position = Vector2(0, 620)
[node name="WallLeft" type="ColorRect" parent="."]
color = Color(0.88, 0.90, 0.94, 1)
size = Vector2(40, 620)
position = Vector2(0, 0)
[node name="WallRight" type="ColorRect" parent="."]
color = Color(0.88, 0.90, 0.94, 1)
size = Vector2(40, 620)
position = Vector2(1240, 0)
[node name="ExamTable" type="ColorRect" parent="."]
color = Color(0.84, 0.86, 0.90, 1)
size = Vector2(320, 40)
position = Vector2(480, 490)
[node name="ExamTableLeg1" type="ColorRect" parent="."]
color = Color(0.65, 0.68, 0.74, 1)
size = Vector2(16, 130)
position = Vector2(500, 530)
[node name="ExamTableLeg2" type="ColorRect" parent="."]
color = Color(0.65, 0.68, 0.74, 1)
size = Vector2(16, 130)
position = Vector2(768, 530)
[node name="LeadApron" type="ColorRect" parent="."]
color = Color(0.30, 0.40, 0.35, 1)
size = Vector2(60, 120)
position = Vector2(1100, 480)
[node name="CharacterSpawn" type="Marker2D" parent="."]
position = Vector2(200, 490)
[node name="XRayMachine" parent="." instance=ExtResource("2_xraymachine")]
position = Vector2(500, 510)
[node name="PlasterStation" parent="." instance=ExtResource("1_iobj")]
position = Vector2(900, 560)

49
scenes/ui/HUD.tscn Normal file
View File

@@ -0,0 +1,49 @@
[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
[node name="SettingsButton" type="Button" parent="."]
anchors_preset = 1
anchor_left = 1.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 0.0
offset_left = -160.0
offset_top = 16.0
offset_right = -96.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"]
[connection signal="pressed" from="SettingsButton" to="." method="_on_settings_button_pressed"]

103
scenes/ui/SettingsMenu.tscn Normal file
View File

@@ -0,0 +1,103 @@
[gd_scene load_steps=2 format=3 uid="uid://cozypaw_settings"]
[ext_resource type="Script" path="res://scripts/systems/settings_menu.gd" id="1_settings"]
[node name="SettingsMenu" type="CanvasLayer"]
script = ExtResource("1_settings")
visible = false
[node name="Backdrop" type="ColorRect" parent="."]
color = Color(0, 0, 0, 0.7)
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 0.0
offset_top = 0.0
offset_right = 0.0
offset_bottom = 0.0
grow_horizontal = 2
grow_vertical = 2
[node name="Panel" type="Panel" parent="."]
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -200.0
offset_top = -150.0
offset_right = 200.0
offset_bottom = 150.0
grow_horizontal = 2
grow_vertical = 2
[node name="Title" type="Label" parent="Panel"]
offset_left = 0.0
offset_top = 16.0
offset_right = 400.0
offset_bottom = 64.0
horizontal_alignment = 1
text = "⚙"
theme_override_font_sizes/font_size = 32
[node name="MusicLabel" type="Label" parent="Panel"]
offset_left = 24.0
offset_top = 76.0
offset_right = 80.0
offset_bottom = 108.0
text = "♪"
theme_override_font_sizes/font_size = 24
[node name="MusicSlider" type="HSlider" parent="Panel"]
min_value = 0.0
max_value = 1.0
step = 0.05
value = 0.6
offset_left = 88.0
offset_top = 76.0
offset_right = 376.0
offset_bottom = 108.0
[node name="SfxLabel" type="Label" parent="Panel"]
offset_left = 24.0
offset_top = 120.0
offset_right = 80.0
offset_bottom = 152.0
text = "🔊"
theme_override_font_sizes/font_size = 24
[node name="SfxSlider" type="HSlider" parent="Panel"]
min_value = 0.0
max_value = 1.0
step = 0.05
value = 1.0
offset_left = 88.0
offset_top = 120.0
offset_right = 376.0
offset_bottom = 152.0
[node name="ResetButton" type="Button" parent="Panel"]
offset_left = 24.0
offset_top = 172.0
offset_right = 88.0
offset_bottom = 236.0
text = "↺"
theme_override_font_sizes/font_size = 28
[node name="ResetConfirmDialog" type="ConfirmationDialog" parent="Panel"]
title = "Reset?"
dialog_text = "Spielstand löschen?"
[node name="CloseButton" type="Button" parent="Panel"]
offset_left = 312.0
offset_top = 172.0
offset_right = 376.0
offset_bottom = 236.0
text = "✕"
theme_override_font_sizes/font_size = 28
[connection signal="value_changed" from="Panel/MusicSlider" to="." method="_on_music_slider_value_changed"]
[connection signal="value_changed" from="Panel/SfxSlider" to="." method="_on_sfx_slider_value_changed"]
[connection signal="pressed" from="Panel/ResetButton" to="." method="_on_reset_button_pressed"]
[connection signal="confirmed" from="Panel/ResetConfirmDialog" to="." method="_on_reset_confirmed"]
[connection signal="pressed" from="Panel/CloseButton" to="." method="_on_close_button_pressed"]

View 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 prev_player: AudioStreamPlayer = _active_player
_active_player = next_player
var tween: Tween = create_tween()
tween.set_parallel(true)
tween.tween_property(prev_player, "volume_db", linear_to_db(0.0), CROSSFADE_DURATION)
tween.tween_property(next_player, "volume_db", linear_to_db(_music_volume), CROSSFADE_DURATION)
tween.chain().tween_callback(prev_player.stop)
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)

View File

@@ -0,0 +1,65 @@
## 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"
var music_volume: float = 0.6
var sfx_volume: float = 1.0
func has_character_position(id: String) -> bool:
return _character_positions.has(id)
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:
var positions: Dictionary = {}
for key: String in _character_positions:
var pos: Vector2 = _character_positions[key]
positions[key] = [pos.x, pos.y]
return {
"character_positions": positions,
"object_states": _object_states,
"current_room": current_room,
"music_volume": music_volume,
"sfx_volume": sfx_volume,
}
func apply_save_data(data: Dictionary) -> void:
if data.has("character_positions"):
_character_positions = {}
for key: String in data["character_positions"]:
var val: Variant = data["character_positions"][key]
if val is Array and val.size() >= 2:
_character_positions[key] = Vector2(val[0], val[1])
if data.has("object_states"):
_object_states = data["object_states"]
if data.has("current_room"):
current_room = data["current_room"]
if data.has("music_volume"):
music_volume = data["music_volume"]
if data.has("sfx_volume"):
sfx_volume = data["sfx_volume"]

View File

@@ -0,0 +1,40 @@
## 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:
_emit_drag_started(event.position)
else:
_emit_drag_ended(event.position)
elif event is InputEventScreenDrag:
if _is_dragging:
_emit_drag_moved(event.position)
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
_emit_drag_started(event.position)
else:
_emit_drag_ended(event.position)
elif event is InputEventMouseMotion and _is_dragging:
_emit_drag_moved(event.position)
func _emit_drag_started(pos: Vector2) -> void:
_is_dragging = true
drag_started.emit(pos)
func _emit_drag_moved(pos: Vector2) -> void:
drag_moved.emit(pos)
func _emit_drag_ended(pos: Vector2) -> void:
_is_dragging = false
drag_ended.emit(pos)

View File

@@ -0,0 +1,43 @@
## 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:
push_error("SaveManager: cannot open save file for writing")
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: Variant = JSON.parse_string(raw)
if parsed is Dictionary:
GameState.apply_save_data(parsed)
func reset_game() -> void:
if FileAccess.file_exists(SAVE_PATH):
var dir: DirAccess = DirAccess.open("user://")
if dir != null:
dir.remove("savegame.json")
GameState.apply_save_data({})
func _on_state_changed() -> void:
save_game()

View File

@@ -0,0 +1,69 @@
## Character — base class for all playable figures (bunny, cat, etc.).
class_name Character extends Node2D
const _CharacterData = preload("res://scripts/characters/character_data.gd")
signal character_picked_up(character: Character)
signal character_placed(character: Character, position: Vector2)
signal state_changed(new_state: CharacterData.State)
@export var character_id: String = ""
@export var display_name: String = ""
@export var data: CharacterData
var _is_held: bool = false
const _STATE_COLORS: Dictionary = {
CharacterData.State.HEALTHY: Color(0.6, 0.8, 1.0),
CharacterData.State.SICK: Color(0.7, 0.9, 0.7),
CharacterData.State.SLEEPING: Color(0.8, 0.7, 0.95),
CharacterData.State.TIRED: Color(1.0, 0.95, 0.6),
CharacterData.State.PREGNANT: Color(1.0, 0.85, 0.9),
CharacterData.State.BABY: Color(0.9, 0.95, 1.0),
}
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)
if data != null:
_update_visual_state()
func set_state(new_state: CharacterData.State) -> void:
if data == null:
return
data.state = new_state
_update_visual_state()
state_changed.emit(new_state)
func _update_visual_state() -> void:
if data == null:
return
var visual: ColorRect = get_node_or_null("Visual") as ColorRect
if visual == null:
return
var color: Color = _STATE_COLORS.get(data.state, Color(0.6, 0.8, 1.0))
visual.color = color
var ear_left: ColorRect = get_node_or_null("Ears/EarLeft") as ColorRect
var ear_right: ColorRect = get_node_or_null("Ears/EarRight") as ColorRect
if ear_left != null:
ear_left.color = color
if ear_right != null:
ear_right.color = color
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
if data == null or data.id.is_empty():
return
GameState.set_character_position(character_id, global_position)
character_placed.emit(self, global_position)

View File

@@ -0,0 +1,12 @@
## CharacterData — Resource holding all persistent state for one character.
class_name CharacterData extends Resource
enum State { HEALTHY, SICK, SLEEPING, TIRED, PREGNANT, BABY }
enum Species { BUNNY, KITTEN }
@export var id: String = ""
@export var display_name: String = ""
@export var species: Species = Species.BUNNY
@export var state: State = State.HEALTHY
@export var current_floor: int = 0
@export var position: Vector2 = Vector2.ZERO

17
scripts/main/main.gd Normal file
View File

@@ -0,0 +1,17 @@
## Main — scene root: wires up RoomNavigator and restores saved character positions.
extends Node2D
func _ready() -> void:
RoomNavigator.initialize($Camera2D)
SaveManager.load_game()
AudioManager.set_music_volume(GameState.music_volume)
AudioManager.set_sfx_volume(GameState.sfx_volume)
_apply_saved_state()
func _apply_saved_state() -> void:
for character in $Characters.get_children():
if character is Character and character.data != null:
if GameState.has_character_position(character.data.id):
character.global_position = GameState.get_character_position(character.data.id)

View File

@@ -0,0 +1,84 @@
## Ambulance — drives in from off-screen right when Emergency Room is entered; tap to toggle.
class_name Ambulance extends Node2D
const OFFSCREEN_OFFSET: float = 1200.0
const BUTTON_HALF_WIDTH: float = 90.0
const BUTTON_HALF_HEIGHT: float = 50.0
const DRIVE_DURATION: float = 1.2
@export var trigger_floor: int = 0
@export var trigger_room: int = 3
var _parked_x: float = 0.0
var _is_parked: bool = false
var _is_animating: bool = false
func _ready() -> void:
_parked_x = position.x
position.x = _parked_x + OFFSCREEN_OFFSET
RoomNavigator.room_changed.connect(_on_room_changed)
func _exit_tree() -> void:
if RoomNavigator.room_changed.is_connected(_on_room_changed):
RoomNavigator.room_changed.disconnect(_on_room_changed)
func _on_room_changed(floor_index: int, room_index: int) -> void:
if floor_index == trigger_floor and room_index == trigger_room:
if not _is_parked and not _is_animating:
_drive_in()
func _input(event: InputEvent) -> void:
if _is_animating:
return
var screen_pos: Vector2
if event is InputEventScreenTouch and event.pressed:
screen_pos = event.position
elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
screen_pos = event.position
else:
return
var canvas_transform: Transform2D = get_viewport().get_canvas_transform()
var world_pos: Vector2 = canvas_transform.affine_inverse() * screen_pos
var local_pos: Vector2 = to_local(world_pos)
if abs(local_pos.x) <= BUTTON_HALF_WIDTH and abs(local_pos.y) <= BUTTON_HALF_HEIGHT:
if _is_parked:
_drive_out()
else:
_drive_in()
func _drive_in() -> void:
_is_animating = true
_is_parked = false
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_OUT)
tween.set_trans(Tween.TRANS_QUAD)
tween.tween_property(self, "position:x", _parked_x, DRIVE_DURATION)
tween.finished.connect(func() -> void:
_is_parked = true
_is_animating = false
_play_stop_bounce()
)
func _drive_out() -> void:
_is_animating = true
_is_parked = false
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_IN)
tween.set_trans(Tween.TRANS_QUAD)
tween.tween_property(self, "position:x", _parked_x + OFFSCREEN_OFFSET, DRIVE_DURATION)
tween.finished.connect(func() -> void:
_is_animating = false
)
func _play_stop_bounce() -> void:
var tween: Tween = create_tween()
tween.tween_property(self, "scale", Vector2(1.08, 0.94), 0.08)
tween.tween_property(self, "scale", Vector2(0.96, 1.06), 0.08)
tween.tween_property(self, "scale", Vector2(1.0, 1.0), 0.08)

View File

@@ -0,0 +1,34 @@
## ElevatorButton — tappable button that navigates the camera to a target floor.
class_name ElevatorButton extends Node2D
const BUTTON_HALF_SIZE: float = 40.0
@export var target_floor: int = 0
func _input(event: InputEvent) -> void:
var screen_pos: Vector2
if event is InputEventScreenTouch and event.pressed:
screen_pos = event.position
elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
screen_pos = event.position
else:
return
var canvas_transform: Transform2D = get_viewport().get_canvas_transform()
var world_pos: Vector2 = canvas_transform.affine_inverse() * screen_pos
var local_pos: Vector2 = to_local(world_pos)
if abs(local_pos.x) <= BUTTON_HALF_SIZE and abs(local_pos.y) <= BUTTON_HALF_SIZE:
_on_pressed()
func _on_pressed() -> void:
if target_floor < 0:
return
RoomNavigator.go_to_floor(target_floor)
_play_bounce()
func _play_bounce() -> void:
var tween: Tween = create_tween()
tween.tween_property(self, "scale", Vector2(1.15, 1.15), 0.08)
tween.tween_property(self, "scale", Vector2(1.0, 1.0), 0.08)

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

View File

@@ -0,0 +1,40 @@
## NavigationArrow — tappable button that navigates camera to a specific floor and room.
class_name NavigationArrow extends Node2D
const BUTTON_HALF_SIZE: float = 48.0
@export var target_floor: int = 0
@export var target_room: int = 0
@export var label_text: String = ""
func _ready() -> void:
var label: Label = get_node_or_null("Label") as Label
if label != null:
label.text = label_text
func _input(event: InputEvent) -> void:
var screen_pos: Vector2
if event is InputEventScreenTouch and event.pressed:
screen_pos = event.position
elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
screen_pos = event.position
else:
return
var canvas_transform: Transform2D = get_viewport().get_canvas_transform()
var world_pos: Vector2 = canvas_transform.affine_inverse() * screen_pos
var local_pos: Vector2 = to_local(world_pos)
if abs(local_pos.x) <= BUTTON_HALF_SIZE and abs(local_pos.y) <= BUTTON_HALF_SIZE:
_on_pressed()
func _on_pressed() -> void:
RoomNavigator.go_to_room(target_floor, target_room)
_play_bounce()
func _play_bounce() -> void:
var tween: Tween = create_tween()
tween.tween_property(self, "scale", Vector2(1.15, 1.15), 0.08)
tween.tween_property(self, "scale", Vector2(1.0, 1.0), 0.08)

View File

@@ -0,0 +1,76 @@
## XRayMachine — X-ray plate slides out when tapped; viewer lights up during exposure.
class_name XRayMachine extends Node2D
enum State { IDLE, SLIDING_IN, EXPOSING, SLIDING_OUT }
const SLIDE_DURATION: float = 0.8
const EXPOSE_DURATION: float = 1.5
const BUTTON_HALF_WIDTH: float = 50.0
const BUTTON_HALF_HEIGHT: float = 100.0
const PLATE_PARKED_X: float = -50.0
const PLATE_ACTIVE_X: float = 130.0
const VIEWER_LIT_COLOR: Color = Color(0.75, 0.90, 1.0, 1)
const VIEWER_DARK_COLOR: Color = Color(0.06, 0.08, 0.12, 1)
var _state: State = State.IDLE
func _ready() -> void:
var plate: Node2D = get_node_or_null("Plate") as Node2D
if plate != null:
plate.position.x = PLATE_PARKED_X
func _input(event: InputEvent) -> void:
if _state != State.IDLE:
return
var screen_pos: Vector2
if event is InputEventScreenTouch and event.pressed:
screen_pos = event.position
elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
screen_pos = event.position
else:
return
var canvas_transform: Transform2D = get_viewport().get_canvas_transform()
var world_pos: Vector2 = canvas_transform.affine_inverse() * screen_pos
var local_pos: Vector2 = to_local(world_pos)
if abs(local_pos.x) <= BUTTON_HALF_WIDTH and abs(local_pos.y) <= BUTTON_HALF_HEIGHT:
_start_scan()
func _start_scan() -> void:
_state = State.SLIDING_IN
var plate: Node2D = get_node_or_null("Plate") as Node2D
if plate == null:
_state = State.IDLE
return
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_QUAD)
tween.tween_property(plate, "position:x", PLATE_ACTIVE_X, SLIDE_DURATION)
tween.finished.connect(_on_plate_in)
func _on_plate_in() -> void:
_state = State.EXPOSING
var viewer: ColorRect = get_node_or_null("Viewer") as ColorRect
if viewer != null:
viewer.color = VIEWER_LIT_COLOR
var timer: SceneTreeTimer = get_tree().create_timer(EXPOSE_DURATION)
timer.timeout.connect(_start_slide_out)
func _start_slide_out() -> void:
_state = State.SLIDING_OUT
var viewer: ColorRect = get_node_or_null("Viewer") as ColorRect
if viewer != null:
viewer.color = VIEWER_DARK_COLOR
var plate: Node2D = get_node_or_null("Plate") as Node2D
if plate == null:
_state = State.IDLE
return
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_QUAD)
tween.tween_property(plate, "position:x", PLATE_PARKED_X, SLIDE_DURATION)
tween.finished.connect(func() -> void: _state = State.IDLE)

View File

@@ -0,0 +1,81 @@
## 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
const DEFAULT_DRAG_RADIUS: float = 64.0
@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() < DEFAULT_DRAG_RADIUS
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() < DEFAULT_DRAG_RADIUS

26
scripts/systems/hud.gd Normal file
View File

@@ -0,0 +1,26 @@
## HUD — heads-up display with back button, music toggle, and settings access.
class_name HUD extends CanvasLayer
const MUSIC_ON_SYMBOL: String = ""
const MUSIC_OFF_SYMBOL: String = ""
var _music_enabled: bool = true
@onready var _settings_menu: SettingsMenu = get_node_or_null("../SettingsMenu") as SettingsMenu
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 = MUSIC_ON_SYMBOL if _music_enabled else MUSIC_OFF_SYMBOL
func _on_settings_button_pressed() -> void:
if _settings_menu != null:
_settings_menu.show_menu()

View File

@@ -0,0 +1,44 @@
## RoomNavigator — autoload that moves the Camera2D smoothly between hospital floors and rooms.
extends Node
signal room_changed(floor_index: int, room_index: int)
const FLOOR_HEIGHT: float = 720.0
const ROOM_WIDTH: float = 1280.0
const CAMERA_TWEEN_DURATION: float = 0.6
var _current_floor: int = 0
var _current_room: int = 0
var _camera: Camera2D
func initialize(camera: Camera2D) -> void:
_camera = camera
func go_to_floor(floor_index: int) -> void:
go_to_room(floor_index, 0)
func go_to_room(floor_index: int, room_index: int) -> void:
if _camera == null:
return
if floor_index == _current_floor and room_index == _current_room:
return
_current_floor = floor_index
_current_room = room_index
var target_x: float = room_index * ROOM_WIDTH + ROOM_WIDTH * 0.5
var target_y: float = floor_index * -FLOOR_HEIGHT + FLOOR_HEIGHT * 0.5
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_IN_OUT)
tween.set_trans(Tween.TRANS_SINE)
tween.tween_property(_camera, "position", Vector2(target_x, target_y), CAMERA_TWEEN_DURATION)
tween.finished.connect(func() -> void: room_changed.emit(floor_index, room_index))
func get_current_floor() -> int:
return _current_floor
func get_current_room() -> int:
return _current_room

View File

@@ -0,0 +1,45 @@
## SettingsMenu — overlay panel for audio volume and game reset.
class_name SettingsMenu extends CanvasLayer
func _ready() -> void:
visible = false
func show_menu() -> void:
var music_slider: HSlider = get_node_or_null("Panel/MusicSlider") as HSlider
var sfx_slider: HSlider = get_node_or_null("Panel/SfxSlider") as HSlider
if music_slider != null:
music_slider.set_value_no_signal(GameState.music_volume)
if sfx_slider != null:
sfx_slider.set_value_no_signal(GameState.sfx_volume)
visible = true
func hide_menu() -> void:
visible = false
func _on_music_slider_value_changed(value: float) -> void:
AudioManager.set_music_volume(value)
GameState.music_volume = value
func _on_sfx_slider_value_changed(value: float) -> void:
AudioManager.set_sfx_volume(value)
GameState.sfx_volume = value
func _on_reset_button_pressed() -> void:
var dialog: ConfirmationDialog = get_node_or_null("Panel/ResetConfirmDialog") as ConfirmationDialog
if dialog != null:
dialog.popup_centered()
func _on_reset_confirmed() -> void:
SaveManager.reset_game()
get_tree().reload_current_scene()
func _on_close_button_pressed() -> void:
hide_menu()