Added rpg system for combat

added lots of loot to find
added level up system
This commit is contained in:
2026-01-11 23:12:09 +01:00
parent ab16194c39
commit 3a7fb29d58
32 changed files with 5076 additions and 96 deletions

View File

@@ -389,6 +389,11 @@ func _initialize_character_stats():
# Initialize health/mana from stats
character_stats.hp = character_stats.maxhp
character_stats.mp = character_stats.maxmp
# Connect signals
if character_stats:
character_stats.level_up_stats.connect(_on_level_up_stats)
character_stats.character_changed.connect(_on_character_changed)
func _randomize_stats():
# Randomize base stats within reasonable ranges
@@ -471,13 +476,33 @@ func _apply_appearance_to_sprites():
sprite_body.vframes = 8
sprite_body.modulate = Color.WHITE # Remove old color tint
# Boots - empty (bare)
# Boots
if sprite_boots:
sprite_boots.texture = null
var equipped_boots = character_stats.equipment["boots"]
if equipped_boots and equipped_boots.equipmentPath != "":
var boots_texture = load(equipped_boots.equipmentPath)
if boots_texture:
sprite_boots.texture = boots_texture
sprite_boots.hframes = 35
sprite_boots.vframes = 8
else:
sprite_boots.texture = null
else:
sprite_boots.texture = null
# Armour - empty (bare)
# Armour
if sprite_armour:
sprite_armour.texture = null
var equipped_armour = character_stats.equipment["armour"]
if equipped_armour and equipped_armour.equipmentPath != "":
var armour_texture = load(equipped_armour.equipmentPath)
if armour_texture:
sprite_armour.texture = armour_texture
sprite_armour.hframes = 35
sprite_armour.vframes = 8
else:
sprite_armour.texture = null
else:
sprite_armour.texture = null
# Facial Hair
if sprite_facial_hair:
@@ -546,19 +571,35 @@ func _apply_appearance_to_sprites():
else:
sprite_addons.texture = null
# Headgear - empty (bare)
# Headgear
if sprite_headgear:
sprite_headgear.texture = null
var equipped_headgear = character_stats.equipment["headgear"]
if equipped_headgear and equipped_headgear.equipmentPath != "":
var headgear_texture = load(equipped_headgear.equipmentPath)
if headgear_texture:
sprite_headgear.texture = headgear_texture
sprite_headgear.hframes = 35
sprite_headgear.vframes = 8
else:
sprite_headgear.texture = null
else:
sprite_headgear.texture = null
# Weapon - empty (bare)
# Weapon (Mainhand)
# NOTE: Weapons should NEVER use equipmentPath - they don't have character sprite sheets
# Weapons are only displayed as inventory icons (spritePath), not as character sprite layers
if sprite_weapon:
sprite_weapon.texture = null
sprite_weapon.texture = null # Weapons don't use character sprite layers
print(name, " appearance applied: skin=", character_stats.skin,
" hair=", character_stats.hairstyle,
" facial_hair=", character_stats.facial_hair,
" eyes=", character_stats.eyes)
func _on_character_changed(_char: CharacterStats):
# Update appearance when character stats change (e.g., equipment)
_apply_appearance_to_sprites()
func _get_player_color() -> Color:
# Legacy function - now returns white (no color tint)
return Color.WHITE
@@ -1582,15 +1623,40 @@ func _perform_attack():
# Delay before spawning sword slash
await get_tree().create_timer(0.15).timeout
# Calculate damage from character_stats with randomization
var base_damage = 20.0 # Default damage
if character_stats:
base_damage = character_stats.damage
# D&D style randomization: ±20% variance
var damage_variance = 0.2
var damage_multiplier = 1.0 + randf_range(-damage_variance, damage_variance)
var final_damage = base_damage * damage_multiplier
# Critical strike chance (based on LCK stat)
var crit_chance = 0.0
if character_stats:
crit_chance = (character_stats.baseStats.lck + character_stats.get_pass("lck")) * 0.01 # 1% per LCK point
var is_crit = randf() < crit_chance
if is_crit:
final_damage *= 2.0 # Critical strikes deal 2x damage
print(name, " CRITICAL STRIKE! (LCK: ", character_stats.baseStats.lck + character_stats.get_pass("lck"), ")")
# Round to 1 decimal place
final_damage = round(final_damage * 10.0) / 10.0
# Spawn sword projectile
if sword_projectile_scene:
var projectile = sword_projectile_scene.instantiate()
get_parent().add_child(projectile)
projectile.setup(attack_direction, self)
projectile.setup(attack_direction, self, final_damage)
# Store crit status for visual feedback
if is_crit:
projectile.set_meta("is_crit", true)
# Spawn projectile a bit in front of the player
var spawn_offset = attack_direction * 10.0 # 10 pixels in front
projectile.global_position = global_position + spawn_offset
print(name, " attacked with sword projectile!")
print(name, " attacked with sword projectile! Damage: ", final_damage, " (base: ", base_damage, ", crit: ", is_crit, ")")
# Sync attack over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
@@ -2093,13 +2159,37 @@ func take_damage(amount: float, attacker_position: Vector2):
if is_dead:
return
# Take damage using character_stats
# Check for dodge chance (based on DEX)
var _was_dodged = false
if character_stats:
character_stats.take_damage(amount, false) # false = not magical damage
print(name, " took ", amount, " damage! Health: ", character_stats.hp, "/", character_stats.maxhp)
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, attacker_position, false, false, true) # is_dodged = true
# Sync dodge visual to other clients
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_sync_damage.rpc(0.0, attacker_position, false, false, true) # is_dodged = true
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, false) # false = not magical, false = not critical (enemy attacks don't crit yet)
# Apply the reduced damage using take_damage (which handles health modification and signals)
var _old_hp = character_stats.hp
character_stats.modify_health(-actual_damage)
if character_stats.hp <= 0:
character_stats.no_health.emit()
character_stats.character_changed.emit(character_stats)
print(name, " took ", actual_damage, " damage (", amount, " base - ", character_stats.defense, " DEF = ", actual_damage, ")! Health: ", character_stats.hp, "/", character_stats.maxhp)
else:
# Fallback for legacy
current_health -= amount
actual_damage = amount
print(name, " took ", amount, " damage! Health: ", current_health)
# Play damage sound effect
@@ -2129,11 +2219,11 @@ func take_damage(amount: float, attacker_position: Vector2):
tween.tween_property(sprite_body, "modulate", Color.WHITE, 0.1)
# Show damage number (red, using dmg_numbers.png font)
_show_damage_number(amount, attacker_position)
_show_damage_number(actual_damage, attacker_position)
# Sync damage visual effects to other clients (including damage numbers)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_sync_damage.rpc(amount, attacker_position)
_sync_damage.rpc(actual_damage, attacker_position)
# Check if dead - but wait for damage animation to play first
var health = character_stats.hp if character_stats else current_health
@@ -2493,11 +2583,9 @@ func use_key():
return false
@rpc("authority", "reliable")
func _show_damage_number(amount: float, from_position: Vector2):
func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false):
# Show damage number (red, using dmg_numbers.png font) above player
# Only show if damage > 0
if amount <= 0:
return
# Show even if amount is 0 for MISS/DODGED
var damage_number_scene = preload("res://scenes/damage_number.tscn")
if not damage_number_scene:
@@ -2507,9 +2595,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_crit else Color.RED
# Calculate direction from attacker (slight upward variation)
var direction_from_attacker = (global_position - from_position).normalized()
@@ -2531,12 +2626,81 @@ func _show_damage_number(amount: float, from_position: Vector2):
get_tree().current_scene.add_child(damage_label)
damage_label.global_position = global_position + Vector2(0, -16)
func _on_level_up_stats(stats_increased: Array):
# Show floating text for level up - "LEVEL UP!" and stat increases
# Use damage_number scene with damage_numbers font
if not character_stats:
return
# Stat name to display name mapping
var stat_display_names = {
"str": "STR",
"dex": "DEX",
"int": "INT",
"end": "END",
"wis": "WIS",
"lck": "LCK"
}
# Stat name to color mapping
var stat_colors = {
"str": Color.RED,
"dex": Color.GREEN,
"int": Color.BLUE,
"end": Color.WHITE,
"wis": Color(0.5, 0.0, 0.5), # Purple
"lck": Color.YELLOW
}
var damage_number_scene = preload("res://scenes/damage_number.tscn")
if not damage_number_scene:
return
# Get entities node for adding text
var game_world = get_tree().get_first_node_in_group("game_world")
var entities_node = null
if game_world:
entities_node = game_world.get_node_or_null("Entities")
if not entities_node:
entities_node = get_tree().current_scene
var base_y_offset = -32.0 # Start above player head
var y_spacing = 12.0 # Space between each text
# Show "LEVEL UP!" first (in white)
var level_up_text = damage_number_scene.instantiate()
if level_up_text:
level_up_text.label = "LEVEL UP!"
level_up_text.color = Color.WHITE
level_up_text.direction = Vector2(0, -1) # Straight up
entities_node.add_child(level_up_text)
level_up_text.global_position = global_position + Vector2(0, base_y_offset)
base_y_offset -= y_spacing
# Show each stat increase
for i in range(stats_increased.size()):
var stat_name = stats_increased[i]
var stat_text = damage_number_scene.instantiate()
if stat_text:
var display_name = stat_display_names.get(stat_name, stat_name.to_upper())
stat_text.label = "+1 " + display_name
stat_text.color = stat_colors.get(stat_name, Color.WHITE)
stat_text.direction = Vector2(randf_range(-0.2, 0.2), -1.0).normalized() # Slight random spread
entities_node.add_child(stat_text)
stat_text.global_position = global_position + Vector2(0, base_y_offset)
base_y_offset -= y_spacing
@rpc("any_peer", "reliable")
func _sync_damage(_amount: float, attacker_position: Vector2):
func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false):
# This RPC only syncs visual effects, not damage application
# (damage is already applied via rpc_take_damage)
if not is_multiplayer_authority():
# Play damage sound effect on clients
# If dodged, only show dodge text, no other effects
if is_dodged:
_show_damage_number(0.0, attacker_position, false, false, true)
return
# Play damage sound and effects
if sfx_take_damage:
sfx_take_damage.play()
@@ -2561,6 +2725,9 @@ func _sync_damage(_amount: float, attacker_position: Vector2):
var tween = create_tween()
tween.tween_property(sprite_body, "modulate", Color.RED, 0.1)
tween.tween_property(sprite_body, "modulate", Color.WHITE, 0.1)
# Show damage number
_show_damage_number(_amount, attacker_position, is_crit, is_miss, false)
func on_grabbed(by_player):
print(name, " grabbed by ", by_player.name)