feat: Implement core player character logic, including state management, network synchronization, character selection, and manager initialization.

This commit is contained in:
Yogi Wiguna
2026-02-12 11:02:30 +08:00
parent 786e73dbaf
commit da858c12aa
7 changed files with 206 additions and 20 deletions
@@ -10,6 +10,7 @@
[ext_resource type="ArrayMesh" uid="uid://bqvqj3fhf5x51" path="res://assets/models/tiles/tile_ghost.tres" id="6_r32il"] [ext_resource type="ArrayMesh" uid="uid://bqvqj3fhf5x51" path="res://assets/models/tiles/tile_ghost.tres" id="6_r32il"]
[ext_resource type="ArrayMesh" uid="uid://cv4bedhida00g" path="res://assets/models/tiles/tile_star.tres" id="7_p5epg"] [ext_resource type="ArrayMesh" uid="uid://cv4bedhida00g" path="res://assets/models/tiles/tile_star.tres" id="7_p5epg"]
[ext_resource type="ArrayMesh" uid="uid://gpnl4cjrivor" path="res://assets/models/tiles/tile_speed.tres" id="7_sx8rm"] [ext_resource type="ArrayMesh" uid="uid://gpnl4cjrivor" path="res://assets/models/tiles/tile_speed.tres" id="7_sx8rm"]
[ext_resource type="ArrayMesh" uid="uid://dx41n2x8v30r1" path="res://assets/models/meshes/crack.res" id="10_r32il"]
[ext_resource type="Texture2D" uid="uid://dpkx1a780pvwv" path="res://assets/textures/tile_diamond.png" id="10_sx8rm"] [ext_resource type="Texture2D" uid="uid://dpkx1a780pvwv" path="res://assets/textures/tile_diamond.png" id="10_sx8rm"]
[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_5d0gc"] [sub_resource type="CompressedTexture2D" id="CompressedTexture2D_5d0gc"]
@@ -120,7 +121,8 @@ item/5/mesh_cast_shadow = 1
item/5/shapes = [] item/5/shapes = []
item/5/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) item/5/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
item/5/navigation_layers = 1 item/5/navigation_layers = 1
item/6/name = "empty" item/6/name = "crack"
item/6/mesh = ExtResource("10_r32il")
item/6/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) item/6/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
item/6/mesh_cast_shadow = 1 item/6/mesh_cast_shadow = 1
item/6/shapes = [] item/6/shapes = []
Binary file not shown.
Binary file not shown.
+1 -2
View File
@@ -4,7 +4,6 @@
resource_name = "tile_a1" resource_name = "tile_a1"
cull_mode = 2 cull_mode = 2
albedo_color = Color(0.335217, 0.328683, 0.29189, 1) albedo_color = Color(0.335217, 0.328683, 0.29189, 1)
metallic = 1.0
roughness = 0.5 roughness = 0.5
emission_enabled = true emission_enabled = true
emission = Color(0.62, 0, 0, 1) emission = Color(0.8235294, 0, 0, 1)
+92 -9
View File
@@ -975,6 +975,8 @@ func _physics_process(delta):
func _unhandled_input(event): func _unhandled_input(event):
# Handle power-up usage # Handle power-up usage
if event.is_action_pressed("use_powerup") and is_multiplayer_authority(): if event.is_action_pressed("use_powerup") and is_multiplayer_authority():
if is_frozen:
return
if powerup_manager and powerup_manager.can_use_special(): if powerup_manager and powerup_manager.can_use_special():
powerup_manager.use_special_effect() powerup_manager.use_special_effect()
return return
@@ -1834,16 +1836,34 @@ func sync_grab_tekton(tekton_path: NodePath):
print("[Player %s] Grabbed Tekton %s" % [name, tekton.name]) print("[Player %s] Grabbed Tekton %s" % [name, tekton.name])
func throw_tekton(): func throw_tekton():
if not is_multiplayer_authority() or not is_carrying_tekton: if not is_multiplayer_authority() or not is_carrying_tekton or is_frozen:
return return
# Determine throw direction (where player is facing) # Determine throw direction (where player is facing)
# For simplicity, we use the player's current rotation to find the target tile # NOTE: Movement manager uses atan2(x, z) which implies 0 rotation = +Z facing.
var forward = - global_transform.basis.z.normalized() # So we must use +basis.z (Positive Z) as forward, not standard -basis.z.
var forward = global_transform.basis.z.normalized()
var throw_dir = Vector2i(round(forward.x), round(forward.z)) var throw_dir = Vector2i(round(forward.x), round(forward.z))
if throw_dir == Vector2i.ZERO: throw_dir = Vector2i(1, 0) # Fallback if throw_dir == Vector2i.ZERO: throw_dir = Vector2i(1, 0) # Fallback
var target_pos = current_position + throw_dir # Calculate distance (5 to 7 tiles)
var rng = RandomNumberGenerator.new()
rng.randomize()
var distance = rng.randi_range(5, 7)
var target_pos = current_position + (throw_dir * distance)
# Clamp to grid bounds if possible, or just check validity
if enhanced_gridmap:
# Simple clamp assuming 0-based indexing and knowing size would be better,
# but if we don't have size, we can just raycast or check validity step by step?
# Let's just try the target. If invalid, maybe pull back?
# Or just let it land "off map" and handle it?
# Better: Clamp to grid dimensions if known.
# EnhancedGridMap usually has columns/rows.
if "columns" in enhanced_gridmap and "rows" in enhanced_gridmap:
target_pos.x = clamp(target_pos.x, 0, enhanced_gridmap.columns - 1)
target_pos.y = clamp(target_pos.y, 0, enhanced_gridmap.rows - 1)
if is_multiplayer_authority(): if is_multiplayer_authority():
rpc("sync_throw_tekton", target_pos) rpc("sync_throw_tekton", target_pos)
@@ -1856,13 +1876,76 @@ func sync_throw_tekton(target_pos: Vector2i):
is_carrying_tekton = false is_carrying_tekton = false
tekton.set_carried(false) tekton.set_carried(false)
# Move Tekton to target pos # Visual Arc Tween
tekton.current_position = target_pos var start_pos = tekton.global_position
# Target world position
var end_world_pos = Vector3(
target_pos.x * cell_size.x + cell_size.x * 0.5,
cell_size.y, # Floor Y
target_pos.y * cell_size.z + cell_size.z * 0.5
) + cell_offset
# Intensity 0.5 for throw (drops 50% tiles) var mid_pos = (start_pos + end_world_pos) / 2.0
tekton.on_hit(self , 0.5) mid_pos.y += 4.0 # Arc height
print("[Player %s] Threw Tekton to %s" % [name, target_pos]) var tween = create_tween()
tween.set_parallel(true)
# We can use a curve or just simple jump logic.
# For a nice arc in 3D: Tween X/Z linearly, Tween Y with ease out/in (bounce-like)?
# Or use a method that interpolates a curve.
# Simple approach: Tween 'position' effectively? No, linear pos is straight line.
# Let's use a value tween and update position manually, or just use a parabolic path helper?
# Easiest readable way: likely just tweening horizontal and vertical separately if we could.
# Let's stick to a simple "Jump" tween:
# 1. Move X/Z linearly to target
tween.tween_property(tekton, "global_position:x", end_world_pos.x, 0.6).set_trans(Tween.TRANS_LINEAR)
tween.tween_property(tekton, "global_position:z", end_world_pos.z, 0.6).set_trans(Tween.TRANS_LINEAR)
# 2. Move Y up then down
var jump_tween = create_tween()
jump_tween.tween_property(tekton, "global_position:y", mid_pos.y, 0.3).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
jump_tween.tween_property(tekton, "global_position:y", end_world_pos.y, 0.3).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN).set_delay(0.3)
# Landing Callback
jump_tween.tween_callback(func():
tekton.current_position = target_pos
# Impact Effects!
# 1. Stun nearby players (Radius 2?)
# "if there's a player around that floor they will got stunned" -> "around that floor" implies radius
var impact_center = target_pos
var stun_radius = 1.5
var players = get_tree().get_nodes_in_group("Players")
print("[Throw] Checking stun impact at %s. Found %d players." % [impact_center, players.size()])
for p in players:
if p == self: continue
# Check distance
var dist = Vector2(p.current_position.x, p.current_position.y).distance_to(Vector2(impact_center.x, impact_center.y))
print("[Throw] Player %s at %s. Dist: %.2f (Radius: %.1f)" % [p.name, p.current_position, dist, stun_radius])
if dist <= stun_radius:
if p.has_method("apply_stagger"):
print("[Throw] Applying stagger to %s" % p.name)
p.apply_stagger(3.0)
NotificationManager.send_message(self , "Stunned " + p.name + "!", NotificationManager.MessageType.WARNING)
# 2. Tekton drops tiles (Spawn tiles around) AND shrinks
if tekton.has_method("on_thrown_landing"):
tekton.on_thrown_landing(self )
else:
# Fallback
tekton.on_hit(self , 1.0)
print("[Player %s] Tekton landed at %s" % [name, target_pos])
).set_delay(0.6)
print("[Player %s] Threw Tekton to %s (Dist: %s)" % [name, target_pos, target_pos.distance_to(tekton.current_position)])
func knock_tekton(): func knock_tekton():
if not is_multiplayer_authority() or is_frozen: if not is_multiplayer_authority() or is_frozen:
+4 -1
View File
@@ -34,6 +34,9 @@ func grab_item(grid_position: Vector2i) -> bool:
if not enhanced_gridmap or not has_ap: if not enhanced_gridmap or not has_ap:
return false return false
if player.get("is_frozen"):
return false
var cell = Vector3i(grid_position.x, 1, grid_position.y) var cell = Vector3i(grid_position.x, 1, grid_position.y)
var item = enhanced_gridmap.get_cell_item(cell) var item = enhanced_gridmap.get_cell_item(cell)
@@ -319,7 +322,7 @@ func auto_put_item() -> bool:
# Check AP only if in turn-based mode # Check AP only if in turn-based mode
var has_ap = player.action_points > 0 if TurnManager.turn_based_mode else true var has_ap = player.action_points > 0 if TurnManager.turn_based_mode else true
if not enhanced_gridmap or not has_ap or player.is_bot or player.is_in_group("Bots"): if not enhanced_gridmap or not has_ap or player.is_bot or player.is_in_group("Bots") or player.get("is_frozen"):
return false return false
# Step 1: Find empty adjacent (or current) grid cells # Step 1: Find empty adjacent (or current) grid cells
+106 -7
View File
@@ -120,15 +120,114 @@ func _process(delta):
global_position = carrier.global_position + Vector3(0, 1.5, 0) global_position = carrier.global_position + Vector3(0, 1.5, 0)
rotation = carrier.rotation rotation = carrier.rotation
func _flash_damage(): var mesh_cache: Array[MeshInstance3D] = []
var original_scales: Array[Vector3] = []
func _ready():
# Cache meshes and their initial scales
# We wait a frame to ensure all children are ready and transforms applied
await get_tree().process_frame
var meshes = find_children("*", "MeshInstance3D", true) var meshes = find_children("*", "MeshInstance3D", true)
for mesh in meshes: for mesh in meshes:
var original_modulate = mesh.transparency mesh_cache.append(mesh)
# Quick flash hack or shader param? original_scales.append(mesh.scale)
# If standard material, maybe just modulate visibility or scale
var t = create_tween() func _flash_damage():
t.tween_property(mesh, "scale", Vector3(1.2, 1.2, 1.2), 0.1) # If cache empty (e.g. called before ready), try to populate or just skip custom scaling
t.tween_property(mesh, "scale", Vector3(1.0, 1.0, 1.0), 0.1) if mesh_cache.is_empty():
return
for i in range(mesh_cache.size()):
var mesh = mesh_cache[i]
if is_instance_valid(mesh):
var base_scale = original_scales[i]
var t = create_tween()
t.tween_property(mesh, "scale", base_scale * 1.2, 0.1)
t.tween_property(mesh, "scale", base_scale, 0.1)
@rpc("any_peer", "call_local", "reliable")
func on_thrown_landing(attacker: Node = null):
"""Called when Tekton lands after being thrown."""
print("[Tekton] Landed! Shrinking and waiting...")
# Disable movement/interaction logic temporarily
var controller = get_node_or_null("TektonController")
if controller and controller.get("timer"):
controller.timer.stop()
# Visual Shrink
# Use cached meshes if available, else find them (but can't restore accurately if not cached)
if mesh_cache.is_empty():
# Fallback if _ready hasn't run or failed
# We'll just define the user's specific vector as fallback target for the sphere
# But better to rely on cache.
pass
for i in range(mesh_cache.size()):
var mesh = mesh_cache[i]
if is_instance_valid(mesh):
var base_scale = original_scales[i]
var t = create_tween()
t.tween_property(mesh, "scale", base_scale * 0.5, 0.2).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
# Wait 1 seconds
await get_tree().create_timer(1.0).timeout
# Grow back
for i in range(mesh_cache.size()):
var mesh = mesh_cache[i]
if is_instance_valid(mesh):
var base_scale = original_scales[i]
var t = create_tween()
t.tween_property(mesh, "scale", base_scale, 0.2).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
# Resume AI
if controller and controller.has_method("_start_timer"):
if is_multiplayer_authority() and not is_carried:
controller._start_timer()
# Spawn tiles (as requested "tekton will spawn a tiles around that floor also")
if is_multiplayer_authority():
spawn_tiles_around(8) # Standard amount
# Floor Freeze (Visual/Instant - Run on all clients locally)
_temporarily_change_floor(current_position, 1, 6, 3.0)
func _temporarily_change_floor(center: Vector2i, radius: int, new_id: int, duration: float):
if not enhanced_gridmap: return
# Run locally on all clients to ensure instant feedback without network delay
var changed_cells = {} # pos: original_id
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center + Vector2i(x, y)
if enhanced_gridmap.is_position_valid(pos):
var cell_3d = Vector3i(pos.x, 0, pos.y)
var original = enhanced_gridmap.get_cell_item(cell_3d)
# Only change if not already the new ID (avoid redundant updates or overriding existing freeze)
if original != new_id:
changed_cells[pos] = original
# Set locally immediately
enhanced_gridmap.set_cell_item(cell_3d, new_id)
await get_tree().create_timer(duration).timeout
# Restore locally
for pos in changed_cells:
var original = changed_cells[pos]
var current_cell = Vector3i(pos.x, 0, pos.y)
var current = enhanced_gridmap.get_cell_item(current_cell)
# Only restore if it hasn't been changed to something else in meantime
if current == new_id:
enhanced_gridmap.set_cell_item(current_cell, original)
# Stun nearby players handled by Thrower (Player.gd) or here?
# Player.gd handles the stun call because it knows the impact zone context better?
# Actually, Player.gd calls this function. Player.gd *also* iterates players to stun them.
# That is fine.
func spawn_tiles_around(count: int = 4): func spawn_tiles_around(count: int = 4):
"""Spawns a mix of normal and special tiles in a radius.""" """Spawns a mix of normal and special tiles in a radius."""