Turns the sandbox into a survival roguelike loop: outfit your gladiator with banked gold, enter the arena, earn gold from kills and wave clears, die, keep 10% of your total value (floored at the 500 fresh-start). Economy (GameState autoload): - banked_gold, owned items, and loadout persist to user://save.cfg - Kill gold (basic 10g, armed 25g) credited server-side to the killer - Wave-clear bonus (25 + 5/wave) for all players; wave synced to peers - Death settlement liquidates gear, guarded against double-fire Character creation / shop: - Menu reframed: armory shop + live Character Sheet loadout preview - Character Sheet gains preview mode (renders from loadout, no player) - Buy/Equip with hand rules; two-handers and off-hands displace each other symmetrically (shop and in-game pickups) - Chosen loadout auto-equips on spawn via existing RPC path Combat feel: - Attacks snap to camera facing and lock direction for the swing - Dash commits to its direction for the full duration - Weapon slots show name text when no icon is set - Hitbox/hurtbox debug meshes hidden by default (H toggles) Death is final: players no longer respawn; a results screen shows waves, time, kills, and the settlement, then returns to camp. Armed enemies no longer respawn (were an infinite gold farm and respawned invisible).
111 lines
2.9 KiB
GDScript
111 lines
2.9 KiB
GDScript
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
|
|
## Gold credited to the killing player (arena economy)
|
|
@export var gold_reward: int = 10
|
|
|
|
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 (check peer is assigned first)
|
|
if multiplayer.multiplayer_peer == null or 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
|