From b4998255604aafa4bade88c67782f6f19ff08277 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sun, 17 May 2026 13:26:21 +0100 Subject: [PATCH 1/4] Dirty robot! --- scenes/robot.gd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scenes/robot.gd b/scenes/robot.gd index 2924016..8beffe8 100644 --- a/scenes/robot.gd +++ b/scenes/robot.gd @@ -64,7 +64,9 @@ func _on_area_3d_area_entered(area: Area3D) -> void: EventBus.game_lost.emit("ROBOT GOT IN") func stalking_check(): - + if safety_zone == null: + return + var robot_position = self.global_transform.origin var elevator = safety_zone.global_transform.origin var distance = robot_position.distance_to(elevator) From c14312c81efcf92c8d454e550af6533291e5a9d2 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sun, 17 May 2026 13:57:29 +0100 Subject: [PATCH 2/4] Ease hand intro tween - replace fixed-duration parallel tween with a tween_method driving a single progress value - button lights now trigger against the eased curve instead of delays --- scenes/game.tscn | 2 ++ scenes/left_hand.gd | 35 ++++++++++++++++++++++++++++------- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/scenes/game.tscn b/scenes/game.tscn index b2c8d5d..9e23712 100644 --- a/scenes/game.tscn +++ b/scenes/game.tscn @@ -80,3 +80,5 @@ script = ExtResource("14_muzak_s") [node name="TitleScreen" parent="." unique_id=1656317143 instance=ExtResource("11_title")] [node name="PauseMenu" parent="." unique_id=1244164419 instance=ExtResource("12_pause")] + +[editable path="World"] diff --git a/scenes/left_hand.gd b/scenes/left_hand.gd index d89f97b..80720cb 100644 --- a/scenes/left_hand.gd +++ b/scenes/left_hand.gd @@ -1,6 +1,10 @@ extends Sprite3D -const INTRO_DURATION := 1.5 +# we easin' now +@export var intro_duration: float = 0.9 +@export var intro_trans: Tween.TransitionType = Tween.TRANS_SINE +@export var intro_ease: Tween.EaseType = Tween.EASE_IN_OUT + const ENTRY_OFFSET := 0.5 const EXIT_OFFSET := 0.5 const FORWARD_OFFSET := 0.05 @@ -11,6 +15,10 @@ const FINGER_OFFSET_X := 0.0 var _buttons: Array = [] var _intro_playing := false var _sprite_half_h := 0.0 +var _intro_sprite_start_y := 0.0 +var _intro_sprite_end_y := 0.0 +var _intro_button_thresholds: Array = [] +var _intro_button_lit: Array = [] func _ready() -> void: visible = false @@ -77,24 +85,37 @@ func _play_intro() -> void: global_position = Vector3(hand_x, sprite_start_y, hand_z) visible = true - - var tween := create_tween().set_parallel(true) - tween.tween_property(self, "global_position:y", sprite_end_y, INTRO_DURATION) - + + # now we have to do some more mathin' due to *easing* + _intro_sprite_start_y = sprite_start_y + _intro_sprite_end_y = sprite_end_y + _intro_button_thresholds.clear() + _intro_button_lit.clear() for i in range(_buttons.size()): var button_y_world = _buttons[i].global_position.y # weird thing: button_y is projected onto the hand's Z-plane to account for perspective # to keep the fingertip visually aligned with each button at the exact right moment + # thanks godot forums! var button_y_proj = camera_pos.y + (button_y_world - camera_pos.y) * z_ratio var t_fraction = (finger_start_y - button_y_proj) / (finger_start_y - finger_end_y) - t_fraction = clamp(t_fraction, 0.0, 1.0) - tween.tween_callback(_light_button.bind(i)).set_delay(t_fraction * INTRO_DURATION) + _intro_button_thresholds.append(clamp(t_fraction, 0.0, 1.0)) + _intro_button_lit.append(false) + + var tween := create_tween() + tween.tween_method(_update_intro_progress, 0.0, 1.0, intro_duration).set_trans(intro_trans).set_ease(intro_ease) await tween.finished visible = false _intro_playing = false EventBus.intro_finished.emit() +func _update_intro_progress(progress: float) -> void: + global_position.y = lerp(_intro_sprite_start_y, _intro_sprite_end_y, progress) + for i in range(_buttons.size()): + if not _intro_button_lit[i] and progress >= _intro_button_thresholds[i]: + _intro_button_lit[i] = true + _light_button(i) + func _light_button(index: int) -> void: var sprite: Sprite3D = _buttons[index].get_node("ButtonSprite") sprite.frame = index + 10 From c3d8ff69897566de7f58cd3b296256c2e242ed28 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sun, 17 May 2026 19:20:58 +0100 Subject: [PATCH 3/4] Squashed commit --- images/goatech_screen.png | Bin 0 -> 873 bytes images/goatech_screen.png.import | 40 ++++++++++ scenes/elevator.gd | 8 +- scenes/elevator_button.tscn | 1 + scenes/elevator_panel.gd | 128 +++++++++++++++++++++++++------ scenes/elevator_panel.tscn | 28 +++---- scenes/game.tscn | 3 + scenes/hud.gd | 36 +-------- scenes/hud.tscn | 22 ------ scenes/screen.gd | 74 +++++++++++++++++- scenes/survivor.gd | 1 + scenes/virtua_hand.gd | 73 ++++++++++++++---- scenes/world.tscn | 10 +-- scripts/event_bus.gd | 4 +- 14 files changed, 308 insertions(+), 120 deletions(-) create mode 100644 images/goatech_screen.png create mode 100644 images/goatech_screen.png.import diff --git a/images/goatech_screen.png b/images/goatech_screen.png new file mode 100644 index 0000000000000000000000000000000000000000..d0907bc1f48836e9db9bc5c0a5f5dc3e8bc57f0a GIT binary patch literal 873 zcmbtSjZchm9RFby*JUwFg&RiXC266x?#{WEYu0l0D7Ct{xklqQVxjA-A{^ShTp4vW z^zx))P3Yx0Z;8q7PA`Ty>-_Of$zdh%oBE$J}yyqZ< z__FO%Idlq6vFi-@o<6ox3hhbMj+ij$1VSW2n41U!kp#jVM39Ii5$1LTMkGd=Z#hz ze{I!)rT1n1-RX(FS4#J1Z?d>Vcu7vK74PSvTnGd0lZ!CZGqmTUM<74{md`Ct*e+Q`0?X?<}9quu+uYn#YJ zD`9!gQ?rvcHwm=|yL=3pGcQuXj=(_8SH0-STV<<~p%X@%T8>W!h()8n1T2sJk)7py zMA@blB-wK8&7ua%!#3pFT5WhEYNC$zgxe<0UAELN(F~SVm<4{(wxteFp3!uVR+p&# z`n*(*hS3{B=5uhQVw?Mma9nu!Qh5I@ZF#IsEp^Nt8wznh>wMfs{EA^*cE@V-Qn8hWAu25#@8zB?nYhRV&_;NdeKka$3 z{|X!ZQ{ck3I!%t@gs05vk00LG5s%iTb!1U>_WIB@57y_ZQ?Z!VmKneGO{#T#+SpxM zBj@?d8V~(C*BEDwDRiYPea!09nX31Ugbq2AsiIT#55HF~2$Txg0<-CEN>83e-56)h z5i*%2hTW#bx-;6>et?<7gSy-gBT9Tag7uvhx A5&!@I literal 0 HcmV?d00001 diff --git a/images/goatech_screen.png.import b/images/goatech_screen.png.import new file mode 100644 index 0000000..aa17d04 --- /dev/null +++ b/images/goatech_screen.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dddmw71jfy4yq" +path="res://.godot/imported/goatech_screen.png-55b460e1ad15ace769d864d7c2c15920.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://images/goatech_screen.png" +dest_files=["res://.godot/imported/goatech_screen.png-55b460e1ad15ace769d864d7c2c15920.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/elevator.gd b/scenes/elevator.gd index a1db543..8cb181d 100644 --- a/scenes/elevator.gd +++ b/scenes/elevator.gd @@ -47,9 +47,13 @@ func _on_pulse_started(duration: float): var lag = max(PULSE_CLOSE_LAG_MIN, PULSE_CLOSE_LAG_TOP - floors_descended * PULSE_CLOSE_LAG_PER_FLOOR) get_tree().create_timer(lag, false).timeout.connect( func(): - if _pulse_close_pending: + if not _pulse_close_pending: + return + if $ElevatorDoorRight.position.x < DOOR_OPEN_X - 0.001: _pulse_close_pending = false - _tween_doors(DOOR_CLOSED_X, duration), + return + _pulse_close_pending = false + _tween_doors(DOOR_CLOSED_X, duration), CONNECT_ONE_SHOT ) diff --git a/scenes/elevator_button.tscn b/scenes/elevator_button.tscn index 479907a..65d2489 100644 --- a/scenes/elevator_button.tscn +++ b/scenes/elevator_button.tscn @@ -4,6 +4,7 @@ [node name="ElevatorButton" type="Sprite3D" unique_id=1758559173] billboard = 1 +alpha_cut = 1 texture_filter = 0 texture = ExtResource("1_sy1b3") hframes = 2 diff --git a/scenes/elevator_panel.gd b/scenes/elevator_panel.gd index 5c38287..1d1fbf6 100644 --- a/scenes/elevator_panel.gd +++ b/scenes/elevator_panel.gd @@ -1,7 +1,6 @@ extends PanelContainer @onready var _screen = $PanelMargin/PanelColumn/Screen -@onready var _close_button: Button = $PanelMargin/PanelColumn/CloseButton var current_floor: int = EventBus.STARTING_FLOOR var saved_count := 0 @@ -32,10 +31,17 @@ var people_in_elevator := 0 const PERFECT_STUN_DURATION := 1.5 const DING_TO_DOORS_DELAY := 2.0 -func _ready(): - _close_button.text = "CLOSE" - _close_button.pressed.connect(_on_block_pressed) +const HACK_DELAY_MIN := 1.25 +const HACK_DELAY_MAX := 1.75 +const FIRST_FLOOR_INTRO_DELAY := 2.0 +const STATIC_DURATION := 0.2 +const DEBUG_SCREEN_STATE := false +enum ScreenState { IDLE, STATIC, HACKED } +var state: ScreenState = ScreenState.IDLE +var floor_token: int = 0 + +func _ready(): _screen.pulse_started.connect(_on_pulse_started) _screen.pulse_blocked.connect(_on_pulse_blocked) _screen.pulse_blocked_perfect.connect(_on_pulse_blocked_perfect) @@ -45,11 +51,15 @@ func _ready(): EventBus.game_lost.connect(func(_reason): $SfxRobotIUnderstand.play()) EventBus.survivor_squeaked_in.connect(_on_survivor_squeaked_in) EventBus.robot_close_warning.connect(_on_robot_close_warning) + EventBus.survivor_entered_elevator.connect(_on_survivor_entered_elevator) func _unhandled_input(event): if event is InputEventKey and event.pressed and not event.echo: if event.keycode == KEY_SPACE: - _on_block_pressed() + match state: + ScreenState.HACKED: _on_open_pressed() + ScreenState.IDLE: _on_close_pressed() + ScreenState.STATIC: pass elif event.keycode == KEY_R: get_tree().reload_current_scene() elif event.keycode >= KEY_1 and event.keycode <= KEY_9: @@ -66,16 +76,23 @@ func _on_game_started(): func _reset_floor_state(): doors_closing_flag = false people_in_elevator = 0 - _close_button.text = "CLOSE" var floors_remaining = current_floor - 1 survivors_remaining = BASE_SURVIVORS + (floors_remaining * SURVIVORS_PER_FLOOR_INCREASE) +func _dbg(msg: String) -> void: + if DEBUG_SCREEN_STATE: + print("[screen] floor=%d token=%d %s" % [current_floor, floor_token, msg]) + func _start_floor(): if EventBus.debug_starting_floor > 0: current_floor = EventBus.debug_starting_floor _onboarded = true EventBus.debug_starting_floor = 0 + floor_token += 1 + var token = floor_token + _dbg("start_floor IDLE") + $SfxBell.play() if _onboarded and randf() < 0.4: $SfxRobotDeepBreath.play() @@ -107,11 +124,51 @@ func _start_floor(): if not _onboarded: _onboarded = true - EventBus.doors_nearly_opened.connect( + + state = ScreenState.IDLE + _screen.enter_idle() + EventBus.active_button_changed.emit("close") + + if current_floor == EventBus.STARTING_FLOOR: + get_tree().create_timer(FIRST_FLOOR_INTRO_DELAY, false).timeout.connect( + func(): + if token != floor_token or doors_closing_flag: + _dbg("first-floor delay bail") + return + _enter_static_then_hacked(token), + CONNECT_ONE_SHOT + ) + else: + schedule_next_hack(token) + +func schedule_next_hack(token: int) -> void: + var delay := randf_range(HACK_DELAY_MIN, HACK_DELAY_MAX) + _dbg("schedule_next_hack delay=%.2f" % delay) + get_tree().create_timer(delay, false).timeout.connect( func(): - if not is_instance_valid(self) or doors_closing_flag: + if token != floor_token: + _dbg("schedule_next_hack token bail") return - _screen.launch_pulse(), + if doors_closing_flag: + _dbg("schedule_next_hack doors_closing bail") + return + _enter_static_then_hacked(token), + CONNECT_ONE_SHOT + ) + +func _enter_static_then_hacked(token: int) -> void: + state = ScreenState.STATIC + _screen.enter_static() + _dbg("STATIC") + get_tree().create_timer(STATIC_DURATION, false).timeout.connect( + func(): + if token != floor_token or doors_closing_flag: + _dbg("post-STATIC bail") + return + state = ScreenState.HACKED + _dbg("HACKED") + _screen.enter_hacked() + EventBus.active_button_changed.emit("open"), CONNECT_ONE_SHOT ) @@ -123,46 +180,71 @@ func _on_robot_close_warning(): if not $SfxRobotDeepBreath.playing: $SfxRobotDeepBreath.play() -func _on_block_pressed(): +func _on_open_pressed(): + if state != ScreenState.HACKED: + _dbg("open rejected (state=%d)" % state) + return if doors_closing_flag: return - EventBus.block_pressed.emit() - if _screen.pulse_active: - _screen.attempt_block() - else: - $SfxRobotThankYou.play() - _on_doors_closing(false) + if not _screen.pulse_active: + return + EventBus.button_pressed.emit() + _screen.attempt_block() + +func _on_close_pressed(): + if state != ScreenState.IDLE: + _dbg("close rejected (state=%d)" % state) + return + if doors_closing_flag: + return + EventBus.button_pressed.emit() + $SfxRobotThankYou.play() + _on_doors_closing(false) + +func _on_survivor_entered_elevator(): + if doors_closing_flag: + return + $SfxDing.play() func _on_pulse_started(duration: float): - _close_button.text = "BLOCK" EventBus.pulse_started.emit(duration) func _on_pulse_blocked_perfect(): EventBus.robot_stun_requested.emit(PERFECT_STUN_DURATION) func _on_pulse_blocked(): - _close_button.text = "CLOSE" EventBus.pulse_blocked.emit() survivors_remaining -= 1 people_in_elevator += 1 - $SfxDing.play() EventBus.people_changed.emit(people_in_elevator, THRESHOLD) + if $SfxBlock.stream: + $SfxBlock.play() if survivors_remaining <= 0: get_tree().create_timer(0.25, false).timeout.connect(_on_doors_closing, CONNECT_ONE_SHOT) return - var gap = _screen.get_pulse_gap() - get_tree().create_timer(gap, false).timeout.connect( + var token = floor_token + state = ScreenState.STATIC + _screen.enter_static() + _dbg("post-block STATIC") + get_tree().create_timer(STATIC_DURATION, false).timeout.connect( func(): - if not doors_closing_flag: - _screen.launch_pulse(), + if token != floor_token or doors_closing_flag: + _dbg("post-block-STATIC bail") + return + state = ScreenState.IDLE + _screen.enter_idle() + _dbg("post-block IDLE") + EventBus.active_button_changed.emit("close") + schedule_next_hack(token), CONNECT_ONE_SHOT ) func _on_doors_closing(fast: bool = false): if doors_closing_flag: return + _dbg("doors_closing fast=%s" % fast) doors_closing_flag = true EventBus.doors_closed.emit(fast) diff --git a/scenes/elevator_panel.tscn b/scenes/elevator_panel.tscn index e62a9c5..172282f 100644 --- a/scenes/elevator_panel.tscn +++ b/scenes/elevator_panel.tscn @@ -12,26 +12,26 @@ corner_radius_top_right = 4 corner_radius_bottom_right = 4 corner_radius_bottom_left = 4 +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_panel"] + [node name="ElevatorPanel" type="PanelContainer" unique_id=574176994] -anchors_preset = 6 +anchors_preset = 1 anchor_left = 1.0 -anchor_top = 0.5 anchor_right = 1.0 -anchor_bottom = 0.5 offset_left = -230.0 -offset_top = -160.0 +offset_top = 20.0 offset_right = -10.0 -offset_bottom = 160.0 +offset_bottom = 200.0 grow_horizontal = 0 -grow_vertical = 2 +theme_override_styles/panel = SubResource("StyleBoxEmpty_panel") script = ExtResource("1_1gr6t") [node name="PanelMargin" type="MarginContainer" parent="." unique_id=1575963889] layout_mode = 2 -theme_override_constants/margin_left = 15 -theme_override_constants/margin_top = 15 -theme_override_constants/margin_right = 15 -theme_override_constants/margin_bottom = 15 +theme_override_constants/margin_left = 0 +theme_override_constants/margin_top = 0 +theme_override_constants/margin_right = 0 +theme_override_constants/margin_bottom = 0 [node name="VirtuaHandTemp" type="Sprite2D" parent="PanelMargin" unique_id=14079675] visible = false @@ -45,7 +45,7 @@ texture = ExtResource("2_03crn") layout_mode = 2 [node name="Screen" type="Panel" parent="PanelMargin/PanelColumn" unique_id=1395085208] -custom_minimum_size = Vector2(160, 160) +custom_minimum_size = Vector2(190, 160) layout_mode = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_n2snw") script = ExtResource("1_3gei6") @@ -76,9 +76,3 @@ text = ">:)" [node name="Pulse" type="Node" parent="PanelMargin/PanelColumn/Screen" unique_id=988615527] script = ExtResource("2_pulse") - -[node name="CloseButton" type="Button" parent="PanelMargin/PanelColumn" unique_id=554485629] -custom_minimum_size = Vector2(0, 60) -layout_mode = 2 -theme_override_font_sizes/font_size = 28 -text = "CLOSE" diff --git a/scenes/game.tscn b/scenes/game.tscn index 9e23712..3fdc46c 100644 --- a/scenes/game.tscn +++ b/scenes/game.tscn @@ -37,6 +37,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.004168272, 1.1920929e-07, [node name="SfxDing" type="AudioStreamPlayer3D" parent="CanvasPanel/ElevatorPanel" unique_id=529226129] stream = ExtResource("4_p57ef") volume_db = 3.0 +max_polyphony = 4 [node name="SfxOpen" type="AudioStreamPlayer3D" parent="CanvasPanel/ElevatorPanel" unique_id=3954090] stream = ExtResource("5_u5sy4") @@ -62,6 +63,8 @@ stream = ExtResource("10_under") [node name="SfxRobotThankYou" type="AudioStreamPlayer3D" parent="CanvasPanel/ElevatorPanel" unique_id=1100000004] stream = ExtResource("11_thx") +[node name="SfxBlock" type="AudioStreamPlayer3D" parent="CanvasPanel/ElevatorPanel" unique_id=1100000007] + [node name="Ambience" type="AudioStreamPlayer" parent="." unique_id=1100000005] process_mode = 3 stream = ExtResource("12_amb") diff --git a/scenes/hud.gd b/scenes/hud.gd index 0873715..d7b2e20 100644 --- a/scenes/hud.gd +++ b/scenes/hud.gd @@ -1,38 +1,4 @@ extends MarginContainer -var _displayed_floor := -1 - func _ready(): - $StatsColumn/FloorLabel.text = "FLOOR: 10" - $StatsColumn/SavedLabel.text = "SAVED: 0" - $StatsColumn/PeopleLabel.text = "PEOPLE: 0/2" - $StatsColumn/ScoreLabel.text = "SCORE: 0" - - EventBus.floor_changed.connect(update_floor) - EventBus.saved_changed.connect(update_saved) - EventBus.people_changed.connect(update_people) - EventBus.score_changed.connect(update_score) - -func update_floor(floor_num: int): - if _displayed_floor == floor_num: - return - var label = $StatsColumn/FloorLabel - var new_text = "FLOOR: " + str(floor_num) - if _displayed_floor == -1: - label.text = new_text - _displayed_floor = floor_num - return - _displayed_floor = floor_num - UIUtils.flip_label_text(label, new_text) - -func update_saved(count: int): - $StatsColumn/SavedLabel.text = "SAVED: " + str(count) - -func update_people(count: int, threshold: int): - var label = $StatsColumn/PeopleLabel - label.text = "PEOPLE: " + str(count) + "/" + str(threshold) - var color = Color(0.4, 1.0, 0.4) if count >= threshold else Color(1.0, 0.4, 0.4) - label.add_theme_color_override("font_color", color) - -func update_score(new_score: int): - $StatsColumn/ScoreLabel.text = "SCORE: " + str(new_score) + pass diff --git a/scenes/hud.tscn b/scenes/hud.tscn index 30f1905..b65c0b1 100644 --- a/scenes/hud.tscn +++ b/scenes/hud.tscn @@ -8,25 +8,3 @@ offset_bottom = 40.0 theme_override_constants/margin_left = 72 theme_override_constants/margin_top = 20 script = ExtResource("1_64ctp") - -[node name="StatsColumn" type="VBoxContainer" parent="." unique_id=1476756270] -layout_mode = 2 - -[node name="PeopleLabel" type="Label" parent="StatsColumn" unique_id=1174457387] -layout_mode = 2 -theme_override_font_sizes/font_size = 24 -text = "PEOPLE: 0" - -[node name="SavedLabel" type="Label" parent="StatsColumn" unique_id=969002022] -layout_mode = 2 -theme_override_font_sizes/font_size = 24 -text = "SAVED: 0" - -[node name="FloorLabel" type="Label" parent="StatsColumn" unique_id=67147411] -layout_mode = 2 -theme_override_font_sizes/font_size = 24 -text = "FLOOR: 0" - -[node name="ScoreLabel" type="Label" parent="StatsColumn" unique_id=1595653166] -layout_mode = 2 -theme_override_font_sizes/font_size = 24 diff --git a/scenes/screen.gd b/scenes/screen.gd index 6411ed8..66a949f 100644 --- a/scenes/screen.gd +++ b/scenes/screen.gd @@ -36,6 +36,11 @@ var _quota_threshold := 0 var _quota_filled_style: StyleBoxFlat var _quota_empty_style: StyleBoxFlat +var _idle_image: TextureRect +var _static_overlay: ColorRect +var _score_label: Label +var _score_value := 0 + @onready var _pulse = $Pulse signal pulse_started(duration: float) @@ -90,6 +95,42 @@ func _ready(): EventBus.floor_changed.connect(_update_floor_label) EventBus.people_changed.connect(_update_quota_dots) + _idle_image = TextureRect.new() + _idle_image.texture = load("res://images/goatech_screen.png") + _idle_image.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + _idle_image.anchor_right = 1.0 + _idle_image.anchor_bottom = 1.0 + _idle_image.mouse_filter = Control.MOUSE_FILTER_IGNORE + _idle_image.visible = false + add_child(_idle_image) + + _static_overlay = ColorRect.new() + _static_overlay.color = Color(1.0, 1.0, 1.0, 0.6) + _static_overlay.anchor_right = 1.0 + _static_overlay.anchor_bottom = 1.0 + _static_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE + _static_overlay.visible = false + add_child(_static_overlay) + + _score_label = Label.new() + _score_label.add_theme_font_size_override("font_size", 12) + _score_label.add_theme_color_override("font_color", Color(0.2, 1, 0.2, 1)) + _score_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + _score_label.anchor_left = 0.0 + _score_label.anchor_right = 1.0 + _score_label.anchor_top = 0.0 + _score_label.anchor_bottom = 0.0 + _score_label.offset_top = QUOTA_DOT_Y + QUOTA_DOT_SIZE + 2 + _score_label.offset_bottom = QUOTA_DOT_Y + QUOTA_DOT_SIZE + 18 + _score_label.offset_right = -QUOTA_DOT_RIGHT_MARGIN + _score_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + _score_label.visible = false + _score_label.text = "SCORE: 0" + add_child(_score_label) + EventBus.score_changed.connect(_on_score_changed) + + enter_idle() + func start(floor_num: int = EventBus.STARTING_FLOOR): active = true $AIFace.text = ">:)" @@ -186,6 +227,10 @@ func _update_quota_dots(count: int, threshold: int): var style = _quota_filled_style if i < count else _quota_empty_style _quota_dots[i].add_theme_stylebox_override("panel", style) +func _on_score_changed(new_score: int): + _score_value = new_score + _score_label.text = "SCORE: " + str(new_score) + func _update_perfect_zone(): var bz = $TargetZone var width = bz.size.x * PERFECT_ZONE_RATIO @@ -235,6 +280,7 @@ func flash(color: Color): _flash_tween.tween_property(style, "bg_color", base_color, 0.3) func show_countdown(): + $AIFace.visible = true stop() $AIFace.text = "3" var tween = create_tween() @@ -243,15 +289,17 @@ func show_countdown(): tween.tween_interval(0.6) tween.tween_callback(func(): $AIFace.text = "1") tween.tween_interval(0.6) - tween.tween_callback(func(): $AIFace.text = "CLOSED") + tween.tween_callback(func(): $AIFace.text = "") func show_win(): + $AIFace.visible = true stop() $TargetZone.visible = false $AIFace.text = "ESCAPED" flash(Color.GREEN) func show_loss(message: String): + $AIFace.visible = true stop() $TargetZone.visible = false $AIFace.text = message @@ -268,6 +316,30 @@ func end_onboarding(): $AIFace.size = Vector2(size.x, 30) $TargetZone.visible = true +func enter_idle() -> void: + _idle_image.visible = true + $AIFace.visible = false + $TargetZone.visible = false + $SweepLine.visible = false + _static_overlay.visible = false + _score_label.visible = true + move_child(_score_label, get_child_count() - 1) + +func enter_static() -> void: + move_child(_static_overlay, get_child_count() - 1) + _static_overlay.visible = true + block_zone_moving = false + _score_label.visible = false + +func enter_hacked() -> void: + _idle_image.visible = false + $AIFace.visible = true + $TargetZone.visible = true + _static_overlay.visible = false + _score_label.visible = false + block_zone_moving = block_zone_speed > 0.0 + launch_pulse() + func _start_ready_pulse(): if _ready_pulse_tween: _ready_pulse_tween.kill() diff --git a/scenes/survivor.gd b/scenes/survivor.gd index 679d92e..55bc1e5 100644 --- a/scenes/survivor.gd +++ b/scenes/survivor.gd @@ -60,6 +60,7 @@ func _on_area_3d_area_entered(area: Area3D) -> void: if final_progress > 0.49: return _saved = true + EventBus.survivor_entered_elevator.emit() queue_free(), CONNECT_ONE_SHOT ) diff --git a/scenes/virtua_hand.gd b/scenes/virtua_hand.gd index 76cfed0..b99a984 100644 --- a/scenes/virtua_hand.gd +++ b/scenes/virtua_hand.gd @@ -1,25 +1,70 @@ extends Sprite3D +const SLIDE_DURATION := 0.25 const UP_DURATION := 0.1 const HOLD_DURATION := 0.05 const DOWN_DURATION := 0.18 -var _rest_position: Vector3 -var _tween: Tween - -@onready var _button: Node3D = get_parent().get_node("ElevatorButton") +var _open_button: Node3D +var _close_button: Node3D +var _target_button: Node3D +var _close_rest_x: float +var _open_rest_x: float +var _rest_y: float +var _slide_tween: Tween +var _press_tween: Tween func _ready() -> void: - _rest_position = position - EventBus.block_pressed.connect(_animate_press) + if not get_parent().is_node_ready(): + await get_parent().ready + await get_tree().process_frame + _open_button = get_parent().get_node("DoorOpen") + _close_button = get_parent().get_node("DoorClose") + _close_rest_x = _project_to_hand_plane(_close_button) + _open_rest_x = _project_to_hand_plane(_open_button) + _rest_y = position.y + _target_button = _close_button + position = Vector3(_close_rest_x, _rest_y, position.z) + print("[VirtuaHand] close_x=", _close_rest_x, " open_x=", _open_rest_x, " hand_pos=", position) + + EventBus.button_pressed.connect(_animate_press) + EventBus.active_button_changed.connect(_on_active_button_changed) + +func _project_to_hand_plane(button: Node3D) -> float: + var cam := get_viewport().get_camera_3d() + if not cam: + return button.position.x + var screen_pos := cam.unproject_position(button.global_position) + var ray_origin := cam.project_ray_origin(screen_pos) + var ray_dir := cam.project_ray_normal(screen_pos) + if absf(ray_dir.z) < 0.0001: + return button.position.x + var t := (global_position.z - ray_origin.z) / ray_dir.z + var hit := ray_origin + ray_dir * t + return get_parent().to_local(hit).x + +func _on_active_button_changed(target: String) -> void: + var new_button: Node3D = _open_button if target == "open" else _close_button + if new_button == _target_button: + return + _target_button = new_button + var target_x: float = _open_rest_x if target == "open" else _close_rest_x + print("[VirtuaHand] slide target=", target, " target_x=", target_x, " current_pos=", position) + if _press_tween and _press_tween.is_valid(): + _press_tween.kill() + if _slide_tween and _slide_tween.is_valid(): + _slide_tween.kill() + _slide_tween = create_tween() + _slide_tween.tween_property(self, "position:x", target_x, SLIDE_DURATION).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) func _animate_press() -> void: - if not is_instance_valid(_button): + if not is_instance_valid(_target_button): return - if _tween and _tween.is_valid(): - _tween.kill() - var press_pos := Vector3(_rest_position.x, _button.position.y, _rest_position.z) - _tween = create_tween() - _tween.tween_property(self, "position", press_pos, UP_DURATION).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) - _tween.tween_interval(HOLD_DURATION) - _tween.tween_property(self, "position", _rest_position, DOWN_DURATION).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN) + if _press_tween and _press_tween.is_valid(): + _press_tween.kill() + var fingertip_offset := texture.get_height() * 0.5 * pixel_size * scale.y + var press_y := _target_button.position.y - fingertip_offset + _press_tween = create_tween() + _press_tween.tween_property(self, "position:y", press_y, UP_DURATION).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) + _press_tween.tween_interval(HOLD_DURATION) + _press_tween.tween_property(self, "position:y", _rest_y, DOWN_DURATION).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN) diff --git a/scenes/world.tscn b/scenes/world.tscn index b44bc82..d35db73 100644 --- a/scenes/world.tscn +++ b/scenes/world.tscn @@ -2,7 +2,7 @@ [ext_resource type="PackedScene" uid="uid://dowvqakiqkvk5" path="res://scenes/camera_3d.tscn" id="1_f3sb7"] [ext_resource type="PackedScene" uid="uid://cxnocjdotkl5e" path="res://scenes/hall_block.tscn" id="1_tlwt5"] -[ext_resource type="PackedScene" uid="uid://brd3iponame0e" path="res://scenes/elevator.tscn" id="4_k0juu"] +[ext_resource type="PackedScene" path="res://scenes/elevator.tscn" id="4_k0juu"] [ext_resource type="PackedScene" uid="uid://bkqwi2yqa0nvg" path="res://scenes/virtua_hand.tscn" id="5_71j4m"] [ext_resource type="PackedScene" uid="uid://cwwexawpj46hk" path="res://scenes/elevator_button.tscn" id="5_qfnet"] [ext_resource type="Script" uid="uid://c6v2lhrkeup5i" path="res://scenes/world.gd" id="5_world"] @@ -39,13 +39,13 @@ unique_name_in_owner = true transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -8.351522) [node name="VirtuaHand" parent="ElevatorSafeZone" unique_id=1811028460 instance=ExtResource("5_71j4m")] -transform = Transform3D(3, 0, 0, 0, 3, 0, 0, 0, 3, -2.61384, 0.554309, -0.67426205) +transform = Transform3D(3, 0, 0, 0, 3, 0, 0, 0, 3, -2.9201458, 1.49612185, -0.67426205) [node name="DoorOpen" parent="ElevatorSafeZone" unique_id=1758559173 instance=ExtResource("5_qfnet")] -transform = Transform3D(0.35, 0, 0, 0, 0.35, 0, 0, 0, 0.35, -3.082784, 2.258, -0.39000034) +transform = Transform3D(0.35, 0, 0, 0, 0.35, 0, 0, 0, 0.35, -2.882784, 3.348, -0.39000034) [node name="DoorClose" parent="ElevatorSafeZone" unique_id=513173058 instance=ExtResource("5_qfnet")] -transform = Transform3D(0.35, 0, 0, 0, 0.35, 0, 0, 0, 0.35, -3.6393127, 2.258, -0.39000034) +transform = Transform3D(0.35, 0, 0, 0, 0.35, 0, 0, 0, 0.35, -3.4393127, 3.348, -0.39000034) frame = 1 [node name="Elevator" parent="ElevatorSafeZone" unique_id=242258467 instance=ExtResource("4_k0juu")] @@ -70,7 +70,7 @@ shape = SubResource("BoxShape3D_k0juu") [node name="ButtonPanel" parent="ElevatorSafeZone" unique_id=1270714626 instance=ExtResource("7_i7141")] transform = Transform3D(0.3, 0, 0, 0, 0.3, 0, 0, 0, 0.3, 3.4193654, 2.2598982, -0.39259052) -[node name="LeftHand" parent="ElevatorSafeZone" instance=ExtResource("8_lefth")] +[node name="LeftHand" parent="ElevatorSafeZone" unique_id=547108366 instance=ExtResource("8_lefth")] transform = Transform3D(3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0) visible = false diff --git a/scripts/event_bus.gd b/scripts/event_bus.gd index c676607..791064d 100644 --- a/scripts/event_bus.gd +++ b/scripts/event_bus.gd @@ -25,7 +25,9 @@ signal robot_stun_requested(duration: float) signal robot_floor_started(delay: float, robot_speed: float) signal robot_close_warning signal survivor_squeaked_in -signal block_pressed +signal button_pressed +signal survivor_entered_elevator signal intro_finished +signal active_button_changed(target: String) @warning_ignore_restore("unused_signal") From 8e2a2d08f0b143a33acff691c0e9f6fa8f95b605 Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Sun, 17 May 2026 19:21:41 +0100 Subject: [PATCH 4/4] Import image --- images/GOATech Screen.png.import | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 images/GOATech Screen.png.import diff --git a/images/GOATech Screen.png.import b/images/GOATech Screen.png.import new file mode 100644 index 0000000..658bbfe --- /dev/null +++ b/images/GOATech Screen.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dpugx0nudt16m" +path="res://.godot/imported/GOATech Screen.png-e2f60eeea5b51e67cce20becaa784bed.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://images/GOATech Screen.png" +dest_files=["res://.godot/imported/GOATech Screen.png-e2f60eeea5b51e67cce20becaa784bed.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