Add graphical CCS Cases 2 prototype

This commit is contained in:
hz
2026-08-05 18:17:12 -05:00
parent 976cb70d4a
commit d76b2c809e
67 changed files with 2393 additions and 0 deletions
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://f0q6fjw8li34"
path="res://.godot/imported/CHIPS-01.wav-582f2a2445236f1807d7d8100300970e.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/CHIPS-01.wav"
dest_files=["res://.godot/imported/CHIPS-01.wav-582f2a2445236f1807d7d8100300970e.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://deq0xy3ap7u21"
path="res://.godot/imported/CHIPS-02.wav-1b7c7e1f05d4c32bfd5f1a0045e00062.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/CHIPS-02.wav"
dest_files=["res://.godot/imported/CHIPS-02.wav-1b7c7e1f05d4c32bfd5f1a0045e00062.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cp28h2en40qkj"
path="res://.godot/imported/CHIPS-03.wav-8b6513f8b6cb9a7a7db482c04e7994a8.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/CHIPS-03.wav"
dest_files=["res://.godot/imported/CHIPS-03.wav-8b6513f8b6cb9a7a7db482c04e7994a8.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://d3lj71hfhlvoe"
path="res://.godot/imported/CHIPS-04.wav-149d7eaf2a33e2747d2f6fcbec4af0fe.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/CHIPS-04.wav"
dest_files=["res://.godot/imported/CHIPS-04.wav-149d7eaf2a33e2747d2f6fcbec4af0fe.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bbyksq0tjsw7t"
path="res://.godot/imported/CLICK-01.wav-12c8c2888253f4fe1c2b6cd3ed702340.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/CLICK-01.wav"
dest_files=["res://.godot/imported/CLICK-01.wav-12c8c2888253f4fe1c2b6cd3ed702340.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://btj7os30hhbqt"
path="res://.godot/imported/CLICK-02.wav-d3e48ccc9b37871e4c1f58ad7a98c6e2.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/CLICK-02.wav"
dest_files=["res://.godot/imported/CLICK-02.wav-d3e48ccc9b37871e4c1f58ad7a98c6e2.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://sg7gexab0vvo"
path="res://.godot/imported/CLICK-03.wav-4bbeb40624144445710defe3f04dff22.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/CLICK-03.wav"
dest_files=["res://.godot/imported/CLICK-03.wav-4bbeb40624144445710defe3f04dff22.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dshj5v2aihbb0"
path="res://.godot/imported/CLICK-04.wav-156128b0878fd0ff55b94b89c4fc9c6f.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/CLICK-04.wav"
dest_files=["res://.godot/imported/CLICK-04.wav-156128b0878fd0ff55b94b89c4fc9c6f.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bf2gtxwjd6eox"
path="res://.godot/imported/DEEPFLAP-01.wav-df2c3f7c79142841e98da9c5a921057d.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/DEEPFLAP-01.wav"
dest_files=["res://.godot/imported/DEEPFLAP-01.wav-df2c3f7c79142841e98da9c5a921057d.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://phq30ylu5bo3"
path="res://.godot/imported/FAN-WHIR-01.wav-95005855ffe41ed39c5614355bed9e5b.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/FAN-WHIR-01.wav"
dest_files=["res://.godot/imported/FAN-WHIR-01.wav-95005855ffe41ed39c5614355bed9e5b.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dhfgv6iuedgax"
path="res://.godot/imported/LOUDCLICK-01.wav-74ce36dff26bf8d7a8977e58e7442165.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/LOUDCLICK-01.wav"
dest_files=["res://.godot/imported/LOUDCLICK-01.wav-74ce36dff26bf8d7a8977e58e7442165.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://ufi3xj7myijj"
path="res://.godot/imported/MAGNET-THWAP-01.wav-841ae8e9cf025cbca12c9a7dd062cbf7.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/MAGNET-THWAP-01.wav"
dest_files=["res://.godot/imported/MAGNET-THWAP-01.wav-841ae8e9cf025cbca12c9a7dd062cbf7.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://brpia062tfuw7"
path="res://.godot/imported/THUMP-01.wav-96534e39f9abaf0b6479d088cb5d44df.sample"
[deps]
source_file="res://assets/ccs_cases_2/audio/THUMP-01.wav"
dest_files=["res://.godot/imported/THUMP-01.wav-96534e39f9abaf0b6479d088cb5d44df.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://20mgki4ctdt2"
path="res://.godot/imported/chat-bubble.png-deb8723b28b12c2c18ca72693079a109.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/ccs_cases_2/images/doctor/chat-bubble.png"
dest_files=["res://.godot/imported/chat-bubble.png-deb8723b28b12c2c18ca72693079a109.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://coe5g5bmhx4da"
path="res://.godot/imported/clock.png-1472633dccc280df9899fa995cf51944.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/ccs_cases_2/images/doctor/clock.png"
dest_files=["res://.godot/imported/clock.png-1472633dccc280df9899fa995cf51944.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 788 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ciyh3pu7oheu7"
path="res://.godot/imported/dialogue-choice-hover.png-74920c58e8a5f4b150faa8f67c77995b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/ccs_cases_2/images/doctor/dialogue-choice-hover.png"
dest_files=["res://.godot/imported/dialogue-choice-hover.png-74920c58e8a5f4b150faa8f67c77995b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 866 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cmgb8fmmsu746"
path="res://.godot/imported/dialogue-choice-normal.png-474d0ee8b44edafede7ea4bf1717544e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/ccs_cases_2/images/doctor/dialogue-choice-normal.png"
dest_files=["res://.godot/imported/dialogue-choice-normal.png-474d0ee8b44edafede7ea4bf1717544e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dwkq4b24bxaj3"
path="res://.godot/imported/dialogue-choice-pressed.png-ddffca745e1cd0dd9cea64607a4049cb.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/ccs_cases_2/images/doctor/dialogue-choice-pressed.png"
dest_files=["res://.godot/imported/dialogue-choice-pressed.png-ddffca745e1cd0dd9cea64607a4049cb.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://blvljbm5ylu14"
path="res://.godot/imported/office-backdrop.png-c4ff7cf7fa21dca6db6c6e4cf3183251.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/ccs_cases_2/images/doctor/office-backdrop.png"
dest_files=["res://.godot/imported/office-backdrop.png-c4ff7cf7fa21dca6db6c6e4cf3183251.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d4fyqjakmkr1g"
path="res://.godot/imported/patient-44-white-male.png-01861ff1e5efd318a354f28d52214f12.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/ccs_cases_2/images/doctor/patient-44-white-male.png"
dest_files=["res://.godot/imported/patient-44-white-male.png-01861ff1e5efd318a354f28d52214f12.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d3bo8n2dxx6h4"
path="res://.godot/imported/lever-sprite-sheet.png-28bddeaacbfc9f3064287f9324158563.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/ccs_cases_2/images/slot/lever-sprite-sheet.png"
dest_files=["res://.godot/imported/lever-sprite-sheet.png-28bddeaacbfc9f3064287f9324158563.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://u5gmnonww4s4"
path="res://.godot/imported/numbers.png-f7b7591684a12e158214a353335df9a3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/ccs_cases_2/images/slot/numbers.png"
dest_files=["res://.godot/imported/numbers.png-f7b7591684a12e158214a353335df9a3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://blq8wype071ou"
path="res://.godot/imported/reel-frame.png-45e4b5f53fe075b210e4af141b08081c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/ccs_cases_2/images/slot/reel-frame.png"
dest_files=["res://.godot/imported/reel-frame.png-45e4b5f53fe075b210e4af141b08081c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ujbkwj0i8e2v"
path="res://.godot/imported/symbols.png-d6e1a2099254ac8842c1e175b464450a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/ccs_cases_2/images/slot/symbols.png"
dest_files=["res://.godot/imported/symbols.png-d6e1a2099254ac8842c1e175b464450a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+158
View File
@@ -0,0 +1,158 @@
[gd_scene load_steps=8 format=3]
[ext_resource type="Texture2D" path="res://assets/ccs_cases_2/images/doctor/office-backdrop.png" id="1_backdrop"]
[ext_resource type="Script" path="res://scripts/ui/ccs_cases_2/doctor_office.gd" id="2_script"]
[ext_resource type="Texture2D" path="res://assets/ccs_cases_2/images/doctor/clock.png" id="3_face"]
[ext_resource type="Texture2D" path="res://assets/ccs_cases_2/images/doctor/patient-44-white-male.png" id="4_patient"]
[ext_resource type="Shader" path="res://shaders/ccs_cases_2/patient_idle.gdshader" id="5_patient_fit"]
[ext_resource type="Texture2D" path="res://assets/ccs_cases_2/images/doctor/chat-bubble.png" id="6_chat_bubble"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_patient_fit"]
shader = ExtResource("5_patient_fit")
shader_parameter/patient_scale = 0.82
shader_parameter/anchor = Vector2(0.5, 0.985)
shader_parameter/patient_offset = Vector2(0, 0)
shader_parameter/breathing_speed = 1.0
shader_parameter/breathing_amount = 0.012
shader_parameter/blink_amount = 0.0
[node name="DoctorScene" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("2_script")
[node name="Backdrop" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
texture = ExtResource("1_backdrop")
expand_mode = 1
stretch_mode = 6
[node name="Patient" type="TextureRect" parent="."]
material = SubResource("ShaderMaterial_patient_fit")
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
texture = ExtResource("4_patient")
expand_mode = 1
stretch_mode = 6
metadata/age = 44
metadata/sex = "male"
metadata/ethnicity = "white"
metadata/demographic_label = "44-year-old white male"
[node name="ClockMount" type="Control" parent="."]
z_index = 10
layout_mode = 1
anchors_preset = 0
scale = Vector2(0.75, 0.75)
anchor_left = 0.695
anchor_top = 0.145
anchor_right = 0.695
anchor_bottom = 0.145
mouse_filter = 2
[node name="Face" type="Sprite2D" parent="ClockMount"]
position = Vector2(0, 1.2)
scale = Vector2(0.095, 0.095)
texture = ExtResource("3_face")
[node name="HourHand" type="Node2D" parent="ClockMount"]
[node name="Shape" type="Polygon2D" parent="ClockMount/HourHand"]
polygon = PackedVector2Array(-3.2, 5, -2.5, -23, 0, -29, 2.5, -23, 3.2, 5)
color = Color(0.035, 0.055, 0.08, 1)
antialiased = true
[node name="MinuteHand" type="Node2D" parent="ClockMount"]
[node name="Shape" type="Polygon2D" parent="ClockMount/MinuteHand"]
polygon = PackedVector2Array(-2.4, 6, -1.7, -34, 0, -40, 1.7, -34, 2.4, 6)
color = Color(0.035, 0.055, 0.08, 1)
antialiased = true
[node name="SecondHand" type="Node2D" parent="ClockMount"]
[node name="Shape" type="Polygon2D" parent="ClockMount/SecondHand"]
polygon = PackedVector2Array(-0.9, 8, -0.8, -38, 0, -43, 0.8, -38, 0.9, 8)
color = Color(0.55, 0.09, 0.065, 1)
antialiased = true
[node name="CenterCap" type="Polygon2D" parent="ClockMount"]
polygon = PackedVector2Array(3.8, 0, 3.29, 1.9, 1.9, 3.29, 0, 3.8, -1.9, 3.29, -3.29, 1.9, -3.8, 0, -3.29, -1.9, -1.9, -3.29, 0, -3.8, 1.9, -3.29, 3.29, -1.9)
color = Color(0.04, 0.055, 0.075, 1)
antialiased = true
[node name="CenterPin" type="Polygon2D" parent="ClockMount"]
polygon = PackedVector2Array(1.8, 0, 1.27, 1.27, 0, 1.8, -1.27, 1.27, -1.8, 0, -1.27, -1.27, 0, -1.8, 1.27, -1.27)
color = Color(0.9, 0.64, 0.19, 1)
antialiased = true
[node name="DialogueBubble" type="Control" parent="."]
z_index = 30
layout_mode = 1
anchors_preset = 0
anchor_left = 0.51
anchor_top = 0.055
anchor_right = 0.51
anchor_bottom = 0.055
offset_right = 420.0
offset_bottom = 140.0
mouse_filter = 2
[node name="Background" type="TextureRect" parent="DialogueBubble"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
self_modulate = Color(0.92, 0.89, 0.82, 0.86)
texture = ExtResource("6_chat_bubble")
expand_mode = 1
stretch_mode = 0
[node name="Text" type="Label" parent="DialogueBubble"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 76.0
offset_top = 31.0
offset_right = -48.0
offset_bottom = -43.0
grow_horizontal = 2
grow_vertical = 2
theme_override_colors/font_color = Color(0.055, 0.105, 0.145, 1)
theme_override_font_sizes/font_size = 22
text = "Hello, doctor."
autowrap_mode = 2
vertical_alignment = 1
[node name="DialogueChoices" type="VBoxContainer" parent="."]
z_index = 29
layout_mode = 1
anchors_preset = 0
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -210.0
offset_top = -206.0
offset_right = 210.0
offset_bottom = -4.0
theme_override_constants/separation = -8
+319
View File
@@ -0,0 +1,319 @@
[gd_scene format=3 uid="uid://bq3huk364skd3"]
[ext_resource type="Script" path="res://scripts/ui/ccs_cases_2/slot_machine.gd" id="1_qtu10"]
[ext_resource type="Shader" path="res://shaders/ccs_cases_2/reel_spin.gdshader" id="2_ee4bf"]
[ext_resource type="Texture2D" path="res://assets/ccs_cases_2/images/slot/symbols.png" id="3_ee4bf"]
[ext_resource type="Texture2D" path="res://assets/ccs_cases_2/images/slot/numbers.png" id="3_kbped"]
[ext_resource type="Texture2D" path="res://assets/ccs_cases_2/images/slot/reel-frame.png" id="4_reel_bg"]
[ext_resource type="Texture2D" path="res://assets/ccs_cases_2/images/slot/lever-sprite-sheet.png" id="6_lever_sheet"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_bvwqe"]
shader = ExtResource("2_ee4bf")
shader_parameter/tex = ExtResource("3_ee4bf")
shader_parameter/n_rows = 6.0
shader_parameter/y_offset = 0.0
[sub_resource type="ShaderMaterial" id="ShaderMaterial_fj5cn"]
shader = ExtResource("2_ee4bf")
shader_parameter/tex = ExtResource("3_ee4bf")
shader_parameter/n_rows = 6.0
shader_parameter/y_offset = 0.0
[sub_resource type="ShaderMaterial" id="ShaderMaterial_iy58a"]
shader = ExtResource("2_ee4bf")
shader_parameter/tex = ExtResource("3_ee4bf")
shader_parameter/n_rows = 6.0
shader_parameter/y_offset = 0.0
shader_parameter/x_offset = -0.018
[sub_resource type="ShaderMaterial" id="ShaderMaterial_6a6wx"]
shader = ExtResource("2_ee4bf")
shader_parameter/tex = ExtResource("3_kbped")
shader_parameter/n_rows = 10.0
shader_parameter/y_offset = 0.0
[sub_resource type="ShaderMaterial" id="ShaderMaterial_ee4bf"]
shader = ExtResource("2_ee4bf")
shader_parameter/tex = ExtResource("3_kbped")
shader_parameter/n_rows = 10.0
shader_parameter/y_offset = 0.0
[sub_resource type="ShaderMaterial" id="ShaderMaterial_kbped"]
shader = ExtResource("2_ee4bf")
shader_parameter/tex = ExtResource("3_kbped")
shader_parameter/n_rows = 10.0
shader_parameter/y_offset = 0.0
[sub_resource type="ShaderMaterial" id="ShaderMaterial_sky72"]
shader = ExtResource("2_ee4bf")
shader_parameter/tex = ExtResource("3_kbped")
shader_parameter/n_rows = 10.0
shader_parameter/y_offset = 0.0
[sub_resource type="AtlasTexture" id="AtlasTexture_reel_divider"]
atlas = ExtResource("4_reel_bg")
region = Rect2(24, 72, 56, 880)
[sub_resource type="AtlasTexture" id="AtlasTexture_smooth_top"]
atlas = ExtResource("4_reel_bg")
region = Rect2(100, 26, 824, 44)
[sub_resource type="AtlasTexture" id="AtlasTexture_smooth_bottom"]
atlas = ExtResource("4_reel_bg")
region = Rect2(100, 954, 824, 44)
[sub_resource type="AtlasTexture" id="AtlasTexture_lever_0"]
atlas = ExtResource("6_lever_sheet")
region = Rect2(0, 0, 307, 1024)
[sub_resource type="AtlasTexture" id="AtlasTexture_lever_1"]
atlas = ExtResource("6_lever_sheet")
region = Rect2(307, 0, 307, 1024)
[sub_resource type="AtlasTexture" id="AtlasTexture_lever_2"]
atlas = ExtResource("6_lever_sheet")
region = Rect2(614, 0, 307, 1024)
[sub_resource type="AtlasTexture" id="AtlasTexture_lever_3"]
atlas = ExtResource("6_lever_sheet")
region = Rect2(921, 0, 307, 1024)
[sub_resource type="AtlasTexture" id="AtlasTexture_lever_4"]
atlas = ExtResource("6_lever_sheet")
region = Rect2(1228, 0, 308, 1024)
[sub_resource type="SpriteFrames" id="SpriteFrames_lever"]
animations = [{
"frames": [{"duration": 1.0, "texture": SubResource("AtlasTexture_lever_0")}, {"duration": 1.0, "texture": SubResource("AtlasTexture_lever_1")}, {"duration": 1.0, "texture": SubResource("AtlasTexture_lever_2")}, {"duration": 1.0, "texture": SubResource("AtlasTexture_lever_3")}, {"duration": 1.0, "texture": SubResource("AtlasTexture_lever_4")}],
"loop": false,
"name": &"pull",
"speed": 12.0
}]
[node name="Control" type="Control" unique_id=1279508523 node_paths=PackedStringArray("icon_reels", "number_reels")]
layout_mode = 3
anchors_preset = 0
offset_right = 1500.0
offset_bottom = 800.0
script = ExtResource("1_qtu10")
icon_reels = [NodePath("Spinner1"), NodePath("Spinner2"), NodePath("Spinner3")]
number_reels = [NodePath("Numbers1"), NodePath("Numbers2"), NodePath("Numbers3"), NodePath("Numbers4")]
[node name="ReelBackground1" type="TextureRect" parent="."]
z_index = -2
layout_mode = 0
offset_left = 315.0
offset_top = 276.0
offset_right = 646.0
offset_bottom = 607.0
mouse_filter = 2
texture = ExtResource("4_reel_bg")
expand_mode = 1
[node name="ReelBackground2" type="TextureRect" parent="."]
z_index = -2
layout_mode = 0
offset_left = 594.0
offset_top = 276.0
offset_right = 925.0
offset_bottom = 607.0
mouse_filter = 2
texture = ExtResource("4_reel_bg")
expand_mode = 1
[node name="ReelBackground3" type="TextureRect" parent="."]
z_index = -2
layout_mode = 0
offset_left = 873.0
offset_top = 276.0
offset_right = 1204.0
offset_bottom = 607.0
mouse_filter = 2
texture = ExtResource("4_reel_bg")
expand_mode = 1
[node name="ReelWhite1" type="ColorRect" parent="."]
z_index = -1
layout_mode = 0
offset_left = 341.0
offset_top = 301.0
offset_right = 620.0
offset_bottom = 580.0
mouse_filter = 2
[node name="ReelWhite2" type="ColorRect" parent="."]
z_index = -1
layout_mode = 0
offset_left = 620.0
offset_top = 301.0
offset_right = 899.0
offset_bottom = 580.0
mouse_filter = 2
[node name="ReelWhite3" type="ColorRect" parent="."]
z_index = -1
layout_mode = 0
offset_left = 899.0
offset_top = 301.0
offset_right = 1178.0
offset_bottom = 580.0
mouse_filter = 2
[node name="Spinner1" type="ColorRect" parent="." unique_id=813668141]
texture_filter = 2
material = SubResource("ShaderMaterial_bvwqe")
layout_mode = 0
offset_left = 341.0
offset_top = 301.0
offset_right = 620.0
offset_bottom = 580.0
[node name="Spinner2" type="ColorRect" parent="." unique_id=203022686]
material = SubResource("ShaderMaterial_fj5cn")
layout_mode = 0
offset_left = 620.0
offset_top = 301.0
offset_right = 899.0
offset_bottom = 580.0
[node name="Spinner3" type="ColorRect" parent="." unique_id=409654364]
material = SubResource("ShaderMaterial_iy58a")
layout_mode = 0
offset_left = 899.0
offset_top = 301.0
offset_right = 1178.0
offset_bottom = 580.0
[node name="MiddleReelLeftDivider" type="TextureRect" parent="."]
z_index = 1
layout_mode = 0
offset_left = 610.0
offset_top = 301.0
offset_right = 630.0
offset_bottom = 580.0
mouse_filter = 2
texture = SubResource("AtlasTexture_reel_divider")
expand_mode = 1
[node name="MiddleReelRightDivider" type="TextureRect" parent="."]
z_index = 1
layout_mode = 0
offset_left = 889.0
offset_top = 301.0
offset_right = 909.0
offset_bottom = 580.0
mouse_filter = 2
texture = SubResource("AtlasTexture_reel_divider")
expand_mode = 1
[node name="ContinuousTopTrim" type="TextureRect" parent="."]
z_index = 2
layout_mode = 0
offset_left = 323.0
offset_top = 276.0
offset_right = 1196.0
offset_bottom = 301.0
mouse_filter = 2
texture = SubResource("AtlasTexture_smooth_top")
expand_mode = 1
[node name="ContinuousBottomTrim" type="TextureRect" parent="."]
z_index = 2
layout_mode = 0
offset_left = 323.0
offset_top = 580.0
offset_right = 1196.0
offset_bottom = 607.0
mouse_filter = 2
texture = SubResource("AtlasTexture_smooth_bottom")
expand_mode = 1
[node name="LeverPivot" type="Node2D" parent="."]
z_index = 5
position = Vector2(1202, 592)
[node name="LeverSprite" type="AnimatedSprite2D" parent="LeverPivot"]
position = Vector2(19, -105)
scale = Vector2(0.8, 0.8)
sprite_frames = SubResource("SpriteFrames_lever")
animation = &"pull"
[node name="LeverButton" type="Button" parent="."]
z_index = 6
layout_mode = 0
offset_left = 1107.0
offset_top = 257.0
offset_right = 1287.0
offset_bottom = 602.0
mouse_default_cursor_shape = 2
focus_mode = 0
self_modulate = Color(1, 1, 1, 0)
flat = true
tooltip_text = "Pull lever"
[node name="Numbers1" type="ColorRect" parent="." unique_id=2125838097]
texture_filter = 2
material = SubResource("ShaderMaterial_6a6wx")
layout_mode = 0
offset_left = 1092.0
offset_top = 110.0
offset_right = 1167.0
offset_bottom = 185.0
[node name="Numbers2" type="ColorRect" parent="." unique_id=1145238072]
texture_filter = 2
material = SubResource("ShaderMaterial_ee4bf")
layout_mode = 0
offset_left = 1171.0
offset_top = 110.0
offset_right = 1246.0
offset_bottom = 185.0
[node name="Numbers3" type="ColorRect" parent="." unique_id=1321048024]
texture_filter = 2
material = SubResource("ShaderMaterial_kbped")
layout_mode = 0
offset_left = 1250.0
offset_top = 110.0
offset_right = 1325.0
offset_bottom = 185.0
[node name="Numbers4" type="ColorRect" parent="." unique_id=1865653938]
texture_filter = 2
material = SubResource("ShaderMaterial_sky72")
layout_mode = 0
offset_left = 1329.0
offset_top = 110.0
offset_right = 1404.0
offset_bottom = 185.0
[node name="TestCounterButton" type="Button" parent="."]
layout_mode = 0
offset_left = 1092.0
offset_top = 202.0
offset_right = 1353.0
offset_bottom = 252.0
text = "Test Counter Setting"
[node name="ContinueButton" type="Button" parent="."]
layout_mode = 0
offset_left = 400.0
offset_top = 110.0
offset_right = 620.0
offset_bottom = 160.0
mouse_default_cursor_shape = 2
text = "Continue"
[node name="SpinResultLabel" type="Label" parent="."]
layout_mode = 0
offset_left = 683.0
offset_top = 744.0
offset_right = 913.0
offset_bottom = 784.0
text = "Ready"
horizontal_alignment = 1
vertical_alignment = 1
[connection signal="pressed" from="LeverButton" to="." method="pull_lever"]
[connection signal="pressed" from="TestCounterButton" to="." method="test_counter_setting"]
[connection signal="pressed" from="ContinueButton" to="." method="go_to_doctor_scene"]
+4
View File
@@ -5,6 +5,10 @@ static func progression_enabled() -> bool:
return str(ProjectSettings.get_setting("step3/deployment_environment", "development")).to_lower() == "uat"
static func ccs_cases_2_enabled() -> bool:
return str(ProjectSettings.get_setting("step3/deployment_environment", "development")).to_lower() == "uat"
static func is_progression_reward(reason_code: String) -> bool:
return reason_code in [
"mastery_practicing",
+161
View File
@@ -0,0 +1,161 @@
extends Control
signal dialogue_choice_selected(index: int, text: String)
const CHOICE_NORMAL := preload("res://assets/ccs_cases_2/images/doctor/dialogue-choice-normal.png")
const CHOICE_HOVER := preload("res://assets/ccs_cases_2/images/doctor/dialogue-choice-hover.png")
const CHOICE_PRESSED := preload("res://assets/ccs_cases_2/images/doctor/dialogue-choice-pressed.png")
const CHOICE_CROP := Rect2(80.0, 200.0, 1610.0, 485.0)
@onready var hour_hand: Node2D = $ClockMount/HourHand
@onready var minute_hand: Node2D = $ClockMount/MinuteHand
@onready var second_hand: Node2D = $ClockMount/SecondHand
@onready var patient: TextureRect = $Patient
@onready var dialogue_bubble: Control = $DialogueBubble
@onready var dialogue_text: Label = $DialogueBubble/Text
@onready var choice_container: VBoxContainer = $DialogueChoices
var blink_tween: Tween
var dialogue_tween: Tween
func _ready() -> void:
_update_clock()
_schedule_patient_blink()
show_patient_dialogue("Hello, doctor.")
show_dialogue_choices([
"What brings you in today?",
"How are you feeling today?",
])
func _process(_delta: float) -> void:
_update_clock()
func _update_clock() -> void:
var time := Time.get_time_dict_from_system()
var unix_time := Time.get_unix_time_from_system()
var second := float(time.second) + fmod(unix_time, 1.0)
var minute := float(time.minute) + second / 60.0
var hour := float(time.hour % 12) + minute / 60.0
hour_hand.rotation = TAU * hour / 12.0
minute_hand.rotation = TAU * minute / 60.0
second_hand.rotation = TAU * second / 60.0
func _schedule_patient_blink() -> void:
blink_tween = create_tween()
blink_tween.tween_interval(randf_range(2.8, 5.2))
blink_tween.tween_method(_set_patient_blink, 0.0, 1.0, 0.075).set_trans(
Tween.TRANS_SINE
).set_ease(Tween.EASE_IN)
blink_tween.tween_interval(0.045)
blink_tween.tween_method(_set_patient_blink, 1.0, 0.0, 0.10).set_trans(
Tween.TRANS_SINE
).set_ease(Tween.EASE_OUT)
blink_tween.tween_callback(_schedule_patient_blink)
func _set_patient_blink(amount: float) -> void:
patient.material.set_shader_parameter("blink_amount", amount)
func show_patient_dialogue(text: String, characters_per_second: float = 35.0) -> void:
if dialogue_tween and dialogue_tween.is_valid():
dialogue_tween.kill()
dialogue_text.text = text
dialogue_text.visible_ratio = 0.0
dialogue_bubble.visible = true
_resize_dialogue_bubble()
var reveal_duration := float(text.length()) / maxf(characters_per_second, 1.0)
dialogue_tween = create_tween()
dialogue_tween.tween_property(dialogue_text, "visible_ratio", 1.0, reveal_duration)
func hide_patient_dialogue() -> void:
if dialogue_tween and dialogue_tween.is_valid():
dialogue_tween.kill()
dialogue_bubble.visible = false
func is_patient_dialogue_visible() -> bool:
return dialogue_bubble.visible
func _resize_dialogue_bubble() -> void:
# Label wrapping is calculated from the fixed text width immediately after
# assignment, so explicit newlines and automatic wraps both affect height.
var line_count := maxi(dialogue_text.get_line_count(), 1)
var target_height := clampf(110.0 + line_count * 30.0, 140.0, 320.0)
dialogue_bubble.size.y = target_height
func show_dialogue_choices(options: Array[String]) -> void:
hide_dialogue_choices()
var stack_height := options.size() * 97.0 + 8.0
choice_container.offset_top = -4.0 - stack_height
for index in options.size():
var option_text := options[index]
var button := TextureButton.new()
button.custom_minimum_size = Vector2(420.0, 105.0)
button.ignore_texture_size = true
button.stretch_mode = TextureButton.STRETCH_SCALE
button.texture_normal = _crop_choice_texture(CHOICE_NORMAL)
button.texture_hover = _crop_choice_texture(CHOICE_HOVER)
button.texture_pressed = _crop_choice_texture(CHOICE_PRESSED)
button.self_modulate = Color(1.0, 1.0, 1.0, 0.86)
button.focus_mode = Control.FOCUS_ALL
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
button.mouse_entered.connect(_on_choice_hover.bind(button, true))
button.mouse_exited.connect(_on_choice_hover.bind(button, false))
button.button_down.connect(_on_choice_pressed_visual.bind(button, true))
button.button_up.connect(_on_choice_pressed_visual.bind(button, false))
button.pressed.connect(_on_dialogue_choice_pressed.bind(index, option_text))
var label := Label.new()
label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
label.offset_left = 48.0
label.offset_top = 14.0
label.offset_right = -48.0
label.offset_bottom = -14.0
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
label.add_theme_color_override("font_color", Color(0.055, 0.105, 0.145))
label.add_theme_font_size_override("font_size", 19)
label.text = option_text
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
button.add_child(label)
choice_container.add_child(button)
choice_container.visible = not options.is_empty()
func hide_dialogue_choices() -> void:
for child in choice_container.get_children():
child.queue_free()
choice_container.visible = false
func _crop_choice_texture(source: Texture2D) -> AtlasTexture:
var cropped := AtlasTexture.new()
cropped.atlas = source
cropped.region = CHOICE_CROP
cropped.filter_clip = true
return cropped
func _on_choice_hover(button: TextureButton, hovered: bool) -> void:
button.self_modulate = (
Color(1.08, 1.05, 0.96, 0.94)
if hovered
else Color(1.0, 1.0, 1.0, 0.86)
)
func _on_choice_pressed_visual(button: TextureButton, pressed: bool) -> void:
button.position.y += 2.0 if pressed else -2.0
func _on_dialogue_choice_pressed(index: int, option_text: String) -> void:
dialogue_choice_selected.emit(index, option_text)
hide_dialogue_choices()
@@ -0,0 +1 @@
uid://dq6ty24drp82o
+770
View File
@@ -0,0 +1,770 @@
extends Control
signal number_change_finished(value: int)
signal spin_finished(symbols: Array[int], payout_multiplier: float)
signal continue_requested
@export var icon_reels: Array[Control]
@export var number_reels: Array[Control]
@export_range(-40.0, 0.0, 0.5) var click_volume_db := -11.0
@export_range(0.0, 0.2, 0.005) var pitch_variation := 0.055
@export_range(-40.0, -10.0, 0.5) var reel_whir_volume_db := -30.0
@export_range(0.0, 1.5, 0.05) var extra_icon_reel_stop_delay := 0.4
@export_range(0.1, 2.0, 0.05) var number_roll_duration := 0.6
@export_category("Slot Math")
@export_range(0.0, 1.0, 0.001) var target_rtp := 0.96
@export var symbol_names := PackedStringArray([
"Star", "Diamond", "Potion", "Shield", "Chips", "Gift"
])
@export var symbol_payout_min := PackedInt32Array([1, 50, 400, 300, 5, 0])
@export var symbol_payout_max := PackedInt32Array([1, 100, 400, 300, 10, 0])
@export var symbol_rtp_shares := PackedFloat32Array([
0.25686958, 0.15625, 0.16666667, 0.15625, 0.20833333, 0.05563042
])
@export var loss_symbol_weights := PackedFloat32Array([1.0, 2.0, 4.0, 6.0, 10.0, 3.0])
@export_range(0.0, 1.0, 0.01) var near_miss_rate := 0.15
@export_category("Early Session Study")
@export_range(0, 20, 1) var early_boost_spin_count := 5
@export_range(0.0, 1.0, 0.01) var early_blue_stack_chance := 0.20
const CLICK_SOUNDS: Array[AudioStream] = [
preload("res://assets/ccs_cases_2/audio/CLICK-01.wav"),
preload("res://assets/ccs_cases_2/audio/CLICK-02.wav"),
preload("res://assets/ccs_cases_2/audio/CLICK-03.wav"),
preload("res://assets/ccs_cases_2/audio/CLICK-04.wav"),
]
# The recordings contain different amounts of silence before their transients.
# Starting here preserves a tiny lead-in while keeping the impact synchronized.
const CLICK_START_OFFSETS: Array[float] = [0.040, 0.039, 0.052, 0.061]
const STOP_CLICK: AudioStream = preload("res://assets/ccs_cases_2/audio/LOUDCLICK-01.wav")
# This recording has about 167 ms of silence before its impact.
const STOP_CLICK_START_OFFSET := 0.167
const STOP_FLAP: AudioStream = preload("res://assets/ccs_cases_2/audio/DEEPFLAP-01.wav")
# The deep layer has about 159 ms of silence before its transient.
const STOP_FLAP_START_OFFSET := 0.159
const SPIN_START_THUMP: AudioStream = preload("res://assets/ccs_cases_2/audio/THUMP-01.wav")
const LEVER_THWAP: AudioStream = preload("res://assets/ccs_cases_2/audio/MAGNET-THWAP-01.wav")
const LEVER_THWAP_START_OFFSET := 0.110
const REEL_WHIR: AudioStream = preload("res://assets/ccs_cases_2/audio/FAN-WHIR-01.wav")
const REEL_WHIR_START_OFFSET := 1.72
const CHIP_SOUNDS: Array[AudioStream] = [
preload("res://assets/ccs_cases_2/audio/CHIPS-01.wav"),
preload("res://assets/ccs_cases_2/audio/CHIPS-02.wav"),
preload("res://assets/ccs_cases_2/audio/CHIPS-03.wav"),
preload("res://assets/ccs_cases_2/audio/CHIPS-04.wav"),
]
const CHIP_START_OFFSETS: Array[float] = [0.224, 0.154, 0.201, 0.261]
const AUDIO_POOL_SIZE := 16
const SYMBOL_COUNT := 6
const SINGLE_COIN_SYMBOL := 0
const BLUE_COIN_STACK_SYMBOL := 4
const PRESENT_SYMBOL := 5
const LEVER_FRAME_POSITIONS: Array[Vector2] = [
Vector2(19.0, -105.0),
Vector2(23.0, -106.0),
Vector2(12.0, -82.0),
Vector2(12.0, -90.0),
Vector2(5.0, -90.0),
]
var values: Array[int] = []
var reel_tweens: Array[Tween] = []
var number_tweens: Array[Tween] = []
var number_value := 100
var number_animation_active := false
var audio_pool: Array[AudioStreamPlayer] = []
var next_audio_player := 0
var last_click_sound := -1
var last_chip_sound := -1
var chip_count_tween: Tween
var reel_whir_player: AudioStreamPlayer
var reel_whir_tween: Tween
var win_effect_tween: Tween
var spin_in_progress := false
var stopped_icon_reels := 0
var selected_payout := 0.0
var selected_near_miss := false
var selected_early_session_boost := false
var session_spins_started := 0
var early_stack_bonus_consumed := false
var rng := RandomNumberGenerator.new()
var lever_tween: Tween
var lever_foley_tween: Tween
var lever_animating := false
@onready var spin_result_label: Label = $SpinResultLabel
@onready var lever_pivot: Node2D = $LeverPivot
@onready var lever_sprite: AnimatedSprite2D = $LeverPivot/LeverSprite
@onready var lever_button: Button = $LeverButton
func _ready() -> void:
rng.randomize()
reel_whir_player = AudioStreamPlayer.new()
reel_whir_player.name = "ReelWhirPlayer"
reel_whir_player.stream = REEL_WHIR
add_child(reel_whir_player)
# A small voice pool lets nearby reel clicks overlap without cutting each other off.
for i in AUDIO_POOL_SIZE:
var player := AudioStreamPlayer.new()
player.name = "ReelClickVoice%d" % (i + 1)
player.volume_db = click_volume_db
add_child(player)
audio_pool.append(player)
_apply_number_immediately(number_value)
func spin() -> void:
if spin_in_progress:
return
var configuration_error := _get_slot_math_error()
if not configuration_error.is_empty():
push_error(configuration_error)
spin_result_label.text = "CONFIG ERROR"
return
if number_value <= 0:
spin_result_label.text = "NO CREDITS"
return
_reset_win_effect()
for active_tween in reel_tweens:
if active_tween and active_tween.is_valid():
active_tween.kill()
reel_tweens.clear()
var early_session_eligible := (
not early_stack_bonus_consumed
and session_spins_started < early_boost_spin_count
)
var outcome := _select_spin_outcome(early_session_eligible)
session_spins_started += 1
values = outcome.symbols
if (
values[0] == BLUE_COIN_STACK_SYMBOL
and values[0] == values[1]
and values[1] == values[2]
):
early_stack_bonus_consumed = true
selected_payout = outcome.payout
selected_near_miss = outcome.is_near_miss
selected_early_session_boost = outcome.early_session_boost
stopped_icon_reels = 0
spin_in_progress = true
lever_button.disabled = true
spin_result_label.text = "Spinning..."
add_to_number(-1)
_play_spin_start()
_play_short_reel_whir()
for i in icon_reels.size():
var spinner := icon_reels[i]
var n_rows := int(spinner.material.get_shader_parameter("n_rows"))
var result := values[i]
var step := 1.0 / float(n_rows)
var start := float(spinner.material.get_shader_parameter("y_offset"))
# Several full downward rotations plus the predetermined final result.
# Negative texture offset moves this vertical strip downward on screen.
var rotations := randi_range(4, 7)
var target := floorf(start) - rotations + result * step
# Later reels stop later, with slight randomness.
var duration := 1.8 + i * 0.55 + randf_range(0.1, 0.45)
# Open up the spacing between the three prominent icon-reel stops without
# changing reel 1 or the timing of the smaller number reels.
if i < 3:
duration += i * extra_icon_reel_stop_delay
var tick_state := {
"last_row": floori(start * n_rows),
"start": start,
"distance": target - start,
"final_row": floori(target * n_rows),
}
var reel_tween := create_tween()
reel_tweens.append(reel_tween)
reel_tween.tween_method(
func(value: float) -> void:
spinner.material.set_shader_parameter("y_offset", value)
var current_row := floori(value * n_rows)
if current_row != tick_state.last_row:
tick_state.last_row = current_row
# The final detent is scheduled separately to compensate for
# audio-device latency; do not also play it at the endpoint.
if current_row != tick_state.final_row:
var progress := clampf(
(value - tick_state.start) / tick_state.distance,
0.0,
1.0
)
_play_reel_click(progress, i),
start,
target,
duration
).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
reel_tween.tween_callback(func() -> void:
spinner.material.set_shader_parameter("y_offset", fmod(target, 1.0))
stopped_icon_reels += 1
if stopped_icon_reels == icon_reels.size():
_complete_spin()
)
if i < 3:
# Audio reaches the speakers after the engine submits it. Start the stop
# sound early by the latency reported for the active audio device so its
# transient reaches the listener at the visual tween endpoint.
var audio_latency := AudioServer.get_output_latency()
audio_latency += AudioServer.get_time_to_next_mix()
var stop_click_tween := create_tween()
reel_tweens.append(stop_click_tween)
stop_click_tween.tween_interval(maxf(duration - audio_latency, 0.0))
stop_click_tween.tween_callback(_play_stop_click)
func _select_spin_outcome(allow_early_session_boost: bool = false) -> Dictionary:
if allow_early_session_boost and rng.randf() < early_blue_stack_chance:
return {
"symbols": [
BLUE_COIN_STACK_SYMBOL,
BLUE_COIN_STACK_SYMBOL,
BLUE_COIN_STACK_SYMBOL,
] as Array[int],
"payout": _roll_symbol_payout(BLUE_COIN_STACK_SYMBOL),
"is_near_miss": false,
"early_session_boost": true,
}
var roll := rng.randf()
var cumulative_probability := 0.0
var share_total := _sum_positive(symbol_rtp_shares)
for symbol_index in SYMBOL_COUNT:
var normalized_share := maxf(symbol_rtp_shares[symbol_index], 0.0) / share_total
var win_probability := target_rtp * normalized_share / _get_symbol_model_value(symbol_index)
cumulative_probability += win_probability
if roll < cumulative_probability:
return {
"symbols": [symbol_index, symbol_index, symbol_index] as Array[int],
"payout": _roll_symbol_payout(symbol_index),
"is_near_miss": false,
"early_session_boost": false,
}
if rng.randf() < near_miss_rate:
var near_miss_symbol := _pick_near_miss_symbol()
# With the current downward travel, the preceding symbol on the cyclic
# strip stops one position beyond the matching symbol.
var adjacent_symbol := posmod(near_miss_symbol - 1, SYMBOL_COUNT)
return {
"symbols": [near_miss_symbol, near_miss_symbol, adjacent_symbol] as Array[int],
"payout": 0.0,
"is_near_miss": true,
"early_session_boost": false,
}
var losing_symbols: Array[int] = []
while true:
losing_symbols = [
_pick_weighted_symbol(loss_symbol_weights),
_pick_weighted_symbol(loss_symbol_weights),
_pick_weighted_symbol(loss_symbol_weights),
]
# First-two matches are reserved for the explicit near-miss outcome so
# its configured rate remains exact and measurable.
if losing_symbols[0] != losing_symbols[1]:
break
return {
"symbols": losing_symbols,
"payout": 0.0,
"is_near_miss": false,
"early_session_boost": false,
}
func reset_early_session_study() -> void:
session_spins_started = 0
early_stack_bonus_consumed = false
func _pick_weighted_symbol(weights: PackedFloat32Array) -> int:
var total := _sum_positive(weights)
var roll := rng.randf() * total
var cumulative := 0.0
for i in SYMBOL_COUNT:
cumulative += maxf(weights[i], 0.0)
if roll < cumulative:
return i
return SYMBOL_COUNT - 1
func _pick_near_miss_symbol() -> int:
var total := 0.0
for i in range(1, SYMBOL_COUNT):
total += maxf(loss_symbol_weights[i], 0.0)
var roll := rng.randf() * total
var cumulative := 0.0
for i in range(1, SYMBOL_COUNT):
cumulative += maxf(loss_symbol_weights[i], 0.0)
if roll < cumulative:
return i
return SYMBOL_COUNT - 1
func _complete_spin() -> void:
if values[0] == PRESENT_SYMBOL and values[0] == values[1] and values[1] == values[2]:
spin_result_label.text = "PRESENT — no award"
elif selected_payout > 0.0:
var symbol_name := symbol_names[values[0]]
spin_result_label.text = "WIN %sx — %s" % [_format_multiplier(selected_payout), symbol_name.to_upper()]
_play_win_celebration()
return
else:
spin_result_label.text = "No win"
_finish_spin()
func _play_win_celebration() -> void:
_reset_win_effect()
win_effect_tween = create_tween()
win_effect_tween.tween_interval(0.10)
win_effect_tween.tween_method(_set_win_intensity, 0.0, 1.0, 0.10).set_trans(
Tween.TRANS_QUAD
).set_ease(Tween.EASE_OUT)
win_effect_tween.tween_method(_set_win_intensity, 1.0, 0.35, 0.13).set_trans(
Tween.TRANS_QUAD
).set_ease(Tween.EASE_IN)
win_effect_tween.tween_method(_set_win_intensity, 0.35, 0.85, 0.14).set_trans(
Tween.TRANS_SINE
).set_ease(Tween.EASE_OUT)
win_effect_tween.tween_callback(_award_selected_win)
win_effect_tween.tween_method(_set_win_intensity, 0.85, 0.0, 0.30).set_trans(
Tween.TRANS_SINE
).set_ease(Tween.EASE_IN_OUT)
win_effect_tween.tween_callback(_finish_spin)
func _award_selected_win() -> void:
if values[0] == BLUE_COIN_STACK_SYMBOL:
_play_chip_count(int(roundf(selected_payout)))
_award_counter_for_symbol(values[0], selected_payout)
func _set_win_intensity(intensity: float) -> void:
for reel in icon_reels:
reel.material.set_shader_parameter("win_intensity", intensity)
func _reset_win_effect() -> void:
if win_effect_tween and win_effect_tween.is_valid():
win_effect_tween.kill()
win_effect_tween = null
_set_win_intensity(0.0)
func _finish_spin() -> void:
_set_win_intensity(0.0)
spin_in_progress = false
if not lever_animating:
lever_button.disabled = false
spin_finished.emit(values.duplicate(), selected_payout)
func pull_lever() -> void:
if lever_animating or spin_in_progress:
return
lever_animating = true
lever_button.disabled = true
if lever_tween and lever_tween.is_valid():
lever_tween.kill()
if lever_foley_tween and lever_foley_tween.is_valid():
lever_foley_tween.kill()
var audio_latency := AudioServer.get_output_latency()
audio_latency += AudioServer.get_time_to_next_mix()
lever_foley_tween = create_tween()
lever_foley_tween.tween_interval(maxf(0.28 - audio_latency, 0.0))
lever_foley_tween.tween_callback(_play_lever_thwap)
lever_tween = create_tween()
lever_tween.tween_method(_set_lever_pull_progress, 0.0, 1.0, 0.28).set_trans(
Tween.TRANS_CUBIC
).set_ease(Tween.EASE_IN)
lever_tween.tween_callback(spin)
lever_tween.tween_interval(0.08)
lever_tween.tween_method(_set_lever_pull_progress, 1.0, 0.0, 0.46).set_trans(
Tween.TRANS_BACK
).set_ease(Tween.EASE_OUT)
lever_tween.tween_callback(func() -> void:
lever_animating = false
if not spin_in_progress:
lever_button.disabled = false
)
func _play_lever_thwap() -> void:
var player := audio_pool[next_audio_player]
next_audio_player = (next_audio_player + 1) % audio_pool.size()
player.stream = LEVER_THWAP
player.pitch_scale = 1.0 + randf_range(-pitch_variation * 0.3, pitch_variation * 0.3)
player.volume_db = click_volume_db + 1.5
player.play(LEVER_THWAP_START_OFFSET)
func _play_chip_count(point_count: int) -> void:
if chip_count_tween and chip_count_tween.is_valid():
chip_count_tween.kill()
chip_count_tween = create_tween()
for point_index in point_count:
chip_count_tween.tween_callback(
_play_chip_point.bind(point_index, point_count)
)
if point_index < point_count - 1:
var progress := float(point_index) / maxf(float(point_count - 1), 1.0)
chip_count_tween.tween_interval(lerpf(0.105, 0.072, progress))
func _play_chip_point(point_index: int, point_count: int) -> void:
var sound_index := rng.randi_range(0, CHIP_SOUNDS.size() - 2)
if last_chip_sound >= 0 and sound_index >= last_chip_sound:
sound_index += 1
last_chip_sound = sound_index
var player := audio_pool[next_audio_player]
next_audio_player = (next_audio_player + 1) % audio_pool.size()
player.stream = CHIP_SOUNDS[sound_index]
var count_progress := float(point_index) / maxf(float(point_count - 1), 1.0)
player.pitch_scale = lerpf(0.96, 1.08, count_progress) + randf_range(-0.012, 0.012)
# Individual recordings have long tails, so keep each voice restrained as
# the sequence builds into the familiar overlapping chip-count texture.
player.volume_db = click_volume_db - 3.0 + randf_range(-0.6, 0.4)
player.play(CHIP_START_OFFSETS[sound_index])
func _play_short_reel_whir() -> void:
if reel_whir_tween and reel_whir_tween.is_valid():
reel_whir_tween.kill()
reel_whir_player.stop()
reel_whir_player.volume_db = reel_whir_volume_db
reel_whir_player.pitch_scale = 0.92 + randf_range(-0.01, 0.01)
# Use only the recording's naturally decelerating tail, then fade it before
# it can compete with the individual reel clicks.
reel_whir_player.play(REEL_WHIR_START_OFFSET)
reel_whir_tween = create_tween()
reel_whir_tween.tween_interval(0.6)
reel_whir_tween.tween_property(reel_whir_player, "volume_db", -60.0, 0.35)
reel_whir_tween.tween_callback(reel_whir_player.stop)
func _set_lever_pull_progress(progress: float) -> void:
# The generated poses contain real perspective changes. Per-frame offsets keep
# their slightly different mounting sockets anchored to the cabinet pivot.
lever_pivot.rotation = 0.0
var frame_index := clampi(roundi(progress * 4.0), 0, 4)
lever_sprite.frame = frame_index
lever_sprite.position = LEVER_FRAME_POSITIONS[frame_index]
func _award_counter_for_symbol(symbol_index: int, payout: float) -> void:
match symbol_index:
SINGLE_COIN_SYMBOL:
add_to_number(int(roundf(payout)))
BLUE_COIN_STACK_SYMBOL:
add_to_number(int(roundf(payout)))
func _roll_symbol_payout(symbol_index: int) -> float:
if symbol_index == PRESENT_SYMBOL:
return 0.0
return float(rng.randi_range(symbol_payout_min[symbol_index], symbol_payout_max[symbol_index]))
func _get_symbol_model_value(symbol_index: int) -> float:
if symbol_index == PRESENT_SYMBOL:
var other_symbol_total := 0.0
for i in PRESENT_SYMBOL:
other_symbol_total += _get_symbol_expected_payout(i)
return other_symbol_total / float(PRESENT_SYMBOL)
return _get_symbol_expected_payout(symbol_index)
func _get_symbol_expected_payout(symbol_index: int) -> float:
return (float(symbol_payout_min[symbol_index]) + float(symbol_payout_max[symbol_index])) * 0.5
func _format_multiplier(multiplier: float) -> String:
if is_equal_approx(multiplier, roundf(multiplier)):
return str(int(roundf(multiplier)))
return "%.2f" % multiplier
func get_theoretical_rtp() -> float:
if not _get_slot_math_error().is_empty():
return 0.0
var share_total := _sum_positive(symbol_rtp_shares)
var theoretical_rtp := 0.0
for i in SYMBOL_COUNT:
var normalized_share := maxf(symbol_rtp_shares[i], 0.0) / share_total
var model_value := _get_symbol_model_value(i)
var probability := target_rtp * normalized_share / model_value
theoretical_rtp += probability * model_value
return theoretical_rtp
func get_theoretical_credited_rtp() -> float:
if not _get_slot_math_error().is_empty():
return 0.0
var share_total := _sum_positive(symbol_rtp_shares)
var credited_rtp := 0.0
for i in [SINGLE_COIN_SYMBOL, BLUE_COIN_STACK_SYMBOL]:
var normalized_share := maxf(symbol_rtp_shares[i], 0.0) / share_total
var model_value := _get_symbol_model_value(i)
var probability := target_rtp * normalized_share / model_value
credited_rtp += probability * _get_symbol_expected_payout(i)
return credited_rtp
func get_theoretical_hit_rate() -> float:
if not _get_slot_math_error().is_empty():
return 0.0
var share_total := _sum_positive(symbol_rtp_shares)
var hit_rate := 0.0
for i in SYMBOL_COUNT:
var normalized_share := maxf(symbol_rtp_shares[i], 0.0) / share_total
hit_rate += target_rtp * normalized_share / _get_symbol_model_value(i)
return hit_rate
func _get_slot_math_error() -> String:
if icon_reels.size() != 3:
return "Slot math requires exactly three icon reels."
if symbol_names.size() != SYMBOL_COUNT:
return "Symbol names must contain exactly six entries."
if symbol_payout_min.size() != SYMBOL_COUNT or symbol_payout_max.size() != SYMBOL_COUNT:
return "Symbol payout minimums and maximums must contain exactly six entries."
if symbol_rtp_shares.size() != SYMBOL_COUNT or loss_symbol_weights.size() != SYMBOL_COUNT:
return "RTP shares and loss display weights must contain exactly six entries."
for i in SYMBOL_COUNT:
if symbol_payout_min[i] < 0 or symbol_payout_max[i] < symbol_payout_min[i]:
return "Symbol payout ranges must be nonnegative and minimum cannot exceed maximum."
if i != PRESENT_SYMBOL and _get_symbol_expected_payout(i) <= 0.0:
return "Every non-present symbol must have a positive expected payout."
if _sum_positive(symbol_rtp_shares) <= 0.0:
return "At least one RTP share must be positive."
if _sum_positive(loss_symbol_weights) <= 0.0:
return "At least one loss display weight must be positive."
var near_miss_weight_total := 0.0
for i in range(1, SYMBOL_COUNT):
near_miss_weight_total += maxf(loss_symbol_weights[i], 0.0)
if near_miss_rate > 0.0 and near_miss_weight_total <= 0.0:
return "Near misses require a positive display weight for a non-coin symbol."
if near_miss_rate < 0.0 or near_miss_rate > 1.0:
return "Near-miss rate must be between zero and one."
var share_total := _sum_positive(symbol_rtp_shares)
var total_win_probability := 0.0
for i in SYMBOL_COUNT:
var normalized_share := maxf(symbol_rtp_shares[i], 0.0) / share_total
total_win_probability += target_rtp * normalized_share / _get_symbol_model_value(i)
if total_win_probability > 1.0:
return "Configured payouts and RTP produce a win probability above 100%."
return ""
func _sum_positive(numbers: PackedFloat32Array) -> float:
var total := 0.0
for number in numbers:
total += maxf(number, 0.0)
return total
func _play_reel_click(spin_progress: float, _reel_index: int) -> void:
if audio_pool.is_empty():
return
# Avoid the artificial sound of choosing the same sample twice in a row.
var sound_index := randi_range(0, CLICK_SOUNDS.size() - 2)
if last_click_sound >= 0 and sound_index >= last_click_sound:
sound_index += 1
last_click_sound = sound_index
var player := audio_pool[next_audio_player]
next_audio_player = (next_audio_player + 1) % audio_pool.size()
player.stream = CLICK_SOUNDS[sound_index]
# The mechanism loses a little energy as it coasts to a stop. Small random
# variation keeps simultaneous reels from sounding cloned.
player.pitch_scale = lerpf(1.08, 0.91, spin_progress) + randf_range(
-pitch_variation,
pitch_variation
)
player.volume_db = click_volume_db + randf_range(-1.5, 0.8)
player.play(CLICK_START_OFFSETS[sound_index])
func _play_stop_click() -> void:
if audio_pool.is_empty():
return
var stop_pitch := 0.97 + randf_range(-pitch_variation * 0.5, pitch_variation * 0.5)
var player := audio_pool[next_audio_player]
next_audio_player = (next_audio_player + 1) % audio_pool.size()
player.stream = STOP_CLICK
player.pitch_scale = stop_pitch
# The source is substantially louder than the four running clicks, so tame it
# slightly while retaining a distinct, weighty reel-lock sound.
player.volume_db = click_volume_db - 2.0 + randf_range(-0.5, 0.5)
player.play(STOP_CLICK_START_OFFSET)
# Layer the quieter low flap under the detent for mechanical body. Matching
# pitch variation makes both recordings read as one physical impact.
var flap_player := audio_pool[next_audio_player]
next_audio_player = (next_audio_player + 1) % audio_pool.size()
flap_player.stream = STOP_FLAP
flap_player.pitch_scale = stop_pitch
flap_player.volume_db = click_volume_db + 4.0 + randf_range(-0.5, 0.5)
flap_player.play(STOP_FLAP_START_OFFSET)
func _play_spin_start() -> void:
if audio_pool.is_empty():
return
var player := audio_pool[next_audio_player]
next_audio_player = (next_audio_player + 1) % audio_pool.size()
player.stream = SPIN_START_THUMP
player.pitch_scale = 0.94 + randf_range(-pitch_variation * 0.35, pitch_variation * 0.35)
player.volume_db = click_volume_db + 2.0 + randf_range(-0.4, 0.4)
player.play()
func set_number(value: int, animate: bool = true) -> void:
var clamped_value := clampi(value, 0, 9999)
if not animate:
_cancel_number_animation()
number_value = clamped_value
_apply_number_immediately(number_value)
number_change_finished.emit(number_value)
return
if clamped_value == number_value and not number_animation_active:
number_change_finished.emit(number_value)
return
var previous_target := number_value
number_value = clamped_value
_animate_number_to(number_value, previous_target, true)
func add_to_number(amount: int, animate: bool = true) -> void:
var previous_target := number_value
var new_value := clampi(number_value + amount, 0, 9999)
if not animate:
set_number(new_value, false)
return
if new_value == previous_target and not number_animation_active:
number_change_finished.emit(number_value)
return
number_value = new_value
_animate_number_to(number_value, previous_target, true)
func get_number() -> int:
return number_value
func is_number_animating() -> bool:
return number_animation_active
func test_counter_setting() -> void:
set_number(randi_range(0, 9999))
func go_to_doctor_scene() -> void:
continue_requested.emit()
func _animate_number_to(target_value: int, previous_target: int, is_addition: bool) -> void:
_cancel_number_animation()
if number_reels.is_empty():
number_change_finished.emit(number_value)
return
number_animation_active = true
var amount := target_value - previous_target
var direction := signi(amount)
var duration := number_roll_duration
if is_addition:
duration += minf(float(absi(amount)) / 1000.0, 1.0) * 0.35
for i in number_reels.size():
var reel := number_reels[i]
var start := float(reel.material.get_shader_parameter("y_offset"))
var target_digit := _digit_at(target_value, i)
var current_digit_position := start * 10.0
var current_digit_phase := fposmod(current_digit_position, 10.0)
var digit_steps: float
if is_addition and direction != 0:
var place := _place_for_index(i)
var transition_count := floori(float(target_value) / place)
transition_count -= floori(float(previous_target) / place)
var extra_turns := mini(floori(float(absi(transition_count)) / 10.0), 3)
if direction > 0:
digit_steps = fposmod(float(target_digit) - current_digit_phase, 10.0)
digit_steps += extra_turns * 10.0
else:
digit_steps = -fposmod(current_digit_phase - float(target_digit), 10.0)
digit_steps -= extra_turns * 10.0
else:
# This is only reached when retargeting to the same value from a
# fractional position; settle directly without choosing a spin direction.
digit_steps = fposmod(float(target_digit) - current_digit_phase + 5.0, 10.0) - 5.0
var target_offset := start + digit_steps / 10.0
var digit_tween := create_tween()
number_tweens.append(digit_tween)
digit_tween.tween_method(
func(offset: float) -> void:
reel.material.set_shader_parameter("y_offset", offset),
start,
target_offset,
duration
).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
digit_tween.tween_callback(func() -> void:
reel.material.set_shader_parameter("y_offset", float(target_digit) / 10.0)
)
var completion_tween := create_tween()
number_tweens.append(completion_tween)
completion_tween.tween_interval(duration)
completion_tween.tween_callback(func() -> void:
number_animation_active = false
number_tweens.clear()
number_change_finished.emit(number_value)
)
func _cancel_number_animation() -> void:
for active_tween in number_tweens:
if active_tween and active_tween.is_valid():
active_tween.kill()
number_tweens.clear()
number_animation_active = false
func _apply_number_immediately(value: int) -> void:
for i in mini(number_reels.size(), 4):
var digit := _digit_at(value, i)
number_reels[i].material.set_shader_parameter("y_offset", float(digit) / 10.0)
func _digit_at(value: int, index: int) -> int:
return floori(float(value) / _place_for_index(index)) % 10
func _place_for_index(index: int) -> int:
return int(pow(10, 3 - index))
@@ -0,0 +1 @@
uid://dv7jjfmghs0rg
+66
View File
@@ -0,0 +1,66 @@
extends Control
const SLOT_SCENE := preload("res://scenes/ccs_cases_2/slot_machine.tscn")
const DOCTOR_SCENE := preload("res://scenes/ccs_cases_2/doctor_office.tscn")
const DESIGN_SIZE := Vector2(1500.0, 800.0)
var stage_root: Control
var active_scene: Control
var current_view := ""
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
clip_contents = true
stage_root = Control.new()
stage_root.name = "GraphicalCcsStage"
stage_root.size = DESIGN_SIZE
add_child(stage_root)
resized.connect(_fit_stage)
_fit_stage()
func on_show(_payload: Variant = null) -> void:
_show_slot_machine()
func on_hide() -> void:
_clear_active_scene()
func _show_slot_machine() -> void:
_set_active_scene(SLOT_SCENE.instantiate(), "slot_machine")
if active_scene.has_signal("continue_requested"):
active_scene.continue_requested.connect(_show_doctor_office)
func _show_doctor_office() -> void:
_set_active_scene(DOCTOR_SCENE.instantiate(), "doctor_office")
func _set_active_scene(scene: Control, view_name: String) -> void:
_clear_active_scene()
active_scene = scene
current_view = view_name
stage_root.add_child(active_scene)
active_scene.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
func _clear_active_scene() -> void:
if active_scene != null and is_instance_valid(active_scene):
if active_scene.get_parent() != null:
active_scene.get_parent().remove_child(active_scene)
active_scene.queue_free()
active_scene = null
current_view = ""
func _fit_stage() -> void:
if stage_root == null:
return
var available := size
if available.x <= 0.0 or available.y <= 0.0:
return
var fit_scale := minf(available.x / DESIGN_SIZE.x, available.y / DESIGN_SIZE.y)
stage_root.scale = Vector2.ONE * fit_scale
stage_root.position = (available - DESIGN_SIZE * fit_scale) * 0.5
+1
View File
@@ -0,0 +1 @@
uid://buqyka1trttee
+8
View File
@@ -342,6 +342,11 @@ func _build_ui() -> void:
ccs_review.name = "CcsReview"; ccs_review.main = self; screens_container.add_child(ccs_review); screens["ccs_review"] = ccs_review
var ccs_history := preload("res://scripts/ui/ccs_history.gd").new()
ccs_history.name = "CcsHistory"; ccs_history.main = self; screens_container.add_child(ccs_history); screens["ccs_history"] = ccs_history
if FeatureFlags.ccs_cases_2_enabled():
var ccs_cases_2 := preload("res://scripts/ui/ccs_cases_2_screen.gd").new()
ccs_cases_2.name = "CcsCases2"
screens_container.add_child(ccs_cases_2)
screens["ccs_cases_2"] = ccs_cases_2
var flashcards_hub := preload("res://scripts/ui/flashcards_hub.gd").new()
flashcards_hub.name = "FlashcardsHub"; flashcards_hub.main = self
@@ -448,6 +453,7 @@ func _populate_navigation(container: VBoxContainer, include_brand: bool) -> void
_add_nav_button(container, "question_bank", "Question Bank", "clipboard-list", true)
_add_nav_button(container, "dynamic_questions", "Dynamic Questions", "activity", true, Brand.is_uat_environment())
_add_nav_button(container, "ccs", "CCS Cases", "stethoscope", true, Brand.is_uat_environment())
_add_nav_button(container, "ccs_cases_2", "CCS Cases 2", "stethoscope", true, FeatureFlags.ccs_cases_2_enabled())
_add_nav_button(container, "exams", "Exam Simulations", "clipboard-list", true)
_add_nav_button(container, "flashcards", "Flashcards", "clipboard-plus", true, Brand.is_uat_environment())
_add_nav_button(container, "bookmarks", "Bookmarks", "bookmark", true)
@@ -496,6 +502,7 @@ func _on_navigation_pressed(key: String) -> void:
"dynamic_questions": show_screen("dynamic_questions")
"exams": show_screen("simulation_history")
"ccs": show_screen("ccs_hub")
"ccs_cases_2": show_screen("ccs_cases_2")
"flashcards": show_screen("flashcards_study")
"performance": show_screen("profile")
"study_plan": show_screen("map")
@@ -518,6 +525,7 @@ func _update_header_title(screen_name: String) -> void:
"simulation_results": "Exam Simulations",
"session_summary": "Session Summary",
"ccs_hub": "CCS Cases",
"ccs_cases_2": "CCS Cases 2",
"flashcards_hub": "Flashcards",
"flashcards_study": "Flashcards",
"profile": "Profile",
+2
View File
@@ -15,6 +15,8 @@ static func key_for_screen(screen_name: String, payload: Variant = null) -> Stri
key = "dynamic_questions"
elif screen_name.begins_with("simulation_"):
key = "exams"
elif screen_name == "ccs_cases_2":
key = "ccs_cases_2"
elif screen_name.begins_with("ccs_"):
key = "ccs"
elif screen_name.begins_with("flashcards_"):
+53
View File
@@ -0,0 +1,53 @@
shader_type canvas_item;
uniform float patient_scale : hint_range(0.4, 1.0) = 0.82;
uniform vec2 anchor = vec2(0.5, 0.985);
uniform vec2 patient_offset = vec2(0.0, 0.0);
uniform float breathing_speed : hint_range(0.5, 3.0) = 1.0;
uniform float breathing_amount : hint_range(0.0, 0.03) = 0.012;
uniform float blink_amount : hint_range(0.0, 1.0) = 0.0;
void fragment() {
// Size the patient around his original centerline, then lower the complete
// seated pose until the knee crease meets the front edge of the cushion.
vec2 source_uv = (UV - anchor - patient_offset) / patient_scale + anchor;
// Deform only the upper body so his hips, knees, and contact with the bench
// remain perfectly still. Two slow, slightly different cycles prevent the
// idle from reading as a mechanical scale pulse.
float torso_mask = smoothstep(0.13, 0.25, source_uv.y)
* (1.0 - smoothstep(0.48, 0.58, source_uv.y));
float breath = 0.5 + 0.5 * sin(TIME * breathing_speed * 1.75);
float chest_scale = 1.0 + breathing_amount * breath * torso_mask;
source_uv.x = 0.5 + (source_uv.x - 0.5) / chest_scale;
source_uv.y = 0.37 + (source_uv.y - 0.37) / (1.0 + breathing_amount * 0.45 * breath * torso_mask);
float upper_body_mask = 1.0 - smoothstep(0.50, 0.62, source_uv.y);
source_uv.x -= sin(TIME * breathing_speed * 0.63) * 0.0015 * upper_body_mask;
if (source_uv.x < 0.0 || source_uv.x > 1.0 || source_uv.y < 0.0 || source_uv.y > 1.0) {
COLOR = vec4(0.0);
} else {
vec4 patient_color = texture(TEXTURE, source_uv);
// Cover each eye with color sampled from the lower eyelid, then retain a
// narrow crease at full closure. Coordinates are in the canonical patient
// texture, so scaling and positioning do not disturb the blink placement.
vec2 eye_size = vec2(0.0105, 0.0065);
vec2 left_eye_delta = (source_uv - vec2(0.4835, 0.1125)) / eye_size;
vec2 right_eye_delta = (source_uv - vec2(0.5165, 0.1125)) / eye_size;
float left_eye = 1.0 - smoothstep(0.72, 1.0, length(left_eye_delta));
float right_eye = 1.0 - smoothstep(0.72, 1.0, length(right_eye_delta));
float eye_mask = max(left_eye, right_eye) * blink_amount;
vec3 lid_color = texture(TEXTURE, source_uv + vec2(0.0, 0.009)).rgb;
patient_color.rgb = mix(patient_color.rgb, lid_color, eye_mask);
float horizontal_extent = max(
1.0 - smoothstep(0.65, 1.0, abs(left_eye_delta.x)),
1.0 - smoothstep(0.65, 1.0, abs(right_eye_delta.x))
);
float crease = (1.0 - smoothstep(0.0, 0.24, min(
abs(left_eye_delta.y), abs(right_eye_delta.y)
))) * horizontal_extent * blink_amount;
patient_color.rgb = mix(patient_color.rgb, lid_color * 0.58, crease * 0.72);
COLOR = patient_color;
}
}
@@ -0,0 +1 @@
uid://l06nw4ake0q
+42
View File
@@ -0,0 +1,42 @@
shader_type canvas_item;
uniform sampler2D tex : repeat_enable;
uniform float n_rows = 6.0;
uniform float y_offset = 0.0;
uniform float x_offset = 0.0;
uniform float win_intensity : hint_range(0.0, 1.0) = 0.0;
uniform vec4 win_glow_color : source_color = vec4(1.0, 0.62, 0.08, 1.0);
void fragment() {
vec2 uv = UV;
uv.x += x_offset;
uv.y /= n_rows;
uv.y += y_offset;
vec4 base = texture(tex, uv);
if (win_intensity <= 0.001) {
COLOR = base;
} else {
// Derive a compact halo from the existing symbol alpha so no replacement
// sprite or second texture is needed.
vec2 pixel = 1.0 / vec2(textureSize(tex, 0));
vec2 radius = pixel * 5.0;
float nearby_alpha = 0.0;
nearby_alpha = max(nearby_alpha, texture(tex, uv + vec2(radius.x, 0.0)).a);
nearby_alpha = max(nearby_alpha, texture(tex, uv - vec2(radius.x, 0.0)).a);
nearby_alpha = max(nearby_alpha, texture(tex, uv + vec2(0.0, radius.y)).a);
nearby_alpha = max(nearby_alpha, texture(tex, uv - vec2(0.0, radius.y)).a);
nearby_alpha = max(nearby_alpha, texture(tex, uv + radius).a);
nearby_alpha = max(nearby_alpha, texture(tex, uv - radius).a);
nearby_alpha = max(nearby_alpha, texture(tex, uv + vec2(radius.x, -radius.y)).a);
nearby_alpha = max(nearby_alpha, texture(tex, uv + vec2(-radius.x, radius.y)).a);
float halo = max(nearby_alpha - base.a, 0.0) * win_intensity;
float flash = smoothstep(0.82, 1.0, win_intensity);
vec3 rich_color = min(base.rgb * 1.18 + win_glow_color.rgb * 0.12, vec3(1.0));
vec3 symbol_color = mix(base.rgb, rich_color, win_intensity);
symbol_color = mix(symbol_color, vec3(1.0), flash * 0.34 * base.a);
vec3 final_rgb = symbol_color * base.a + win_glow_color.rgb * halo;
COLOR = vec4(final_rgb, max(base.a, halo * 0.9));
}
}
@@ -0,0 +1 @@
uid://l6j3wkyf1ygq
+49
View File
@@ -0,0 +1,49 @@
extends SceneTree
var failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var prior_environment: String = ProjectSettings.get_setting("step3/deployment_environment", "development")
ProjectSettings.set_setting("step3/deployment_environment", "uat")
var main_screen = load("res://scripts/ui/main.gd").new()
root.add_child(main_screen)
await process_frame
var nav_button := main_screen.sidebar.find_child("NavCcsCases2", true, false) as Button
_assert(nav_button != null and nav_button.visible, "UAT should expose a CCS Cases 2 navigation item")
_assert(main_screen.screens.has("ccs_cases_2"), "UAT should register the graphical CCS screen")
main_screen.queue_free()
await process_frame
ProjectSettings.set_setting("step3/deployment_environment", prior_environment)
var host := Control.new()
host.size = Vector2(1200.0, 700.0)
root.add_child(host)
var screen = load("res://scripts/ui/ccs_cases_2_screen.gd").new()
host.add_child(screen)
await process_frame
screen.on_show()
await process_frame
_assert(screen.current_view == "slot_machine", "CCS Cases 2 should open on the slot machine")
_assert(screen.active_scene != null and screen.active_scene.has_signal("continue_requested"), "slot machine should expose an embedded continue signal")
_assert(is_equal_approx(screen.stage_root.scale.x, screen.stage_root.scale.y), "the graphical stage should preserve its aspect ratio")
screen.active_scene.continue_requested.emit()
await process_frame
_assert(screen.current_view == "doctor_office", "Continue should open the doctor office without replacing the app scene")
screen.on_hide()
await process_frame
_assert(screen.active_scene == null and screen.current_view.is_empty(), "hiding CCS Cases 2 should release the prototype scene")
host.queue_free()
await process_frame
quit(1 if failures > 0 else 0)
func _assert(condition: bool, message: String) -> void:
if condition:
return
failures += 1
push_error(message)
+1
View File
@@ -0,0 +1 @@
uid://1voi721kqyhn
+2
View File
@@ -20,10 +20,12 @@ func _run() -> void:
var prior_environment: String = ProjectSettings.get_setting("step3/deployment_environment", "development")
ProjectSettings.set_setting("step3/deployment_environment", "uat")
_assert(FeatureFlags.progression_enabled(), "UAT should enable progression")
_assert(FeatureFlags.ccs_cases_2_enabled(), "UAT should enable CCS Cases 2")
for environment in ["prd", "production", "development", "unknown"]:
ProjectSettings.set_setting("step3/deployment_environment", environment)
_assert(not FeatureFlags.progression_enabled(), "%s should not enable progression" % environment)
_assert(not FeatureFlags.ccs_cases_2_enabled(), "%s should not enable CCS Cases 2" % environment)
ProjectSettings.set_setting("step3/deployment_environment", "prd")
var received: Array = []
+1
View File
@@ -14,6 +14,7 @@ func _initialize() -> void:
_assert(NavigationRoutes.key_for_screen("player") == "question_bank", "question player should map to Question Bank")
_assert(NavigationRoutes.key_for_screen("simulation_results") == "exams", "simulation results should activate Exams")
_assert(NavigationRoutes.key_for_screen("ccs_review") == "ccs", "CCS review should activate CCS Cases")
_assert(NavigationRoutes.key_for_screen("ccs_cases_2") == "ccs_cases_2", "graphical CCS should activate CCS Cases 2")
_assert(NavigationRoutes.key_for_screen("profile") == "performance", "profile should activate Performance")
_assert(NavigationRoutes.key_for_screen("map") == "study_plan", "study map should activate Study Plan")
_assert(NavigationRoutes.key_for_screen("settings") == "settings", "settings should activate Settings")