Colours and practise dummy
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
extends BaseUnit
|
||||
class_name BaseEnemy
|
||||
|
||||
## Base class for all enemies in the game
|
||||
## Provides common enemy functionality like AI, pathfinding, and targeting
|
||||
|
||||
signal target_changed(new_target: Node)
|
||||
|
||||
## Current target (usually a player)
|
||||
var current_target: Node = null
|
||||
## Enemy detection/aggro range
|
||||
@export var detection_range: float = 10.0
|
||||
## Whether this enemy is aggressive (will attack players)
|
||||
@export var is_aggressive: bool = true
|
||||
|
||||
func _ready():
|
||||
super._ready()
|
||||
|
||||
# Enemies should respawn by default
|
||||
can_respawn = true
|
||||
|
||||
# Connect to health signals for AI reactions
|
||||
health_changed.connect(_on_enemy_health_changed)
|
||||
died.connect(_on_enemy_died)
|
||||
respawned.connect(_on_enemy_respawned)
|
||||
|
||||
func _physics_process(delta):
|
||||
# Only server handles enemy AI
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
if is_dead:
|
||||
return
|
||||
|
||||
# Update target if needed
|
||||
_update_target()
|
||||
|
||||
## Find and update current target
|
||||
func _update_target():
|
||||
# Subclasses can override this to implement custom targeting logic
|
||||
pass
|
||||
|
||||
## Get all players in range
|
||||
func get_players_in_range(range_dist: float) -> Array[Node]:
|
||||
var players_in_range: Array[Node] = []
|
||||
|
||||
# Find the players container
|
||||
var level = get_tree().get_current_scene()
|
||||
if not level or not level.has_node("PlayersContainer"):
|
||||
return players_in_range
|
||||
|
||||
var players_container = level.get_node("PlayersContainer")
|
||||
|
||||
for player in players_container.get_children():
|
||||
if player is Character and not player.is_dead:
|
||||
var distance = global_position.distance_to(player.global_position)
|
||||
if distance <= range_dist:
|
||||
players_in_range.append(player)
|
||||
|
||||
return players_in_range
|
||||
|
||||
## Get nearest player
|
||||
func get_nearest_player() -> Node:
|
||||
var players = get_players_in_range(detection_range)
|
||||
|
||||
if players.is_empty():
|
||||
return null
|
||||
|
||||
var nearest_player = null
|
||||
var nearest_distance = INF
|
||||
|
||||
for player in players:
|
||||
var distance = global_position.distance_to(player.global_position)
|
||||
if distance < nearest_distance:
|
||||
nearest_distance = distance
|
||||
nearest_player = player
|
||||
|
||||
return nearest_player
|
||||
|
||||
## Health changed callback
|
||||
func _on_enemy_health_changed(old_health: float, new_health: float):
|
||||
# Subclasses can override to react to damage
|
||||
pass
|
||||
|
||||
## Death callback
|
||||
func _on_enemy_died(killer_id: int):
|
||||
print("[Enemy ", name, "] died. Killer ID: ", killer_id)
|
||||
|
||||
# Subclasses can override for death effects
|
||||
pass
|
||||
|
||||
## Respawn callback
|
||||
func _on_enemy_respawned():
|
||||
print("[Enemy ", name, "] respawned at ", global_position)
|
||||
|
||||
# Clear target on respawn
|
||||
current_target = null
|
||||
target_changed.emit(null)
|
||||
|
||||
# Subclasses can override for respawn effects
|
||||
pass
|
||||
|
||||
## Override hurt animation
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func _play_hurt_animation():
|
||||
# Flash red or play hurt animation
|
||||
# Subclasses should implement this with their specific animations
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://base_enemy_script
|
||||
+84
-1
@@ -4,10 +4,12 @@ extends Node3D
|
||||
@onready var nick_input: LineEdit = $Menu/MainContainer/MainMenu/Option1/NickInput
|
||||
@onready var address_input: LineEdit = $Menu/MainContainer/MainMenu/Option3/AddressInput
|
||||
@onready var players_container: Node3D = $PlayersContainer
|
||||
@onready var weapons_container: Node3D = null # Will be created if doesn't exist
|
||||
@onready var weapons_container: Node3D = $WeaponsContainer
|
||||
@onready var enemies_container: Node3D = $EnemiesContainer
|
||||
@onready var menu: Control = $Menu
|
||||
@onready var main_menu: VBoxContainer = $Menu/MainContainer/MainMenu
|
||||
@export var player_scene: PackedScene
|
||||
@export var practice_dummy_scene: PackedScene
|
||||
|
||||
# Weapon spawning counter (server-side only)
|
||||
var _weapon_spawn_counter: int = 0
|
||||
@@ -43,6 +45,15 @@ func _ready():
|
||||
add_child(weapons_container)
|
||||
print("Created WeaponsContainer")
|
||||
|
||||
# Create or find enemies container
|
||||
if has_node("EnemiesContainer"):
|
||||
enemies_container = get_node("EnemiesContainer")
|
||||
else:
|
||||
enemies_container = Node3D.new()
|
||||
enemies_container.name = "EnemiesContainer"
|
||||
add_child(enemies_container)
|
||||
print("Created EnemiesContainer")
|
||||
|
||||
# Don't initialize weapons in _ready() - wait for Host/Join to be pressed
|
||||
# This is handled in initialize_multiplayer() which is called after the
|
||||
# multiplayer peer is properly set up
|
||||
@@ -74,6 +85,9 @@ func initialize_multiplayer():
|
||||
# Spawn initial weapons when server starts
|
||||
_spawn_initial_weapons()
|
||||
|
||||
# Spawn practice dummies
|
||||
_spawn_practice_dummies()
|
||||
|
||||
# Spawn the host player (peer ID 1)
|
||||
print("[Level] Spawning host player")
|
||||
var host_info = Network.players.get(1, {"nick": "Host", "skin": "blue"})
|
||||
@@ -219,6 +233,18 @@ func _on_player_connected(peer_id, player_info):
|
||||
else:
|
||||
print("[Server] Skipping invalid weapon ", weapon_id)
|
||||
|
||||
# Sync existing enemies to the newly joined player
|
||||
print("[Server] Syncing enemies to newly connected peer: ", peer_id)
|
||||
if enemies_container:
|
||||
for enemy in enemies_container.get_children():
|
||||
if enemy is BaseEnemy:
|
||||
# Extract ID from name (e.g., "PracticeDummy_1" -> 1)
|
||||
var enemy_name_parts = enemy.name.split("_")
|
||||
if enemy_name_parts.size() >= 2:
|
||||
var enemy_id = enemy_name_parts[-1].to_int()
|
||||
print("[Server] Syncing enemy ", enemy.name, " at position ", enemy.global_position, " to peer ", peer_id)
|
||||
rpc_id(peer_id, "_spawn_dummy_local", enemy_id, enemy.global_position)
|
||||
|
||||
# Sync equipped weapons for all existing players to the newly joined player
|
||||
print("[Server] Syncing equipped weapons to newly connected peer: ", peer_id)
|
||||
for player_node in players_container.get_children():
|
||||
@@ -414,6 +440,63 @@ func _on_jemz_preset():
|
||||
skin_input.text = "Red"
|
||||
address_input.text = "127.0.0.1"
|
||||
|
||||
# ---------- ENEMY SPAWNING ----------
|
||||
func _spawn_practice_dummies():
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
if not practice_dummy_scene:
|
||||
push_warning("Practice dummy scene not assigned!")
|
||||
return
|
||||
|
||||
# Wait a frame for everything to be ready
|
||||
await get_tree().process_frame
|
||||
|
||||
print("[Server] Spawning practice dummies")
|
||||
|
||||
# Spawn dummies at different positions
|
||||
var dummy_positions = [
|
||||
Vector3(10, 0, 0),
|
||||
Vector3(-10, 0, 0),
|
||||
Vector3(0, 0, 10),
|
||||
Vector3(0, 0, -10),
|
||||
]
|
||||
|
||||
var dummy_counter = 0
|
||||
for pos in dummy_positions:
|
||||
dummy_counter += 1
|
||||
rpc("_spawn_dummy_local", dummy_counter, pos)
|
||||
|
||||
## Spawn a practice dummy on all clients
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func _spawn_dummy_local(dummy_id: int, spawn_pos: Vector3):
|
||||
if not practice_dummy_scene:
|
||||
push_error("Practice dummy scene not loaded!")
|
||||
return
|
||||
|
||||
if not enemies_container:
|
||||
push_error("EnemiesContainer not found!")
|
||||
return
|
||||
|
||||
var dummy_name = "PracticeDummy_" + str(dummy_id)
|
||||
|
||||
# Don't spawn duplicates
|
||||
if enemies_container.has_node(dummy_name):
|
||||
print("[Peer ", multiplayer.get_unique_id(), "] Dummy ", dummy_name, " already exists")
|
||||
return
|
||||
|
||||
print("[Peer ", multiplayer.get_unique_id(), "] Spawning dummy ", dummy_name, " at ", spawn_pos)
|
||||
|
||||
var dummy = practice_dummy_scene.instantiate()
|
||||
dummy.name = dummy_name
|
||||
dummy.position = spawn_pos
|
||||
|
||||
# Set multiplayer authority to server
|
||||
dummy.set_multiplayer_authority(1)
|
||||
|
||||
enemies_container.add_child(dummy, true)
|
||||
print("[Peer ", multiplayer.get_unique_id(), "] Dummy ", dummy_name, " spawned successfully")
|
||||
|
||||
# ---------- WEAPON SPAWNING ----------
|
||||
## Spawn a weapon in the world (called from server, syncs to all clients)
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
|
||||
@@ -65,3 +65,46 @@ func play_attack(anim_name: String = "Attack_OneHand") -> void:
|
||||
#push_warning("Animation '%s' not found, using Attack_OneHand" % anim_name)
|
||||
animation_player.play("Attack_OneHand", -1, 1.0)
|
||||
animation_player.animation_set_next("Attack_OneHand", "")
|
||||
|
||||
## Apply hue shift to all mesh instances in the character
|
||||
func set_character_color(hue_shift: float) -> void:
|
||||
# Find all MeshInstance3D children recursively
|
||||
var mesh_instances = _find_mesh_instances(self)
|
||||
|
||||
for mesh in mesh_instances:
|
||||
_apply_hue_to_mesh(mesh, hue_shift)
|
||||
|
||||
## Recursively find all MeshInstance3D nodes
|
||||
func _find_mesh_instances(node: Node) -> Array[MeshInstance3D]:
|
||||
var meshes: Array[MeshInstance3D] = []
|
||||
|
||||
if node is MeshInstance3D:
|
||||
meshes.append(node)
|
||||
|
||||
for child in node.get_children():
|
||||
meshes.append_array(_find_mesh_instances(child))
|
||||
|
||||
return meshes
|
||||
|
||||
## Apply hue shift to a mesh instance
|
||||
func _apply_hue_to_mesh(mesh_instance: MeshInstance3D, hue_shift: float) -> void:
|
||||
if not mesh_instance:
|
||||
return
|
||||
|
||||
# Get or create material for each surface
|
||||
for i in range(mesh_instance.get_surface_override_material_count()):
|
||||
var material = mesh_instance.get_surface_override_material(i)
|
||||
|
||||
# If no override material, get the base material and duplicate it
|
||||
if not material:
|
||||
material = mesh_instance.mesh.surface_get_material(i)
|
||||
if material:
|
||||
material = material.duplicate()
|
||||
mesh_instance.set_surface_override_material(i, material)
|
||||
|
||||
# Apply hue shift if it's a StandardMaterial3D
|
||||
if material and material is StandardMaterial3D:
|
||||
var std_mat = material as StandardMaterial3D
|
||||
# Create a modulate color from hue
|
||||
var color = Color.from_hsv(hue_shift, 0.6, 1.0)
|
||||
std_mat.albedo_color = color
|
||||
|
||||
+35
-11
@@ -348,12 +348,27 @@ func get_texture_from_name(skin_color: SkinColor) -> CompressedTexture2D:
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func set_player_skin(skin_name: SkinColor) -> void:
|
||||
var texture = get_texture_from_name(skin_name)
|
||||
# Check if we're using the LilguyRigged model
|
||||
if _body is LilguyBody:
|
||||
# Use hue-based color system for Lilguy
|
||||
var hue = _get_hue_from_skin_color(skin_name)
|
||||
_body.set_character_color(hue)
|
||||
else:
|
||||
# Use texture-based system for 3DGodotRobot
|
||||
var texture = get_texture_from_name(skin_name)
|
||||
set_mesh_texture(_bottom_mesh, texture)
|
||||
set_mesh_texture(_chest_mesh, texture)
|
||||
set_mesh_texture(_face_mesh, texture)
|
||||
set_mesh_texture(_limbs_head_mesh, texture)
|
||||
|
||||
set_mesh_texture(_bottom_mesh, texture)
|
||||
set_mesh_texture(_chest_mesh, texture)
|
||||
set_mesh_texture(_face_mesh, texture)
|
||||
set_mesh_texture(_limbs_head_mesh, texture)
|
||||
## Convert SkinColor enum to hue value (0.0 to 1.0)
|
||||
func _get_hue_from_skin_color(skin_color: SkinColor) -> float:
|
||||
match skin_color:
|
||||
SkinColor.BLUE: return 0.6 # Blue hue
|
||||
SkinColor.GREEN: return 0.33 # Green hue
|
||||
SkinColor.RED: return 0.0 # Red hue
|
||||
SkinColor.YELLOW: return 0.16 # Yellow hue
|
||||
_: return 0.6 # Default to blue
|
||||
|
||||
func set_mesh_texture(mesh_instance: MeshInstance3D, texture: CompressedTexture2D) -> void:
|
||||
if mesh_instance:
|
||||
@@ -435,16 +450,25 @@ func _server_apply_damage(target_name: String, damage: float, attacker_id: int,
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
# Get the target from the players container
|
||||
var level = get_tree().get_current_scene()
|
||||
if not level or not level.has_node("PlayersContainer"):
|
||||
if not level:
|
||||
return
|
||||
|
||||
var players_container = level.get_node("PlayersContainer")
|
||||
if not players_container.has_node(target_name):
|
||||
return
|
||||
var target = null
|
||||
|
||||
var target = players_container.get_node(target_name)
|
||||
# Check players container first
|
||||
if level.has_node("PlayersContainer"):
|
||||
var players_container = level.get_node("PlayersContainer")
|
||||
if players_container.has_node(target_name):
|
||||
target = players_container.get_node(target_name)
|
||||
|
||||
# If not found in players, check enemies container
|
||||
if not target and level.has_node("EnemiesContainer"):
|
||||
var enemies_container = level.get_node("EnemiesContainer")
|
||||
if enemies_container.has_node(target_name):
|
||||
target = enemies_container.get_node(target_name)
|
||||
|
||||
# Apply damage if target found
|
||||
if target and target is BaseUnit:
|
||||
target.take_damage(damage, attacker_id, knockback, attacker_pos)
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
extends BaseEnemy
|
||||
class_name PracticeDummy
|
||||
|
||||
## A stationary practice dummy for testing combat
|
||||
## Cannot move or attack - just takes damage and shows health
|
||||
|
||||
## Visual mesh reference
|
||||
@onready var _mesh: MeshInstance3D = null
|
||||
## Health label above dummy
|
||||
@onready var _health_label: Label3D = null
|
||||
## Original material for hit flash effect
|
||||
var _original_material: Material = null
|
||||
## Hit flash effect
|
||||
var _hit_flash_timer: float = 0.0
|
||||
const HIT_FLASH_DURATION: float = 0.2
|
||||
|
||||
func _ready():
|
||||
super._ready()
|
||||
|
||||
# Practice dummy should not be aggressive
|
||||
is_aggressive = false
|
||||
|
||||
# Auto-find mesh and health label
|
||||
_mesh = get_node_or_null("Mesh")
|
||||
_health_label = get_node_or_null("HealthLabel")
|
||||
|
||||
# Store original material for flash effect
|
||||
if _mesh:
|
||||
_original_material = _mesh.get_surface_override_material(0)
|
||||
if not _original_material and _mesh.mesh:
|
||||
_original_material = _mesh.mesh.surface_get_material(0)
|
||||
|
||||
# Update initial health display
|
||||
_update_health_display()
|
||||
|
||||
# Connect health change to update display
|
||||
health_changed.connect(_on_health_display_changed)
|
||||
|
||||
func _process(delta):
|
||||
# Handle hit flash timer
|
||||
if _hit_flash_timer > 0:
|
||||
_hit_flash_timer -= delta
|
||||
if _hit_flash_timer <= 0:
|
||||
_reset_material()
|
||||
|
||||
func _physics_process(delta):
|
||||
# Don't call super._physics_process since we don't move
|
||||
# Just apply gravity
|
||||
if not is_on_floor():
|
||||
velocity.y -= ProjectSettings.get_setting("physics/3d/default_gravity") * delta
|
||||
move_and_slide()
|
||||
|
||||
## Update health display
|
||||
func _update_health_display():
|
||||
if _health_label:
|
||||
_health_label.text = "HP: %d/%d" % [int(current_health), int(max_health)]
|
||||
|
||||
func _on_health_display_changed(_old_health: float, _new_health: float):
|
||||
_update_health_display()
|
||||
|
||||
## Override hurt animation to flash red
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func _play_hurt_animation():
|
||||
if _mesh:
|
||||
_flash_red()
|
||||
|
||||
## Flash the dummy red when hit
|
||||
func _flash_red():
|
||||
if not _mesh:
|
||||
return
|
||||
|
||||
_hit_flash_timer = HIT_FLASH_DURATION
|
||||
|
||||
# Create red material
|
||||
var red_material = StandardMaterial3D.new()
|
||||
red_material.albedo_color = Color(1.5, 0.3, 0.3) # Bright red
|
||||
|
||||
# Copy properties from original if it exists
|
||||
if _original_material and _original_material is StandardMaterial3D:
|
||||
var orig = _original_material as StandardMaterial3D
|
||||
red_material.metallic = orig.metallic
|
||||
red_material.roughness = orig.roughness
|
||||
red_material.albedo_texture = orig.albedo_texture
|
||||
|
||||
_mesh.set_surface_override_material(0, red_material)
|
||||
|
||||
## Reset to original material
|
||||
func _reset_material():
|
||||
if _mesh and _original_material:
|
||||
_mesh.set_surface_override_material(0, _original_material.duplicate())
|
||||
|
||||
## Override death to just hide the mesh
|
||||
func _on_enemy_died(killer_id: int):
|
||||
super._on_enemy_died(killer_id)
|
||||
|
||||
# Hide mesh when dead
|
||||
if _mesh:
|
||||
_mesh.visible = false
|
||||
|
||||
print("[PracticeDummy] Killed by player ", killer_id, ". Respawning in ", respawn_delay, " seconds...")
|
||||
|
||||
## Override respawn to show mesh again
|
||||
func _on_enemy_respawned():
|
||||
super._on_enemy_respawned()
|
||||
|
||||
# Show mesh when respawned
|
||||
if _mesh:
|
||||
_mesh.visible = true
|
||||
_reset_material()
|
||||
|
||||
_update_health_display()
|
||||
print("[PracticeDummy] Respawned!")
|
||||
@@ -0,0 +1 @@
|
||||
uid://practice_dummy_script
|
||||
Reference in New Issue
Block a user