mekton boss npc uses static tekton mesh w/ emission color cycling; updated bot ghost check

This commit is contained in:
god
2026-07-06 02:20:43 +08:00
parent 783b3b5894
commit b3a421bb55
6 changed files with 104 additions and 209 deletions
+5 -66
View File
@@ -1,77 +1,16 @@
[gd_scene format=3 uid="uid://ddy2r7xto80gq"] [gd_scene load_steps=3 format=3 uid="uid://ddy2r7xto80gq"]
[ext_resource type="Script" uid="uid://du7cne5070ia0" path="res://scripts/controllers/candy_survival_npc_controller.gd" id="1_canon"] [ext_resource type="Script" uid="uid://du7cne5070ia0" path="res://scripts/controllers/candy_survival_npc_controller.gd" id="1_canon"]
[ext_resource type="PackedScene" uid="uid://df7h7y7y7y7y7" path="res://scenes/static_tekton_mesh.tscn" id="2_tekton"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_base"]
albedo_color = Color(0.15, 0.15, 0.2, 1)
metallic = 0.8
roughness = 0.2
[sub_resource type="CylinderMesh" id="CylinderMesh_base"]
material = SubResource("StandardMaterial3D_base")
top_radius = 0.8
bottom_radius = 1.2
height = 0.5
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_tank"]
albedo_color = Color(1, 0.4, 0.8, 1)
metallic = 0.2
roughness = 0.1
emission_enabled = true
emission = Color(1, 0.4, 0.8, 1)
emission_energy_multiplier = 2.0
[sub_resource type="SphereMesh" id="SphereMesh_tank"]
material = SubResource("StandardMaterial3D_tank")
radius = 0.9
height = 1.8
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_pipe"]
albedo_color = Color(0.7, 0.7, 0.8, 1)
metallic = 1.0
roughness = 0.2
[sub_resource type="CylinderMesh" id="CylinderMesh_pipe"]
material = SubResource("StandardMaterial3D_pipe")
top_radius = 0.25
bottom_radius = 0.4
height = 1.5
[sub_resource type="TorusMesh" id="TorusMesh_ring"]
material = SubResource("StandardMaterial3D_pipe")
inner_radius = 0.95
outer_radius = 1.15
rings = 32
[node name="CandySurvivalNPC" type="Node3D" unique_id=1515964905] [node name="CandySurvivalNPC" type="Node3D" unique_id=1515964905]
script = ExtResource("1_canon") script = ExtResource("1_canon")
[node name="Base" type="MeshInstance3D" parent="." unique_id=867716100] [node name="MektonMesh" parent="." unique_id=1698719440 instance=ExtResource("2_tekton")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.25, 0) transform = Transform3D(-0.5, 0, -4.37114e-08, 0, 0.5, 0, 4.37114e-08, 0, -0.5, 0, 0, 0)
mesh = SubResource("CylinderMesh_base")
[node name="Tank" type="MeshInstance3D" parent="." unique_id=2051786234]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4, 0)
mesh = SubResource("SphereMesh_tank")
[node name="Pipe" type="MeshInstance3D" parent="." unique_id=491265631]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.7, 0)
mesh = SubResource("CylinderMesh_pipe")
[node name="Ring1" type="MeshInstance3D" parent="." unique_id=1607309179]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4, 0)
mesh = SubResource("TorusMesh_ring")
[node name="Ring2" type="MeshInstance3D" parent="." unique_id=850923213]
transform = Transform3D(0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 0, 0, 1, 0, 1.4, 0)
mesh = SubResource("TorusMesh_ring")
[node name="Ring3" type="MeshInstance3D" parent="." unique_id=677024967]
transform = Transform3D(0.707107, -0.707107, 0, 0.707107, 0.707107, 0, 0, 0, 1, 0, 1.4, 0)
mesh = SubResource("TorusMesh_ring")
[node name="OmniLight3D" type="OmniLight3D" parent="." unique_id=1751121483] [node name="OmniLight3D" type="OmniLight3D" parent="." unique_id=1751121483]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.0, 0)
light_color = Color(1, 0.4, 0.8, 1) light_color = Color(1, 0.4, 0.8, 1)
light_energy = 3.0 light_energy = 3.0
omni_range = 8.0 omni_range = 8.0
+27 -9
View File
@@ -215,6 +215,21 @@ func _run_ai_tick():
print("[BotController] Action Taken: Ghost (trapped)") print("[BotController] Action Taken: Ghost (trapped)")
return return
# Priority 0.7: Candy Survival — Deliver candies to Mekton NPC
if strategic_planner and strategic_planner.is_candy_survival_mode():
var gm = strategic_planner._get_candy_survival_manager()
if gm and gm.is_active:
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
var held_color = gm.player_candy_color.get(pid, -1)
if held_color != -1 and gm.player_candies.get(pid, 0) > 0:
if held_color == gm.current_face:
# Face matches! Run to the NPC Center!
var path = strategic_planner.find_path(actor.current_position, Vector2i(8, 8))
if path.size() > 0:
_execute_move(path[0])
print("[BotController] Action Taken: Delivering candies to Mekton!")
return
# Priority 1: Tekton Management (Grab Tekton if full boost, or spawn if carrying) # Priority 1: Tekton Management (Grab Tekton if full boost, or spawn if carrying)
# Spawning while carrying is high priority; Hunting is medium priority. # Spawning while carrying is high priority; Hunting is medium priority.
if await _try_tekton_action(): if await _try_tekton_action():
@@ -266,21 +281,24 @@ func _run_ai_tick():
func _try_activate_ghost() -> bool: func _try_activate_ghost() -> bool:
"""Activate Ghost powerup when the planner reports imminent danger. """Activate Ghost powerup when the planner reports imminent danger.
Uses the existing SpecialTilesManager to activate the held ghost powerup. Uses CandySurvivalManager to activate the native ghost charges.
Returns true if activation was triggered.""" Returns true if activation was triggered."""
if not strategic_planner or not strategic_planner.is_candy_survival_mode(): if not strategic_planner or not strategic_planner.is_candy_survival_mode():
return false return false
if not strategic_planner.should_activate_ghost_now(): if not strategic_planner.should_activate_ghost_now():
return false return false
var stm = actor.get_node_or_null("SpecialTilesManager")
if not stm:
return false
if stm.has_method("activate_effect"):
stm.activate_effect(stm.SpecialEffect.INVISIBLE_MODE)
print("[BotController] %s activated Ghost powerup (trapped)" % actor.name)
return true
return false
var gm = strategic_planner._get_candy_survival_manager()
if not gm or not gm.is_active:
return false
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
if multiplayer.is_server():
gm.try_activate_ghost(pid)
else:
gm.rpc_id(1, "try_activate_ghost", pid)
return true
func _on_step_onto_unsafe() -> bool: func _on_step_onto_unsafe() -> bool:
"""Refuse to step onto a sticky/telegraphed cell and re-plan. Returns true """Refuse to step onto a sticky/telegraphed cell and re-plan. Returns true
if the bot had to abort the planned move.""" if the bot had to abort the planned move."""
+5 -5
View File
@@ -62,16 +62,16 @@ func _get_candy_survival_manager() -> Node:
return null return null
func _bot_has_ghost_powerup() -> bool: func _bot_has_ghost_powerup() -> bool:
"""Check if the bot has a ghost powerup in its SpecialTilesManager inventory.""" """Check if the bot has native ghost charges in CandySurvivalManager."""
var stm = actor.get_node_or_null("SpecialTilesManager") var gm = _get_candy_survival_manager()
if not stm: if not gm:
return false return false
return stm.inventory.get(stm.SpecialEffect.INVISIBLE_MODE, false) var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
return gm.player_ghosts.get(pid, 0) > 0
func _is_bot_ghost_active() -> bool: func _is_bot_ghost_active() -> bool:
"""Check if the bot is currently in ghost (invisible) mode.""" """Check if the bot is currently in ghost (invisible) mode."""
return actor.get("is_invisible") == true return actor.get("is_invisible") == true
func _is_overlay_unsafe(pos: Vector2i) -> bool: func _is_overlay_unsafe(pos: Vector2i) -> bool:
"""True if the cell carries a sticky or telegraphed overlay on layer 2.""" """True if the cell carries a sticky or telegraphed overlay on layer 2."""
if not enhanced_gridmap: if not enhanced_gridmap:
@@ -1,139 +1,57 @@
extends Node3D extends Node3D
class_name CandySurvivalNPCController class_name CandySurvivalNPCController
@export var is_static_turret: bool = true @onready var mekton_mesh = $MektonMesh
var face_material: StandardMaterial3D
@onready var ring1 = $Ring1 if has_node("Ring1") else null
@onready var ring2 = $Ring2 if has_node("Ring2") else null
@onready var ring3 = $Ring3 if has_node("Ring3") else null
@onready var tank = $Tank if has_node("Tank") else null
func _ready() -> void: func _ready() -> void:
_apply_outline_recursive(self) # Setup unique material for the body to change colors
if mekton_mesh:
var skeleton = mekton_mesh.get_node_or_null("Throwing Tiles/Skeleton3D")
if skeleton:
var body = skeleton.get_node_or_null("ted_body")
if body and body is MeshInstance3D:
var mat = body.get_surface_override_material(0)
if not mat:
if body.mesh:
mat = body.mesh.surface_get_material(0)
func _apply_outline_recursive(node: Node) -> void: if mat:
if node is MeshInstance3D and node.mesh: face_material = mat.duplicate()
# Some procedural meshes might not have get_surface_count or it might be 1. else:
# If the mesh has material_override, apply outline to that face_material = StandardMaterial3D.new()
if node.material_override and node.material_override is StandardMaterial3D:
var mat = node.material_override
if not mat.next_pass:
var outline_mat = ShaderMaterial.new()
outline_mat.shader = load("res://assets/shaders/outline3d.gdshader")
mat.next_pass = outline_mat
else:
for i in range(node.mesh.get_surface_count()):
var mat = node.get_active_material(i)
if mat:
if not mat.next_pass:
var unique_mat = mat.duplicate()
var outline_mat = ShaderMaterial.new()
outline_mat.shader = load("res://assets/shaders/outline3d.gdshader")
unique_mat.next_pass = outline_mat
node.set_surface_override_material(i, unique_mat)
if node is GPUParticles3D and node.draw_pass_1 and node.draw_pass_1.material: face_material.emission_enabled = true
var mat = node.draw_pass_1.material face_material.emission_energy_multiplier = 2.0
if not mat.next_pass: body.set_surface_override_material(0, face_material)
var outline_mat = ShaderMaterial.new()
outline_mat.shader = load("res://assets/shaders/outline3d.gdshader")
mat.next_pass = outline_mat
for child in node.get_children(): @rpc("authority", "call_local", "reliable")
_apply_outline_recursive(child) func set_face_color_rpc(color_index: int) -> void:
if not face_material: return
func _process(delta: float) -> void: # enum CandyColor { HEART=0, DIAMOND=1, STAR=2, COIN=3 }
if ring1: ring1.rotate_y(delta * 1.5) var c = Color.WHITE
if ring2: ring2.rotate_x(delta * -1.0) match color_index:
if ring3: ring3.rotate_z(delta * 1.2) 0: c = Color(1.0, 0.2, 0.2) # HEART (Red)
1: c = Color(0.2, 0.4, 1.0) # DIAMOND (Blue)
2: c = Color(1.0, 0.9, 0.2) # STAR (Yellow)
3: c = Color(1.0, 0.6, 0.0) # COIN (Orange/Gold)
if tank and tank.mesh and tank.mesh.material: face_material.albedo_color = c
var mat = tank.mesh.material as StandardMaterial3D face_material.emission = c
if mat:
# Gentle pulse of the candy tank # Also tint the OmniLight if we have one
var pulse = (sin(Time.get_ticks_msec() / 300.0) + 1.0) * 0.5 var light = $OmniLight3D
mat.emission_energy_multiplier = 1.0 + (pulse * 2.0) if light:
light.light_color = c
@rpc("authority", "call_local", "reliable") @rpc("authority", "call_local", "reliable")
func play_animation_rpc(anim_name: String) -> void: func play_animation_rpc(anim_name: String) -> void:
# Stub for future model animations if mekton_mesh:
pass var anim = mekton_mesh.get_node_or_null("AnimationPlayer")
if anim and anim.has_animation(anim_name):
func spawn_projectile(target_world_pos: Vector3, duration: float) -> void: anim.play(anim_name)
var projectile = MeshInstance3D.new()
var sphere = SphereMesh.new()
sphere.radius = 0.3
sphere.height = 0.6
projectile.mesh = sphere
var mat = StandardMaterial3D.new()
mat.albedo_color = Color(1.0, 0.4, 0.8) # Candy pink for Candy Survival
mat.emission_enabled = true
mat.emission = Color(1.0, 0.4, 0.8)
mat.emission_energy_multiplier = 3.0
projectile.material_override = mat
projectile.top_level = true
add_child(projectile)
# Start projectile slightly above the cannon center
projectile.global_position = global_position + Vector3(0, 3.0, 0)
var tween = create_tween()
if not tween:
projectile.queue_free()
return
# VFX trail
var particles = GPUParticles3D.new()
var pmat = ParticleProcessMaterial.new()
pmat.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
pmat.emission_sphere_radius = 0.3
pmat.gravity = Vector3(0, 0, 0)
pmat.scale_min = 0.1
pmat.scale_max = 0.3
particles.process_material = pmat
var pmesh = SphereMesh.new()
pmesh.radius = 0.1
pmesh.height = 0.2
var spatial_mat = StandardMaterial3D.new()
spatial_mat.albedo_color = Color(1.0, 0.6, 0.9)
spatial_mat.emission_enabled = true
spatial_mat.emission = Color(1.0, 0.6, 0.9)
pmesh.material = spatial_mat
particles.draw_pass_1 = pmesh
particles.amount = 16
particles.lifetime = 0.4
projectile.add_child(particles)
_apply_outline_recursive(projectile)
tween.set_parallel(true)
tween.tween_property(projectile, "global_position:x", target_world_pos.x, duration).set_trans(Tween.TRANS_LINEAR)
tween.tween_property(projectile, "global_position:z", target_world_pos.z, duration).set_trans(Tween.TRANS_LINEAR)
var mid_y = max(global_position.y, target_world_pos.y) + 4.0
var tween_y = create_tween()
# Important: Make sure both halves of the y-tween together equal `duration`
tween_y.tween_property(projectile, "global_position:y", mid_y, duration / 2.0).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
tween_y.tween_property(projectile, "global_position:y", target_world_pos.y, duration / 2.0).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
# Add some spin to the projectile
var spin_tween = create_tween()
spin_tween.set_loops()
spin_tween.tween_property(projectile, "rotation", Vector3(PI*2, PI*2, PI*2), 0.5).as_relative()
# We need to wait for the X/Z tween to finish, but since it's parallel we can just use a separate timer or tween
# to kill the projectile exactly when duration is reached, ensuring it doesn't get killed early by X/Z finishing 1 frame earlier than Y
get_tree().create_timer(duration).timeout.connect(func():
if is_instance_valid(spin_tween):
spin_tween.kill()
if is_instance_valid(projectile):
projectile.queue_free()
)
func can_rpc() -> bool: func can_rpc() -> bool:
if not multiplayer.has_multiplayer_peer(): return false if not multiplayer.has_multiplayer_peer(): return false
return multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED return multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED
@@ -172,6 +172,11 @@ func _process(delta: float) -> void:
face_timer = 0.0 face_timer = 0.0
current_face = CandyColor.values()[(current_face + 1) % CandyColor.size()] current_face = CandyColor.values()[(current_face + 1) % CandyColor.size()]
# Sync color to the Mekton NPC
if mekton_node and mekton_node.has_method("set_face_color_rpc"):
mekton_node.rpc("set_face_color_rpc", current_face)
# Sugar Rush timers # Sugar Rush timers
var any_rush = false var any_rush = false
for pid in player_sugar_rush.keys(): for pid in player_sugar_rush.keys():
@@ -256,7 +261,11 @@ func get_multiplier(pid: int) -> float:
# ── Mekton Delivery ── # ── Mekton Delivery ──
@rpc("any_peer", "call_local", "reliable")
func try_deliver(pid: int) -> bool: func try_deliver(pid: int) -> bool:
if not multiplayer.is_server():
return false
var color = player_candy_color.get(pid, -1) var color = player_candy_color.get(pid, -1)
if color == -1 or player_candies.get(pid, 0) == 0: if color == -1 or player_candies.get(pid, 0) == 0:
return false return false
@@ -384,6 +393,8 @@ func _spawn_mekton_npc(center: Vector2i) -> Node:
var npc = load(path).instantiate() var npc = load(path).instantiate()
npc.position = Vector3(center.x + 1, 0, center.y + 1) npc.position = Vector3(center.x + 1, 0, center.y + 1)
add_child(npc) add_child(npc)
if npc.has_method("set_face_color_rpc"):
npc.set_face_color_rpc(current_face)
return npc return npc
return null return null
@@ -165,6 +165,15 @@ func simple_move_to(grid_position: Vector2i) -> bool:
print("[Move] Failed: Blocked by Candy Survival Sticky cell at %s" % grid_position) print("[Move] Failed: Blocked by Candy Survival Sticky cell at %s" % grid_position)
return false return false
if gm and gm.is_active and gm.has_method("try_deliver"):
var dist = abs(grid_position.x - 8) + abs(grid_position.y - 8)
if dist <= 2:
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
if multiplayer.is_server():
gm.try_deliver(pid)
else:
gm.rpc_id(1, "try_deliver", pid)
rotate_towards_target(grid_position) rotate_towards_target(grid_position)
rotate_towards_target(grid_position) rotate_towards_target(grid_position)