Added rpg system for combat
added lots of loot to find added level up system
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user