feat: Implement core player character logic, including state management, network synchronization, character selection, and manager initialization.
This commit is contained in:
@@ -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.
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user