From fb9159d646a9c374d66d620408f327c70a5c3c6f Mon Sep 17 00:00:00 2001 From: PsychoPinkQ Date: Sat, 18 Jul 2026 05:23:20 +0800 Subject: [PATCH 1/4] fix: demo player shadow & energy label --- Scripts/demo_renderer.gd | 70 +++++++++++++++++++++++++++++ Scripts/energy_ball.gd | 11 ++++- Scripts/network/state_serializer.gd | 10 +++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/Scripts/demo_renderer.gd b/Scripts/demo_renderer.gd index 75277ae8..c4c0a8b8 100644 --- a/Scripts/demo_renderer.gd +++ b/Scripts/demo_renderer.gd @@ -6,6 +6,8 @@ extends Node const ProxyClass = preload("res://Scripts/demo_player_proxy.gd") const FALLBACK_HEALTH_TEXTURE = preload("res://Shapes/feather.svg") +const ENERGY_TEXT_SCENE = preload("res://Scenes/energy_text.tscn") +const PLAYER_SHADOW_TEXTURE = preload("res://Shapes/Circle.svg") @export var stage_a: Node2D @export var stage_b: Node2D @@ -77,9 +79,11 @@ func _ready() -> void: { "ghosts": _ghosts_a, "balls": _balls_a, + "energy_collect_seq": {}, "stage": stage_a, "high_stage": high_stage_a, "player": _player_a, + "shadow": null, "prev_player": {}, "proxy": null, "loaded_skin_id": "", @@ -94,9 +98,11 @@ func _ready() -> void: { "ghosts": _ghosts_b, "balls": _balls_b, + "energy_collect_seq": {}, "stage": stage_b, "high_stage": high_stage_b, "player": _player_b, + "shadow": null, "prev_player": {}, "proxy": null, "loaded_skin_id": "", @@ -308,6 +314,14 @@ func _update_energy_balls(screen: Dictionary, stage: Node2D, balls_array: Array) if ball_id == -1: continue + var collect_seq_by_id: Dictionary = screen["energy_collect_seq"] + var collect_seq: int = ball_data.get("collect_seq", 0) + if collect_seq_by_id.has(ball_id): + var previous_seq: int = collect_seq_by_id.get(ball_id, 0) + if collect_seq > previous_seq: + _spawn_demo_energy_text(screen, ball_data) + collect_seq_by_id[ball_id] = collect_seq + var collected: bool = ball_data.get("collected", false) var combo: int = ball_data.get("combo", 0) if collected: @@ -325,6 +339,7 @@ func _update_energy_balls(screen: Dictionary, stage: Node2D, balls_array: Array) var ghost: Node2D = balls[ball_id] if is_instance_valid(ghost): ghost.global_position = pos + ghost.scale = ball_data.get("scale", ghost.scale) ghost.visible = true ghost.now_combo = combo else: @@ -335,6 +350,7 @@ func _update_energy_balls(screen: Dictionary, stage: Node2D, balls_array: Array) stage.add_child(ghost) _suppress_energy_ball(ghost) ghost.global_position = pos + ghost.scale = ball_data.get("scale", Vector2.ONE) ghost.visible = true balls[ball_id] = ghost @@ -345,6 +361,33 @@ func _update_energy_balls(screen: Dictionary, stage: Node2D, balls_array: Array) if is_instance_valid(old): old.queue_free() balls.erase(ball_id) + screen["energy_collect_seq"].erase(ball_id) + + +func _spawn_demo_energy_text(screen: Dictionary, ball_data: Dictionary) -> void: + var stage: Node2D = screen.get("stage", null) as Node2D + if not stage: + return + + var gain: int = ball_data.get("last_collect_gain", 0) + if gain <= 0: + return + var combo: int = ball_data.get("last_collect_combo", 0) + combo = mini(maxi(combo, 0), 5) + var pos: Vector2 = ball_data.get( + "last_collect_position", ball_data.get("position", Vector2.ZERO) + ) + + var text := ENERGY_TEXT_SCENE.instantiate() as Label + if not text: + return + stage.add_child(text) + text.text = "+%d" % gain + text.z_index = Util.LAYERS.get("EnergyBall", 0) + 1 + var text_offset: Vector2 = Vector2(text.size.x / 2.0, 0.0) + text.global_position = pos - text_offset + if text.has_method("initialize"): + text.initialize(combo) ## Lightweight suppression for energy ball ghosts: keeps _physics_process @@ -414,6 +457,7 @@ func _apply_player(screen: Dictionary, stage: Node2D, player_state: Dictionary) # Update position var pos: Vector2 = player_state.get("position", Vector2.ZERO) var sprite_y: float = player_state.get("sprite_y", 0.0) + _update_player_shadow(screen, stage, pos, sprite_y, player_state.get("is_jumping", false)) ghost.global_position = Vector2(pos.x, pos.y - sprite_y) # Update velocity for skin _process() animations @@ -460,6 +504,32 @@ func _apply_player(screen: Dictionary, stage: Node2D, player_state: Dictionary) } +func _update_player_shadow( + screen: Dictionary, stage: Node2D, pos: Vector2, sprite_y: float, is_jumping: bool +) -> void: + if not stage: + return + var shadow: Sprite2D = screen.get("shadow", null) + if not is_instance_valid(shadow): + shadow = Sprite2D.new() + shadow.name = "PlayerShadowGhost" + shadow.texture = PLAYER_SHADOW_TEXTURE + shadow.z_index = Util.LAYERS.get("Player/ShadowSprite", 0) + shadow.modulate = Color(0, 0, 0, 0.5) + stage.add_child(shadow) + screen["shadow"] = shadow + + shadow.global_position = pos + var should_show: bool = is_jumping or sprite_y > 0.0 + shadow.visible = should_show + if not should_show: + return + + var jump_ratio: float = clamp(sprite_y / 120.0, 0.0, 1.0) + shadow.scale = Vector2.ONE * lerp(0.2, 0.1, jump_ratio) + shadow.modulate.a = lerp(0.5, 0.15, jump_ratio) + + ## Updates the energy HUD for [param screen] from [param energy] and [param max_energy]. func _update_energy_hud(screen: Dictionary, energy: int, max_energy: int) -> void: var label = screen["energy_label"] diff --git a/Scripts/energy_ball.gd b/Scripts/energy_ball.gd index 8ca0733d..c68ad9f8 100644 --- a/Scripts/energy_ball.gd +++ b/Scripts/energy_ball.gd @@ -15,6 +15,10 @@ var now_combo := 0 var time_elapsed := 0.0 var energy_gain: Array var energy_ball_phase: int = 0 +var collect_seq := 0 +var last_collect_gain := 0 +var last_collect_combo := 0 +var last_collect_position := Vector2.ZERO var rng := RandomNumberGenerator.new() @@ -52,8 +56,13 @@ func _reload_from_game_data() -> void: func _on_body_entered(body: Node2D) -> void: if collision_shape.disabled or body != player_node: return + var gain: int = energy_gain[energy_ball_phase][now_combo] + collect_seq += 1 + last_collect_gain = gain + last_collect_combo = now_combo + last_collect_position = global_position collision_shape.set_deferred("disabled", true) - Global.energyball_collected.emit(energy_gain[energy_ball_phase][now_combo]) + Global.energyball_collected.emit(gain) Audio.play_coconut_sfx(now_combo) _spawn_energy_text() now_combo = min(now_combo + 1, energy_gain[energy_ball_phase].size() - 1) diff --git a/Scripts/network/state_serializer.gd b/Scripts/network/state_serializer.gd index 008e0e98..fd506024 100644 --- a/Scripts/network/state_serializer.gd +++ b/Scripts/network/state_serializer.gd @@ -162,8 +162,18 @@ func _serialize_energy_balls() -> Array[Dictionary]: { "id": child.get_instance_id(), "position": child.global_position if child is Node2D else Vector2.ZERO, + "scale": child.scale if child is Node2D else Vector2.ONE, "collected": not child.visible if "visible" in child else false, "combo": child.get("now_combo") if "now_combo" in child else 0, + "collect_seq": child.get("collect_seq") if "collect_seq" in child else 0, + "last_collect_gain": + child.get("last_collect_gain") if "last_collect_gain" in child else 0, + "last_collect_combo": + child.get("last_collect_combo") if "last_collect_combo" in child else 0, + "last_collect_position": + child.get("last_collect_position") + if "last_collect_position" in child + else Vector2.ZERO, } ) ) From 2a8bb45a20394f2f91d7548725b087ac5dbceed9 Mon Sep 17 00:00:00 2001 From: PsychoPinkQ Date: Sat, 18 Jul 2026 05:27:03 +0800 Subject: [PATCH 2/4] chore: pass hook --- Scripts/demo_renderer.gd | 2 +- Scripts/network/state_serializer.gd | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Scripts/demo_renderer.gd b/Scripts/demo_renderer.gd index c4c0a8b8..99d6c2bb 100644 --- a/Scripts/demo_renderer.gd +++ b/Scripts/demo_renderer.gd @@ -631,7 +631,7 @@ func _load_skin(skin_id: String) -> Node2D: func _create_player_ghost(stage: Node2D) -> Node2D: var ghost := Sprite2D.new() ghost.name = "PlayerGhost" - ghost.texture = preload("res://Shapes/Circle.svg") + ghost.texture = PLAYER_SHADOW_TEXTURE ghost.scale = Vector2(0.2, 0.2) ghost.z_index = Util.LAYERS["Player/BodySprite"] ghost.modulate = Color(0.2, 0.6, 1.0, 1.0) diff --git a/Scripts/network/state_serializer.gd b/Scripts/network/state_serializer.gd index fd506024..bdba77a2 100644 --- a/Scripts/network/state_serializer.gd +++ b/Scripts/network/state_serializer.gd @@ -171,9 +171,11 @@ func _serialize_energy_balls() -> Array[Dictionary]: "last_collect_combo": child.get("last_collect_combo") if "last_collect_combo" in child else 0, "last_collect_position": - child.get("last_collect_position") - if "last_collect_position" in child - else Vector2.ZERO, + ( + child.get("last_collect_position") + if "last_collect_position" in child + else Vector2.ZERO + ), } ) ) From f872853f045cbb01c8a50dd3b7d6c714bcd42ed8 Mon Sep 17 00:00:00 2001 From: PsychoPinkQ Date: Sat, 18 Jul 2026 06:41:00 +0800 Subject: [PATCH 3/4] fix: demo trap9 display --- Scripts/traps/trap9-mortar.gd | 117 ++++++++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 11 deletions(-) diff --git a/Scripts/traps/trap9-mortar.gd b/Scripts/traps/trap9-mortar.gd index 515cf03b..565b0f34 100644 --- a/Scripts/traps/trap9-mortar.gd +++ b/Scripts/traps/trap9-mortar.gd @@ -29,6 +29,8 @@ var explosion_radius: float = 0.0 var shell_vertical_speed: float = 0.0 var my_shell_rotate_speed: float var is_demo := false +var demo_shell: Sprite2D +var demo_shadow: Sprite2D @onready var shell: Sprite2D = $ShellShadow/Shell @onready var shell_shadow: Node2D = $ShellShadow @@ -41,12 +43,85 @@ var is_demo := false func _ready() -> void: + _setup_layers() if is_demo: + _setup_demo_display() return - $ShellShadow/Shell.z_index = Util.LAYERS["Trap9Mortar/Shell"] - $ShellShadow/Shadow.z_index = Util.LAYERS["Trap9Mortar/Shadow"] - $Explosion.z_index = Util.LAYERS["Trap9Mortar/Explosion"] - shell.reparent(Global.high_stage) + _reparent_shell_to_high_stage() + + +func _exit_tree() -> void: + if is_instance_valid(shell) and shell.get_parent() != shell_shadow: + if not shell.is_queued_for_deletion(): + shell.queue_free() + + +func _setup_layers() -> void: + shell.z_index = Util.LAYERS["Trap9Mortar/Shell"] + shadow.z_index = Util.LAYERS["Trap9Mortar/Shadow"] + explosion.z_index = Util.LAYERS["Trap9Mortar/Explosion"] + + +func _reparent_shell_to_high_stage() -> void: + if Global.high_stage and shell.get_parent() != Global.high_stage: + shell.reparent(Global.high_stage) + + +func _setup_demo_display() -> void: + shell_shadow.visible = false + shell.visible = false + shadow.visible = false + + +func _create_demo_sprite(sprite_name: String, source: Sprite2D, layer: int) -> Sprite2D: + var sprite := Sprite2D.new() + sprite.name = sprite_name + sprite.texture = source.texture + sprite.scale = source.scale + sprite.centered = source.centered + sprite.offset = source.offset + sprite.top_level = true + sprite.z_as_relative = false + sprite.z_index = layer + sprite.visible = false + add_child(sprite) + return sprite + + +func _ensure_demo_visuals() -> void: + if not is_instance_valid(demo_shadow): + demo_shadow = _create_demo_sprite( + "DemoMortarShadow", shadow, Util.LAYERS["Trap9Mortar/Shadow"] + ) + if not is_instance_valid(demo_shell): + demo_shell = _create_demo_sprite("DemoMortarShell", shell, Util.LAYERS["Trap9Mortar/Shell"]) + + +func _apply_demo_visuals( + data: Dictionary, + shadow_position: Vector2, + shell_position: Vector2, + shell_rotation: float, + explosion_visible: bool +) -> void: + _ensure_demo_visuals() + _setup_demo_display() + + var show_shadow: bool = data.get("shadow_visible", not explosion_visible) + show_shadow = show_shadow and data.get("shell_shadow_visible", not explosion_visible) + show_shadow = show_shadow and not explosion_visible + var show_shell: bool = data.get("shell_visible", not explosion_visible) + show_shell = show_shell and not explosion_visible + + demo_shadow.global_position = shadow_position + demo_shadow.rotation = shell_rotation + demo_shadow.self_modulate = data.get("shadow_modulate", Color.BLACK) + demo_shadow.visible = show_shadow + + demo_shell.global_position = shell_position + demo_shell.rotation = shell_rotation + demo_shell.self_modulate = Color.WHITE + demo_shell.visible = show_shell static func initialize(start_pos: Vector2, end_pos: Vector2, air_time: float) -> Trap9Mortar: @@ -139,9 +214,13 @@ func serialize_state() -> Dictionary: "type": "trap9-mortar", "position": global_position, "shadow_position": shell_shadow.global_position, + "demo_shell_position": shell_shadow.global_position + Vector2(0.0, remote_shell.position.y), "shell_y": remote_shell.position.y, "shell_rotation": remote_shell.rotation, "shadow_modulate": shadow.self_modulate, + "shadow_visible": shadow.visible, + "shell_visible": shell.visible, + "shell_shadow_visible": shell_shadow.visible, "explosion_visibility": explosion.visible, "modulate": modulate, "particle_emitting": effect.emitting @@ -149,13 +228,29 @@ func serialize_state() -> Dictionary: func apply_demo_state(data: Dictionary) -> void: - shell_shadow.global_position = data.get("shadow_position", Vector2.ZERO) + var shadow_position: Vector2 = data.get("shadow_position", Vector2.ZERO) + var shell_position: Vector2 = data.get( + "demo_shell_position", shadow_position + Vector2(0.0, data.get("shell_y", 0.0)) + ) + var shell_rotation: float = data.get("shell_rotation", 0.0) + var explosion_visible: bool = data.get("explosion_visibility", false) + global_position = data.get("position", Vector2.ZERO) - shell.position.y = data.get("shell_y", 0.0) - shell.rotation = data.get("shell_rotation", 0.0) - shadow.rotation = shell.rotation - shadow.self_modulate = data.get("shadow_modulate", Color.BLACK) - explosion.visible = data.get("explosion_visibility", false) - shell_shadow.visible = !explosion.visible + explosion.visible = explosion_visible modulate = data.get("modulate", Color.WHITE) effect.emitting = data.get("particle_emitting", false) + + if is_demo: + _apply_demo_visuals( + data, shadow_position, shell_position, shell_rotation, explosion_visible + ) + return + + shell_shadow.global_position = shadow_position + shell.global_position = shell_position + shell.rotation = shell_rotation + shadow.rotation = shell_rotation + shadow.self_modulate = data.get("shadow_modulate", Color.BLACK) + shell_shadow.visible = data.get("shell_shadow_visible", not explosion_visible) + shadow.visible = data.get("shadow_visible", not explosion_visible) + shell.visible = data.get("shell_visible", not explosion_visible) From bf00f0b2beca646a1087b0b2629995dd792f62db Mon Sep 17 00:00:00 2001 From: PsychoPinkQ Date: Sat, 18 Jul 2026 06:57:50 +0800 Subject: [PATCH 4/4] fix: reparent demo energy label --- Scripts/demo_renderer.gd | 31 +++++++++++++++++++++++++------ Scripts/energy_label.gd | 8 +++++++- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/Scripts/demo_renderer.gd b/Scripts/demo_renderer.gd index 99d6c2bb..d00ca728 100644 --- a/Scripts/demo_renderer.gd +++ b/Scripts/demo_renderer.gd @@ -365,10 +365,6 @@ func _update_energy_balls(screen: Dictionary, stage: Node2D, balls_array: Array) func _spawn_demo_energy_text(screen: Dictionary, ball_data: Dictionary) -> void: - var stage: Node2D = screen.get("stage", null) as Node2D - if not stage: - return - var gain: int = ball_data.get("last_collect_gain", 0) if gain <= 0: return @@ -381,15 +377,38 @@ func _spawn_demo_energy_text(screen: Dictionary, ball_data: Dictionary) -> void: var text := ENERGY_TEXT_SCENE.instantiate() as Label if not text: return - stage.add_child(text) + if stage_layer: + stage_layer.add_child(text) + else: + var stage: Node2D = screen.get("stage", null) as Node2D + if not stage: + text.queue_free() + return + stage.add_child(text) text.text = "+%d" % gain text.z_index = Util.LAYERS.get("EnergyBall", 0) + 1 var text_offset: Vector2 = Vector2(text.size.x / 2.0, 0.0) - text.global_position = pos - text_offset + if stage_layer: + text.global_position = _world_to_demo_canvas(screen, pos) - text_offset + else: + text.global_position = pos - text_offset if text.has_method("initialize"): text.initialize(combo) +func _world_to_demo_canvas(screen: Dictionary, world_position: Vector2) -> Vector2: + var stage: Node2D = screen.get("stage", null) as Node2D + if not stage: + return world_position + + var viewport := stage.get_viewport() + var canvas_position: Vector2 = viewport.canvas_transform * world_position + var viewport_parent := viewport.get_parent() + if viewport_parent is Control: + canvas_position += (viewport_parent as Control).get_global_rect().position + return canvas_position + + ## Lightweight suppression for energy ball ghosts: keeps _physics_process ## for coconut animation, but disables collision and _process. func _suppress_energy_ball(node: Node) -> void: diff --git a/Scripts/energy_label.gd b/Scripts/energy_label.gd index efefb3a4..4820538a 100644 --- a/Scripts/energy_label.gd +++ b/Scripts/energy_label.gd @@ -9,6 +9,7 @@ var time: float = 0 func _ready() -> void: scale = Vector2(0.8, 0.8) energy_label.text = "0" + _refresh_limitation_position() func _process(delta: float) -> void: @@ -18,8 +19,13 @@ func _process(delta: float) -> void: func _update_energy(energy_amount: int) -> void: energy_label.text = str(energy_amount) - limitation_label.position = Vector2(energy_label.text.length() * 32 + 52, 0) + _refresh_limitation_position() func _update_max_energy(max_energy: int) -> void: limitation_label.text = "/%d" % max_energy + _refresh_limitation_position() + + +func _refresh_limitation_position() -> void: + limitation_label.position = Vector2(energy_label.text.length() * 32 + 52, 0)