Added rpg system for combat
added lots of loot to find added level up system
This commit is contained in:
@@ -4,10 +4,12 @@ extends CharacterBody2D
|
||||
|
||||
@export var max_health: float = 50.0
|
||||
@export var move_speed: float = 80.0
|
||||
@export var damage: float = 10.0
|
||||
@export var damage: float = 10.0 # Legacy - use character_stats.damage instead
|
||||
@export var attack_cooldown: float = 1.0
|
||||
@export var exp_reward: float = 10.0 # EXP granted when this enemy is defeated
|
||||
|
||||
var current_health: float = 50.0
|
||||
var character_stats: CharacterStats # RPG stats system (same as players)
|
||||
var is_dead: bool = false
|
||||
var target_player: Node = null
|
||||
var attack_timer: float = 0.0
|
||||
@@ -35,6 +37,9 @@ var anim_speed: float = 0.15 # Seconds per frame
|
||||
@onready var collision_shape = get_node_or_null("CollisionShape2D")
|
||||
|
||||
func _ready():
|
||||
# Initialize CharacterStats for RPG system
|
||||
_initialize_character_stats()
|
||||
|
||||
current_health = max_health
|
||||
add_to_group("enemy")
|
||||
|
||||
@@ -51,6 +56,27 @@ func _ready():
|
||||
# Walls are on layer 7 (bit 6 = 64), not layer 4!
|
||||
collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
||||
|
||||
# Initialize CharacterStats for this enemy
|
||||
# Override in subclasses to set specific baseStats
|
||||
func _initialize_character_stats():
|
||||
character_stats = CharacterStats.new()
|
||||
character_stats.character_type = "enemy"
|
||||
character_stats.character_name = name
|
||||
# Default stats - override in subclasses
|
||||
character_stats.baseStats.str = 10
|
||||
character_stats.baseStats.dex = 10
|
||||
character_stats.baseStats.int = 10
|
||||
character_stats.baseStats.end = 10
|
||||
character_stats.baseStats.wis = 10
|
||||
character_stats.baseStats.cha = 10
|
||||
character_stats.baseStats.lck = 10
|
||||
# Initialize hp and mp
|
||||
character_stats.hp = character_stats.maxhp
|
||||
character_stats.mp = character_stats.maxmp
|
||||
# Sync max_health and current_health from character_stats (for backwards compatibility)
|
||||
max_health = character_stats.maxhp
|
||||
current_health = character_stats.hp
|
||||
|
||||
func _physics_process(delta):
|
||||
if is_dead:
|
||||
# Even when dead, allow knockback to continue briefly
|
||||
@@ -259,7 +285,7 @@ func _find_nearest_player_to_position(pos: Vector2, max_range: float = 100.0) ->
|
||||
|
||||
return nearest
|
||||
|
||||
func take_damage(amount: float, from_position: Vector2):
|
||||
func take_damage(amount: float, from_position: Vector2, is_critical: bool = false):
|
||||
# Only process damage on server/authority
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
@@ -273,8 +299,41 @@ func take_damage(amount: float, from_position: Vector2):
|
||||
if nearest_player:
|
||||
killer_player = nearest_player # Update killer to the most recent attacker
|
||||
|
||||
current_health -= amount
|
||||
print(name, " took ", amount, " damage! Health: ", current_health)
|
||||
# Check for dodge chance (based on DEX) - same as players
|
||||
var _was_dodged = false
|
||||
if character_stats:
|
||||
var dodge_roll = randf()
|
||||
var dodge_chance = character_stats.dodge_chance
|
||||
if dodge_roll < dodge_chance:
|
||||
_was_dodged = true
|
||||
print(name, " DODGED the attack! (DEX: ", character_stats.baseStats.dex + character_stats.get_pass("dex"), ", dodge chance: ", dodge_chance * 100.0, "%)")
|
||||
# Show "DODGED" text
|
||||
_show_damage_number(0.0, from_position, false, false, true) # is_dodged = true
|
||||
# Sync dodge visual to clients
|
||||
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||
var enemy_name = name
|
||||
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and game_world.has_method("_sync_enemy_damage_visual"):
|
||||
game_world._sync_enemy_damage_visual.rpc(enemy_name, enemy_index)
|
||||
return # No damage taken, exit early
|
||||
|
||||
# If not dodged, apply damage with DEF reduction
|
||||
var actual_damage = amount
|
||||
if character_stats:
|
||||
# Calculate damage after DEF reduction (critical hits pierce 80% of DEF)
|
||||
actual_damage = character_stats.calculate_damage(amount, false, is_critical) # false = not magical, is_critical = crit pierce
|
||||
character_stats.modify_health(-actual_damage)
|
||||
current_health = character_stats.hp
|
||||
if character_stats.hp <= 0:
|
||||
character_stats.no_health.emit()
|
||||
var effective_def = character_stats.defense * (0.2 if is_critical else 1.0)
|
||||
print(name, " took ", actual_damage, " damage (", amount, " base - ", effective_def, " DEF = ", actual_damage, ")! Health: ", current_health, "/", character_stats.maxhp)
|
||||
else:
|
||||
# Fallback for legacy (shouldn't happen if _initialize_character_stats is called)
|
||||
current_health -= amount
|
||||
actual_damage = amount
|
||||
print(name, " took ", amount, " damage! Health: ", current_health, " (critical: ", is_critical, ")")
|
||||
|
||||
# Calculate knockback direction (away from attacker)
|
||||
var knockback_direction = (global_position - from_position).normalized()
|
||||
@@ -287,10 +346,9 @@ func take_damage(amount: float, from_position: Vector2):
|
||||
# Flash red (even if dying, show the hit)
|
||||
_flash_damage()
|
||||
|
||||
# Show damage number (red, using dmg_numbers.png font) above enemy
|
||||
# Only show if damage > 0
|
||||
if amount > 0:
|
||||
_show_damage_number(amount, from_position)
|
||||
# Show damage number (red/orange, using dmg_numbers.png font) above enemy
|
||||
# Always show damage number, even if 0
|
||||
_show_damage_number(actual_damage, from_position, is_critical)
|
||||
|
||||
# Sync damage visual to clients
|
||||
# Use game_world to route damage visual sync instead of direct RPC to avoid node path issues
|
||||
@@ -321,16 +379,14 @@ func take_damage(amount: float, from_position: Vector2):
|
||||
call_deferred("_notify_doors_enemy_died")
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func rpc_take_damage(amount: float, from_position: Vector2):
|
||||
func rpc_take_damage(amount: float, from_position: Vector2, is_critical: bool = false):
|
||||
# RPC version - only process on server/authority
|
||||
if is_multiplayer_authority():
|
||||
take_damage(amount, from_position)
|
||||
take_damage(amount, from_position, is_critical)
|
||||
|
||||
func _show_damage_number(amount: float, from_position: Vector2):
|
||||
# Show damage number (red, using dmg_numbers.png font) above enemy
|
||||
# Only show if damage > 0
|
||||
if amount <= 0:
|
||||
return
|
||||
func _show_damage_number(amount: float, from_position: Vector2, is_critical: bool = false, is_miss: bool = false, is_dodged: bool = false):
|
||||
# Show damage number (red/orange for crits, cyan for dodge, gray for miss, using dmg_numbers.png font) above enemy
|
||||
# Show even if amount is 0 for MISS/DODGED
|
||||
|
||||
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||
if not damage_number_scene:
|
||||
@@ -340,9 +396,16 @@ func _show_damage_number(amount: float, from_position: Vector2):
|
||||
if not damage_label:
|
||||
return
|
||||
|
||||
# Set damage text and red color
|
||||
damage_label.label = str(int(amount))
|
||||
damage_label.color = Color.RED
|
||||
# Set text and color based on type
|
||||
if is_dodged:
|
||||
damage_label.label = "DODGED"
|
||||
damage_label.color = Color.CYAN
|
||||
elif is_miss:
|
||||
damage_label.label = "MISS"
|
||||
damage_label.color = Color.GRAY
|
||||
else:
|
||||
damage_label.label = str(int(amount))
|
||||
damage_label.color = Color.ORANGE if is_critical else Color.RED
|
||||
|
||||
# Calculate direction from attacker (slight upward variation)
|
||||
var direction_from_attacker = (global_position - from_position).normalized()
|
||||
@@ -418,11 +481,16 @@ func _die():
|
||||
is_dead = true
|
||||
print(name, " died!")
|
||||
|
||||
# Credit kill to the player who dealt the fatal damage
|
||||
# Credit kill and grant EXP to the player who dealt the fatal damage
|
||||
if killer_player and is_instance_valid(killer_player) and killer_player.character_stats:
|
||||
killer_player.character_stats.kills += 1
|
||||
print(name, " kill credited to ", killer_player.name, " (total kills: ", killer_player.character_stats.kills, ")")
|
||||
|
||||
# Grant EXP to the killer
|
||||
if exp_reward > 0:
|
||||
killer_player.character_stats.add_xp(exp_reward)
|
||||
print(name, " granted ", exp_reward, " EXP to ", killer_player.name)
|
||||
|
||||
# Sync kill update to client if this player belongs to a client
|
||||
# Only sync if we're on the server and the killer is a client's player
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
||||
@@ -475,16 +543,21 @@ func _spawn_loot():
|
||||
var loot_chance = randf()
|
||||
print(name, " loot chance roll: ", loot_chance, " (need > 0.3)")
|
||||
if loot_chance > 0.3:
|
||||
# Random loot type
|
||||
# Decide what to drop: 30% coin, 30% food, 40% item
|
||||
var drop_roll = randf()
|
||||
var loot_type = 0
|
||||
var drop_item = false
|
||||
|
||||
# 50% chance for coin
|
||||
if randf() < 0.5:
|
||||
if drop_roll < 0.3:
|
||||
# 30% chance for coin
|
||||
loot_type = 0 # COIN
|
||||
# 50% chance for food item
|
||||
else:
|
||||
elif drop_roll < 0.6:
|
||||
# 30% chance for food item
|
||||
var food_types = [1, 2, 3] # APPLE, BANANA, CHERRY
|
||||
loot_type = food_types[randi() % food_types.size()]
|
||||
else:
|
||||
# 40% chance for Item instance
|
||||
drop_item = true
|
||||
|
||||
# Generate random velocity values (same on all clients)
|
||||
var random_angle = randf() * PI * 2
|
||||
@@ -500,10 +573,20 @@ func _spawn_loot():
|
||||
if game_world and game_world.has_method("_find_nearby_safe_spawn_position"):
|
||||
safe_spawn_pos = game_world._find_nearby_safe_spawn_position(global_position, 64.0)
|
||||
|
||||
# Spawn on server
|
||||
var loot = loot_scene.instantiate()
|
||||
var entities_node = get_parent()
|
||||
if entities_node:
|
||||
if not entities_node:
|
||||
print(name, " ERROR: entities_node is null! Cannot spawn loot!")
|
||||
return
|
||||
|
||||
if drop_item:
|
||||
# Spawn Item instance as loot
|
||||
var item = ItemDatabase.get_random_enemy_drop()
|
||||
if item:
|
||||
ItemLootHelper.spawn_item_loot(item, global_position, entities_node, game_world)
|
||||
print(name, " ✓ dropped item: ", item.item_name, " at ", safe_spawn_pos)
|
||||
else:
|
||||
# Spawn regular loot (coin or food)
|
||||
var loot = loot_scene.instantiate()
|
||||
entities_node.add_child(loot)
|
||||
loot.global_position = safe_spawn_pos
|
||||
loot.loot_type = loot_type
|
||||
@@ -519,8 +602,7 @@ func _spawn_loot():
|
||||
# Reuse game_world variable from above
|
||||
if game_world:
|
||||
# Generate unique loot ID
|
||||
if not "loot_id_counter" in game_world:
|
||||
game_world.loot_id_counter = 0
|
||||
# loot_id_counter is declared as a variable in game_world.gd, so it always exists
|
||||
var loot_id = game_world.loot_id_counter
|
||||
game_world.loot_id_counter += 1
|
||||
# Store loot ID on server loot instance
|
||||
@@ -530,8 +612,6 @@ func _spawn_loot():
|
||||
print(name, " ✓ synced loot spawn to clients")
|
||||
else:
|
||||
print(name, " ERROR: game_world not found for loot sync!")
|
||||
else:
|
||||
print(name, " ERROR: entities_node is null! Cannot spawn loot!")
|
||||
else:
|
||||
print(name, " loot chance failed (", loot_chance, " <= 0.3), no loot dropped")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user