diff --git a/assets/ccs_cases_2/audio/CHIPS-01.wav b/assets/ccs_cases_2/audio/CHIPS-01.wav new file mode 100644 index 0000000..73d4468 Binary files /dev/null and b/assets/ccs_cases_2/audio/CHIPS-01.wav differ diff --git a/assets/ccs_cases_2/audio/CHIPS-01.wav.import b/assets/ccs_cases_2/audio/CHIPS-01.wav.import new file mode 100644 index 0000000..4f602c6 --- /dev/null +++ b/assets/ccs_cases_2/audio/CHIPS-01.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/CHIPS-02.wav b/assets/ccs_cases_2/audio/CHIPS-02.wav new file mode 100644 index 0000000..b128a50 Binary files /dev/null and b/assets/ccs_cases_2/audio/CHIPS-02.wav differ diff --git a/assets/ccs_cases_2/audio/CHIPS-02.wav.import b/assets/ccs_cases_2/audio/CHIPS-02.wav.import new file mode 100644 index 0000000..5b893a7 --- /dev/null +++ b/assets/ccs_cases_2/audio/CHIPS-02.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/CHIPS-03.wav b/assets/ccs_cases_2/audio/CHIPS-03.wav new file mode 100644 index 0000000..c614d08 Binary files /dev/null and b/assets/ccs_cases_2/audio/CHIPS-03.wav differ diff --git a/assets/ccs_cases_2/audio/CHIPS-03.wav.import b/assets/ccs_cases_2/audio/CHIPS-03.wav.import new file mode 100644 index 0000000..2df2744 --- /dev/null +++ b/assets/ccs_cases_2/audio/CHIPS-03.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/CHIPS-04.wav b/assets/ccs_cases_2/audio/CHIPS-04.wav new file mode 100644 index 0000000..245ff34 Binary files /dev/null and b/assets/ccs_cases_2/audio/CHIPS-04.wav differ diff --git a/assets/ccs_cases_2/audio/CHIPS-04.wav.import b/assets/ccs_cases_2/audio/CHIPS-04.wav.import new file mode 100644 index 0000000..0d88c6f --- /dev/null +++ b/assets/ccs_cases_2/audio/CHIPS-04.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/CLICK-01.wav b/assets/ccs_cases_2/audio/CLICK-01.wav new file mode 100644 index 0000000..53d21c9 Binary files /dev/null and b/assets/ccs_cases_2/audio/CLICK-01.wav differ diff --git a/assets/ccs_cases_2/audio/CLICK-01.wav.import b/assets/ccs_cases_2/audio/CLICK-01.wav.import new file mode 100644 index 0000000..64acb36 --- /dev/null +++ b/assets/ccs_cases_2/audio/CLICK-01.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/CLICK-02.wav b/assets/ccs_cases_2/audio/CLICK-02.wav new file mode 100644 index 0000000..6770335 Binary files /dev/null and b/assets/ccs_cases_2/audio/CLICK-02.wav differ diff --git a/assets/ccs_cases_2/audio/CLICK-02.wav.import b/assets/ccs_cases_2/audio/CLICK-02.wav.import new file mode 100644 index 0000000..da06a54 --- /dev/null +++ b/assets/ccs_cases_2/audio/CLICK-02.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/CLICK-03.wav b/assets/ccs_cases_2/audio/CLICK-03.wav new file mode 100644 index 0000000..addb819 Binary files /dev/null and b/assets/ccs_cases_2/audio/CLICK-03.wav differ diff --git a/assets/ccs_cases_2/audio/CLICK-03.wav.import b/assets/ccs_cases_2/audio/CLICK-03.wav.import new file mode 100644 index 0000000..44fb189 --- /dev/null +++ b/assets/ccs_cases_2/audio/CLICK-03.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/CLICK-04.wav b/assets/ccs_cases_2/audio/CLICK-04.wav new file mode 100644 index 0000000..fa820b3 Binary files /dev/null and b/assets/ccs_cases_2/audio/CLICK-04.wav differ diff --git a/assets/ccs_cases_2/audio/CLICK-04.wav.import b/assets/ccs_cases_2/audio/CLICK-04.wav.import new file mode 100644 index 0000000..da1a203 --- /dev/null +++ b/assets/ccs_cases_2/audio/CLICK-04.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/DEEPFLAP-01.wav b/assets/ccs_cases_2/audio/DEEPFLAP-01.wav new file mode 100644 index 0000000..a362446 Binary files /dev/null and b/assets/ccs_cases_2/audio/DEEPFLAP-01.wav differ diff --git a/assets/ccs_cases_2/audio/DEEPFLAP-01.wav.import b/assets/ccs_cases_2/audio/DEEPFLAP-01.wav.import new file mode 100644 index 0000000..5f7354b --- /dev/null +++ b/assets/ccs_cases_2/audio/DEEPFLAP-01.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/FAN-WHIR-01.wav b/assets/ccs_cases_2/audio/FAN-WHIR-01.wav new file mode 100644 index 0000000..bbca3b1 Binary files /dev/null and b/assets/ccs_cases_2/audio/FAN-WHIR-01.wav differ diff --git a/assets/ccs_cases_2/audio/FAN-WHIR-01.wav.import b/assets/ccs_cases_2/audio/FAN-WHIR-01.wav.import new file mode 100644 index 0000000..af6c464 --- /dev/null +++ b/assets/ccs_cases_2/audio/FAN-WHIR-01.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/LOUDCLICK-01.wav b/assets/ccs_cases_2/audio/LOUDCLICK-01.wav new file mode 100644 index 0000000..1b00b85 Binary files /dev/null and b/assets/ccs_cases_2/audio/LOUDCLICK-01.wav differ diff --git a/assets/ccs_cases_2/audio/LOUDCLICK-01.wav.import b/assets/ccs_cases_2/audio/LOUDCLICK-01.wav.import new file mode 100644 index 0000000..38460cb --- /dev/null +++ b/assets/ccs_cases_2/audio/LOUDCLICK-01.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/MAGNET-THWAP-01.wav b/assets/ccs_cases_2/audio/MAGNET-THWAP-01.wav new file mode 100644 index 0000000..895236d Binary files /dev/null and b/assets/ccs_cases_2/audio/MAGNET-THWAP-01.wav differ diff --git a/assets/ccs_cases_2/audio/MAGNET-THWAP-01.wav.import b/assets/ccs_cases_2/audio/MAGNET-THWAP-01.wav.import new file mode 100644 index 0000000..4c3ff33 --- /dev/null +++ b/assets/ccs_cases_2/audio/MAGNET-THWAP-01.wav.import @@ -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 diff --git a/assets/ccs_cases_2/audio/THUMP-01.wav b/assets/ccs_cases_2/audio/THUMP-01.wav new file mode 100644 index 0000000..8af084f Binary files /dev/null and b/assets/ccs_cases_2/audio/THUMP-01.wav differ diff --git a/assets/ccs_cases_2/audio/THUMP-01.wav.import b/assets/ccs_cases_2/audio/THUMP-01.wav.import new file mode 100644 index 0000000..341ab73 --- /dev/null +++ b/assets/ccs_cases_2/audio/THUMP-01.wav.import @@ -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 diff --git a/assets/ccs_cases_2/images/doctor/chat-bubble.png b/assets/ccs_cases_2/images/doctor/chat-bubble.png new file mode 100644 index 0000000..467a67c Binary files /dev/null and b/assets/ccs_cases_2/images/doctor/chat-bubble.png differ diff --git a/assets/ccs_cases_2/images/doctor/chat-bubble.png.import b/assets/ccs_cases_2/images/doctor/chat-bubble.png.import new file mode 100644 index 0000000..0921b20 --- /dev/null +++ b/assets/ccs_cases_2/images/doctor/chat-bubble.png.import @@ -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 diff --git a/assets/ccs_cases_2/images/doctor/clock.png b/assets/ccs_cases_2/images/doctor/clock.png new file mode 100644 index 0000000..3c6995c Binary files /dev/null and b/assets/ccs_cases_2/images/doctor/clock.png differ diff --git a/assets/ccs_cases_2/images/doctor/clock.png.import b/assets/ccs_cases_2/images/doctor/clock.png.import new file mode 100644 index 0000000..a9003c7 --- /dev/null +++ b/assets/ccs_cases_2/images/doctor/clock.png.import @@ -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 diff --git a/assets/ccs_cases_2/images/doctor/dialogue-choice-hover.png b/assets/ccs_cases_2/images/doctor/dialogue-choice-hover.png new file mode 100644 index 0000000..111fdfe Binary files /dev/null and b/assets/ccs_cases_2/images/doctor/dialogue-choice-hover.png differ diff --git a/assets/ccs_cases_2/images/doctor/dialogue-choice-hover.png.import b/assets/ccs_cases_2/images/doctor/dialogue-choice-hover.png.import new file mode 100644 index 0000000..2d80d30 --- /dev/null +++ b/assets/ccs_cases_2/images/doctor/dialogue-choice-hover.png.import @@ -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 diff --git a/assets/ccs_cases_2/images/doctor/dialogue-choice-normal.png b/assets/ccs_cases_2/images/doctor/dialogue-choice-normal.png new file mode 100644 index 0000000..379f4ae Binary files /dev/null and b/assets/ccs_cases_2/images/doctor/dialogue-choice-normal.png differ diff --git a/assets/ccs_cases_2/images/doctor/dialogue-choice-normal.png.import b/assets/ccs_cases_2/images/doctor/dialogue-choice-normal.png.import new file mode 100644 index 0000000..bc3952b --- /dev/null +++ b/assets/ccs_cases_2/images/doctor/dialogue-choice-normal.png.import @@ -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 diff --git a/assets/ccs_cases_2/images/doctor/dialogue-choice-pressed.png b/assets/ccs_cases_2/images/doctor/dialogue-choice-pressed.png new file mode 100644 index 0000000..605d912 Binary files /dev/null and b/assets/ccs_cases_2/images/doctor/dialogue-choice-pressed.png differ diff --git a/assets/ccs_cases_2/images/doctor/dialogue-choice-pressed.png.import b/assets/ccs_cases_2/images/doctor/dialogue-choice-pressed.png.import new file mode 100644 index 0000000..ef845ad --- /dev/null +++ b/assets/ccs_cases_2/images/doctor/dialogue-choice-pressed.png.import @@ -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 diff --git a/assets/ccs_cases_2/images/doctor/office-backdrop.png b/assets/ccs_cases_2/images/doctor/office-backdrop.png new file mode 100644 index 0000000..1147ee3 Binary files /dev/null and b/assets/ccs_cases_2/images/doctor/office-backdrop.png differ diff --git a/assets/ccs_cases_2/images/doctor/office-backdrop.png.import b/assets/ccs_cases_2/images/doctor/office-backdrop.png.import new file mode 100644 index 0000000..0054c01 --- /dev/null +++ b/assets/ccs_cases_2/images/doctor/office-backdrop.png.import @@ -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 diff --git a/assets/ccs_cases_2/images/doctor/patient-44-white-male.png b/assets/ccs_cases_2/images/doctor/patient-44-white-male.png new file mode 100644 index 0000000..5ad2187 Binary files /dev/null and b/assets/ccs_cases_2/images/doctor/patient-44-white-male.png differ diff --git a/assets/ccs_cases_2/images/doctor/patient-44-white-male.png.import b/assets/ccs_cases_2/images/doctor/patient-44-white-male.png.import new file mode 100644 index 0000000..57bf216 --- /dev/null +++ b/assets/ccs_cases_2/images/doctor/patient-44-white-male.png.import @@ -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 diff --git a/assets/ccs_cases_2/images/slot/lever-sprite-sheet.png b/assets/ccs_cases_2/images/slot/lever-sprite-sheet.png new file mode 100644 index 0000000..65b1926 Binary files /dev/null and b/assets/ccs_cases_2/images/slot/lever-sprite-sheet.png differ diff --git a/assets/ccs_cases_2/images/slot/lever-sprite-sheet.png.import b/assets/ccs_cases_2/images/slot/lever-sprite-sheet.png.import new file mode 100644 index 0000000..7ae2435 --- /dev/null +++ b/assets/ccs_cases_2/images/slot/lever-sprite-sheet.png.import @@ -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 diff --git a/assets/ccs_cases_2/images/slot/numbers.png b/assets/ccs_cases_2/images/slot/numbers.png new file mode 100644 index 0000000..a3dc6a1 Binary files /dev/null and b/assets/ccs_cases_2/images/slot/numbers.png differ diff --git a/assets/ccs_cases_2/images/slot/numbers.png.import b/assets/ccs_cases_2/images/slot/numbers.png.import new file mode 100644 index 0000000..53d8187 --- /dev/null +++ b/assets/ccs_cases_2/images/slot/numbers.png.import @@ -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 diff --git a/assets/ccs_cases_2/images/slot/reel-frame.png b/assets/ccs_cases_2/images/slot/reel-frame.png new file mode 100644 index 0000000..528c380 Binary files /dev/null and b/assets/ccs_cases_2/images/slot/reel-frame.png differ diff --git a/assets/ccs_cases_2/images/slot/reel-frame.png.import b/assets/ccs_cases_2/images/slot/reel-frame.png.import new file mode 100644 index 0000000..c639816 --- /dev/null +++ b/assets/ccs_cases_2/images/slot/reel-frame.png.import @@ -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 diff --git a/assets/ccs_cases_2/images/slot/symbols.png b/assets/ccs_cases_2/images/slot/symbols.png new file mode 100644 index 0000000..4761c73 Binary files /dev/null and b/assets/ccs_cases_2/images/slot/symbols.png differ diff --git a/assets/ccs_cases_2/images/slot/symbols.png.import b/assets/ccs_cases_2/images/slot/symbols.png.import new file mode 100644 index 0000000..ac72719 --- /dev/null +++ b/assets/ccs_cases_2/images/slot/symbols.png.import @@ -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 diff --git a/scenes/ccs_cases_2/doctor_office.tscn b/scenes/ccs_cases_2/doctor_office.tscn new file mode 100644 index 0000000..3671630 --- /dev/null +++ b/scenes/ccs_cases_2/doctor_office.tscn @@ -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 diff --git a/scenes/ccs_cases_2/slot_machine.tscn b/scenes/ccs_cases_2/slot_machine.tscn new file mode 100644 index 0000000..4a603b6 --- /dev/null +++ b/scenes/ccs_cases_2/slot_machine.tscn @@ -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"] diff --git a/scripts/config/feature_flags.gd b/scripts/config/feature_flags.gd index e56eeb4..7dcf1fc 100644 --- a/scripts/config/feature_flags.gd +++ b/scripts/config/feature_flags.gd @@ -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", diff --git a/scripts/ui/ccs_cases_2/doctor_office.gd b/scripts/ui/ccs_cases_2/doctor_office.gd new file mode 100644 index 0000000..b954cdd --- /dev/null +++ b/scripts/ui/ccs_cases_2/doctor_office.gd @@ -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() diff --git a/scripts/ui/ccs_cases_2/doctor_office.gd.uid b/scripts/ui/ccs_cases_2/doctor_office.gd.uid new file mode 100644 index 0000000..7f6d815 --- /dev/null +++ b/scripts/ui/ccs_cases_2/doctor_office.gd.uid @@ -0,0 +1 @@ +uid://dq6ty24drp82o diff --git a/scripts/ui/ccs_cases_2/slot_machine.gd b/scripts/ui/ccs_cases_2/slot_machine.gd new file mode 100644 index 0000000..d26d0e9 --- /dev/null +++ b/scripts/ui/ccs_cases_2/slot_machine.gd @@ -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)) diff --git a/scripts/ui/ccs_cases_2/slot_machine.gd.uid b/scripts/ui/ccs_cases_2/slot_machine.gd.uid new file mode 100644 index 0000000..c66a3d9 --- /dev/null +++ b/scripts/ui/ccs_cases_2/slot_machine.gd.uid @@ -0,0 +1 @@ +uid://dv7jjfmghs0rg diff --git a/scripts/ui/ccs_cases_2_screen.gd b/scripts/ui/ccs_cases_2_screen.gd new file mode 100644 index 0000000..aa11a20 --- /dev/null +++ b/scripts/ui/ccs_cases_2_screen.gd @@ -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 diff --git a/scripts/ui/ccs_cases_2_screen.gd.uid b/scripts/ui/ccs_cases_2_screen.gd.uid new file mode 100644 index 0000000..764db9a --- /dev/null +++ b/scripts/ui/ccs_cases_2_screen.gd.uid @@ -0,0 +1 @@ +uid://buqyka1trttee diff --git a/scripts/ui/main.gd b/scripts/ui/main.gd index 2b4a16e..3932622 100644 --- a/scripts/ui/main.gd +++ b/scripts/ui/main.gd @@ -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", diff --git a/scripts/ui/navigation_routes.gd b/scripts/ui/navigation_routes.gd index c9a1005..37d51b7 100644 --- a/scripts/ui/navigation_routes.gd +++ b/scripts/ui/navigation_routes.gd @@ -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_"): diff --git a/shaders/ccs_cases_2/patient_idle.gdshader b/shaders/ccs_cases_2/patient_idle.gdshader new file mode 100644 index 0000000..3783e4c --- /dev/null +++ b/shaders/ccs_cases_2/patient_idle.gdshader @@ -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; + } +} diff --git a/shaders/ccs_cases_2/patient_idle.gdshader.uid b/shaders/ccs_cases_2/patient_idle.gdshader.uid new file mode 100644 index 0000000..b14e015 --- /dev/null +++ b/shaders/ccs_cases_2/patient_idle.gdshader.uid @@ -0,0 +1 @@ +uid://l06nw4ake0q diff --git a/shaders/ccs_cases_2/reel_spin.gdshader b/shaders/ccs_cases_2/reel_spin.gdshader new file mode 100644 index 0000000..06a7796 --- /dev/null +++ b/shaders/ccs_cases_2/reel_spin.gdshader @@ -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)); + } +} diff --git a/shaders/ccs_cases_2/reel_spin.gdshader.uid b/shaders/ccs_cases_2/reel_spin.gdshader.uid new file mode 100644 index 0000000..944c7e4 --- /dev/null +++ b/shaders/ccs_cases_2/reel_spin.gdshader.uid @@ -0,0 +1 @@ +uid://l6j3wkyf1ygq diff --git a/tests/ccs_cases_2_ui_test.gd b/tests/ccs_cases_2_ui_test.gd new file mode 100644 index 0000000..bc361ae --- /dev/null +++ b/tests/ccs_cases_2_ui_test.gd @@ -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) diff --git a/tests/ccs_cases_2_ui_test.gd.uid b/tests/ccs_cases_2_ui_test.gd.uid new file mode 100644 index 0000000..bdf1f7c --- /dev/null +++ b/tests/ccs_cases_2_ui_test.gd.uid @@ -0,0 +1 @@ +uid://1voi721kqyhn diff --git a/tests/progression_feature_flag_test.gd b/tests/progression_feature_flag_test.gd index 1f88fea..5ad1d5e 100644 --- a/tests/progression_feature_flag_test.gd +++ b/tests/progression_feature_flag_test.gd @@ -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 = [] diff --git a/tests/shell_navigation_test.gd b/tests/shell_navigation_test.gd index f2fd4ce..aa6e8e3 100644 --- a/tests/shell_navigation_test.gd +++ b/tests/shell_navigation_test.gd @@ -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")