lots of changeru

This commit is contained in:
2026-01-25 16:27:19 +01:00
parent a95e22d2fa
commit 7abadb92a9
14 changed files with 855 additions and 106 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dyid2xlxo1gnn"
path="res://.godot/imported/hand_monster.png-a541f67ec1e51b48eea2792ad28a46cc.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/gfx/enemies/hand_monster.png"
dest_files=["res://.godot/imported/hand_monster.png-a541f67ec1e51b48eea2792ad28a46cc.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -3,10 +3,77 @@
[ext_resource type="Script" uid="uid://bax7e73v836nx" path="res://scripts/player_manager.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://cxfvw8y7jqn2p" path="res://scenes/player.tscn" id="2"]
[ext_resource type="Script" uid="uid://db58xcyo4cjk" path="res://scripts/game_world.gd" id="4"]
[ext_resource type="Shader" uid="uid://dob36l1rwi2en" path="res://shaders/game_world.gdshader" id="4_bhwwd"]
[ext_resource type="Script" uid="uid://wff5063ctp7g" path="res://scripts/debug_overlay.gd" id="5"]
[ext_resource type="AudioStream" uid="uid://dthr2w8x0cj6v" path="res://assets/audio/sfx/ambience/wind-castle-loop.wav.mp3" id="6_6c6v5"]
[ext_resource type="TileSet" uid="uid://dqem5tbvooxrg" path="res://assets/gfx/RPG DUNGEON VOL 3.tres" id="9"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_pdbwf"]
shader = ExtResource("4_bhwwd")
shader_parameter/original_0 = Color(0, 0, 0, 1)
shader_parameter/original_1 = Color(0, 0, 0, 1)
shader_parameter/original_2 = Color(0, 0, 0, 1)
shader_parameter/original_3 = Color(0, 0, 0, 1)
shader_parameter/original_4 = Color(0, 0, 0, 1)
shader_parameter/original_5 = Color(0, 0, 0, 1)
shader_parameter/original_6 = Color(0, 0, 0, 1)
shader_parameter/original_7 = Color(0, 0, 0, 1)
shader_parameter/original_8 = Color(0, 0, 0, 1)
shader_parameter/original_9 = Color(0, 0, 0, 1)
shader_parameter/original_10 = Color(0, 0, 0, 1)
shader_parameter/original_11 = Color(0, 0, 0, 1)
shader_parameter/original_12 = Color(0, 0, 0, 1)
shader_parameter/original_13 = Color(0, 0, 0, 1)
shader_parameter/replace_0 = Color(0, 0, 0, 1)
shader_parameter/replace_1 = Color(0, 0, 0, 1)
shader_parameter/replace_2 = Color(0, 0, 0, 1)
shader_parameter/replace_3 = Color(0, 0, 0, 1)
shader_parameter/replace_4 = Color(0, 0, 0, 1)
shader_parameter/replace_5 = Color(0, 0, 0, 1)
shader_parameter/replace_6 = Color(0, 0, 0, 1)
shader_parameter/replace_7 = Color(0, 0, 0, 1)
shader_parameter/replace_8 = Color(0, 0, 0, 1)
shader_parameter/replace_9 = Color(0, 0, 0, 1)
shader_parameter/replace_10 = Color(0, 0, 0, 1)
shader_parameter/replace_11 = Color(0, 0, 0, 1)
shader_parameter/replace_12 = Color(0, 0, 0, 1)
shader_parameter/replace_13 = Color(0, 0, 0, 1)
shader_parameter/tint = Color(1, 1, 1, 1)
shader_parameter/ambient = Color(1, 1, 1, 1)
[sub_resource type="ShaderMaterial" id="ShaderMaterial_bhwwd"]
shader = ExtResource("4_bhwwd")
shader_parameter/original_0 = Color(0, 0, 0, 1)
shader_parameter/original_1 = Color(0, 0, 0, 1)
shader_parameter/original_2 = Color(0, 0, 0, 1)
shader_parameter/original_3 = Color(0, 0, 0, 1)
shader_parameter/original_4 = Color(0, 0, 0, 1)
shader_parameter/original_5 = Color(0, 0, 0, 1)
shader_parameter/original_6 = Color(0, 0, 0, 1)
shader_parameter/original_7 = Color(0, 0, 0, 1)
shader_parameter/original_8 = Color(0, 0, 0, 1)
shader_parameter/original_9 = Color(0, 0, 0, 1)
shader_parameter/original_10 = Color(0, 0, 0, 1)
shader_parameter/original_11 = Color(0, 0, 0, 1)
shader_parameter/original_12 = Color(0, 0, 0, 1)
shader_parameter/original_13 = Color(0, 0, 0, 1)
shader_parameter/replace_0 = Color(0, 0, 0, 1)
shader_parameter/replace_1 = Color(0, 0, 0, 1)
shader_parameter/replace_2 = Color(0, 0, 0, 1)
shader_parameter/replace_3 = Color(0, 0, 0, 1)
shader_parameter/replace_4 = Color(0, 0, 0, 1)
shader_parameter/replace_5 = Color(0, 0, 0, 1)
shader_parameter/replace_6 = Color(0, 0, 0, 1)
shader_parameter/replace_7 = Color(0, 0, 0, 1)
shader_parameter/replace_8 = Color(0, 0, 0, 1)
shader_parameter/replace_9 = Color(0, 0, 0, 1)
shader_parameter/replace_10 = Color(0, 0, 0, 1)
shader_parameter/replace_11 = Color(0, 0, 0, 1)
shader_parameter/replace_12 = Color(0, 0, 0, 1)
shader_parameter/replace_13 = Color(0, 0, 0, 1)
shader_parameter/tint = Color(1, 1, 1, 1)
shader_parameter/ambient = Color(1, 1, 1, 1)
[node name="GameWorld" type="Node2D" unique_id=430665106]
script = ExtResource("4")
@@ -21,11 +88,13 @@ zoom = Vector2(3, 3)
[node name="DungeonLayer0" type="TileMapLayer" parent="Environment" unique_id=1234567891]
z_index = -2
material = SubResource("ShaderMaterial_pdbwf")
tile_set = ExtResource("9")
[node name="TileMapLayerAbove" type="TileMapLayer" parent="Environment" unique_id=1234567892]
modulate = Color(1, 1, 1, 0.46666667)
z_index = 1
material = SubResource("ShaderMaterial_bhwwd")
tile_set = ExtResource("9")
[node name="Entities" type="Node2D" parent="." unique_id=1447395523]

View File

@@ -367,6 +367,12 @@ func _deal_explosion_damage():
print("Bomb hit enemy: ", body.name, " for ", final_damage, " damage!")
# Deal damage to interactable objects (pots, boxes, etc.)
elif body.has_method("can_be_destroyed") and body.can_be_destroyed() and body.has_method("take_damage"):
var attacker_pos = player_owner.global_position if player_owner else global_position
body.take_damage(final_damage, attacker_pos)
print("Bomb hit interactable: ", body.name, " for ", final_damage, " damage!")
func _spawn_explosion_tile_particles():
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world:

View File

@@ -1,10 +1,11 @@
extends Node2D
# Frostspike spell — instant damage, no debuff. Frames 44134416, blue PointLight2D.
# If is_center: spawn center spike, then 0.5s later spawn 4 adjacent spikes.
# If is_center: spawn center spike, then 0.5s later spawn 4 adjacent, then third-wave center (2x scale, 2x damage).
var player_owner: Node = null
var damage: float = 15.0
var damage_mult: float = 1.0
var is_center: bool = false
var damage_dealt: bool = false
var elapsed: float = 0.0
@@ -25,11 +26,14 @@ func _ready() -> void:
if is_center:
_spawn_adjacent_after_delay()
func setup(target_pos: Vector2, owner_player: Node, damage_value: float, center: bool) -> void:
func setup(target_pos: Vector2, owner_player: Node, damage_value: float, center: bool, scale_mult: float = 1.0, dmg_mult: float = 1.0) -> void:
global_position = target_pos
player_owner = owner_player
damage = damage_value
damage_mult = dmg_mult
is_center = center
if scale_mult != 1.0:
scale = Vector2(scale_mult, scale_mult)
func _spawn_adjacent_after_delay() -> void:
await get_tree().create_timer(0.5).timeout
@@ -48,8 +52,12 @@ func _spawn_adjacent_after_delay() -> void:
var par = get_parent()
for pos in adjacent:
var sp = scene.instantiate()
par.add_child(sp)
sp.setup(pos, player_owner, damage, false)
par.add_child(sp)
# Third wave: center again, 2x scale, 2x damage (most damage)
var third = scene.instantiate()
third.setup(global_position, player_owner, damage, false, 2.0, 2.0)
par.add_child(third)
_finish_center_spike()
func _finish_center_spike() -> void:
@@ -77,7 +85,7 @@ func _deal_damage_once() -> void:
for body in hit_area.get_overlapping_bodies():
if body == player_owner:
continue
var final_damage = damage
var final_damage = damage * damage_mult
if player_owner and player_owner.character_stats:
var int_stat = player_owner.character_stats.baseStats.int + player_owner.character_stats.get_pass("int")
final_damage += int_stat * 0.5
@@ -100,3 +108,5 @@ func _deal_damage_once() -> void:
body.rpc_take_damage.rpc_id(eid, final_damage, attacker_pos, false, false, false)
else:
body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false, false)
elif body.has_method("can_be_destroyed") and body.can_be_destroyed() and body.has_method("take_damage"):
body.take_damage(final_damage, attacker_pos)

View File

@@ -279,8 +279,12 @@ func level_up() -> void:
health_changed.emit(hp, maxhp)
mana_changed.emit(mp, maxmp)
func modify_health(amount: float) -> void:
hp = clamp(hp + amount, 0, maxhp)
func modify_health(amount: float, allow_overheal: bool = false) -> void:
hp += amount
if allow_overheal:
hp = max(0.0, hp)
else:
hp = clamp(hp, 0.0, maxhp)
health_changed.emit(hp, maxhp)
character_changed.emit(self)
@@ -320,8 +324,8 @@ func take_damage(amount: float, is_magical: bool = false) -> float:
character_changed.emit(self)
return actual_damage
func heal(amount: float) -> void:
modify_health(amount)
func heal(amount: float, allow_overheal: bool = false) -> void:
modify_health(amount, allow_overheal)
func use_mana(amount: float) -> bool:
if mp >= amount:

View File

@@ -108,6 +108,12 @@ func _ready() -> void:
if index_str.is_valid_int():
set_meta("door_index", index_str.to_int())
func _update_door_visibility():
# Hide door sprite when open; show when closed. Re-evaluate on any state change.
var sprite = get_node_or_null("Sprite2D")
if sprite:
sprite.visible = is_closed
func _update_door_texture():
# Update door texture based on door type
var sprite = get_node_or_null("Sprite2D")
@@ -297,6 +303,8 @@ func _process(delta: float) -> void:
is_closed = true
set_collision_layer_value(7, true)
_update_door_visibility()
func _update_collision_based_on_position():
# Update collision based on whether door is at closed position or moved away
# CRITICAL: This function should NEVER be called for KeyDoors!
@@ -607,6 +615,8 @@ func _ready_after_setup():
set_collision_layer_value(7, true)
LogManager.log("Door: Fixed state - door is now CLOSED (is_closed: " + str(is_closed) + ", collision: " + str(get_collision_layer_value(7)) + ")", LogManager.CATEGORY_DOOR)
_update_door_visibility()
# NOTE: Doors are NOT connected via signals to room triggers
# Instead, room triggers call door._on_room_entered() directly
# This prevents doors from reacting to ALL room entries, only their own blocking room

View File

@@ -1127,7 +1127,7 @@ func _show_loot_floating_text(player: Node, text: String, color: Color, item_tex
floating_text.global_position = Vector2(player.global_position.x, player.global_position.y - 20)
floating_text.setup(text, color, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame)
func _apply_heal_spell_sync(target_name: String, amount: float):
func _apply_heal_spell_sync(target_name: String, amount_to_apply: float, display_amount: int, is_crit: bool, is_overheal: bool, allow_overheal: bool):
var target: Node = null
for p in get_tree().get_nodes_in_group("player"):
if p.name == target_name and is_instance_valid(p):
@@ -1137,8 +1137,8 @@ func _apply_heal_spell_sync(target_name: String, amount: float):
return
var me = multiplayer.get_unique_id()
var tid = target.get_multiplayer_authority()
if me == tid and target.has_method("heal"):
target.heal(amount)
if me == tid and target.has_method("heal") and amount_to_apply > 0:
target.heal(amount_to_apply, allow_overheal)
var entities = get_node_or_null("Entities")
var parent = entities if entities else target.get_parent()
if not parent:
@@ -1150,7 +1150,15 @@ func _apply_heal_spell_sync(target_name: String, amount: float):
eff.global_position = target.global_position
if eff.has_method("setup"):
eff.setup(target)
_show_loot_floating_text(target, "+" + str(int(amount)) + " HP", Color.GREEN, null, 1, 1, 0)
var prefix = ""
if is_crit and is_overheal:
prefix = "CRIT OVERHEAL! "
elif is_crit:
prefix = "CRIT! "
elif is_overheal:
prefix = "OVERHEAL! "
var heal_text = prefix + "+" + str(display_amount) + " HP"
_show_loot_floating_text(target, heal_text, Color.GREEN, null, 1, 1, 0)
@rpc("authority", "unreliable")
func _sync_enemy_position(enemy_name: String, enemy_index: int, pos: Vector2, vel: Vector2, z_pos: float, dir: int, frame: int, anim: String, frame_num: int, state_value: int):
@@ -2102,6 +2110,20 @@ func _is_valid_spell_target(target_pos: Vector2, player_pos: Vector2) -> bool:
return true
func _is_walkable_tile(tile_center: Vector2) -> bool:
"""True if tile is floor/door/corridor (not a wall). No raycast - use for adjacent spikes only."""
if dungeon_data.is_empty() or not dungeon_data.has("grid"):
return false
var tile_size = 16
var tile_x = int(tile_center.x / tile_size)
var tile_y = int(tile_center.y / tile_size)
var grid = dungeon_data.grid
var map_size = dungeon_data.map_size
if tile_x < 0 or tile_y < 0 or tile_x >= map_size.x or tile_y >= map_size.y:
return false
var v = grid[tile_x][tile_y]
return v == 1 or v == 2 or v == 3
func _get_adjacent_valid_spell_tile_centers(center_world_pos: Vector2, player_pos: Vector2) -> Array:
var out: Array = []
if not dungeon_tilemap_layer or dungeon_data.is_empty() or not dungeon_data.has("grid"):
@@ -2111,7 +2133,7 @@ func _get_adjacent_valid_spell_tile_centers(center_world_pos: Vector2, player_po
for off in offsets:
var t = center_tile + off
var tile_center = dungeon_tilemap_layer.map_to_local(t) + dungeon_tilemap_layer.global_position
if _is_valid_spell_target(tile_center, player_pos):
if _is_walkable_tile(tile_center):
out.append(tile_center)
return out
@@ -2693,6 +2715,183 @@ func _generate_dungeon():
LogManager.log("GameWorld: Dungeon generation completed successfully", LogManager.CATEGORY_DUNGEON)
# Dungeon shader color replacement: 13 original colors (wall x6, ground x5, fallout x2)
const _DUNGEON_ORIGINALS: Array = [
Color(24/255.0, 59/255.0, 255/255.0), # 0 wall
Color(33/255.0, 50/255.0, 195/255.0), # 1 wall
Color(98/255.0, 29/255.0, 93/255.0), # 2 wall
Color(66/255.0, 13/255.0, 52/255.0), # 3 wall
Color(74/255.0, 33/255.0, 134/255.0), # 4 wall
Color(50/255.0, 12/255.0, 23/255.0), # 5 wall
Color(149/255.0, 79/255.0, 111/255.0), # 6 ground
Color(192/255.0, 95/255.0, 193/255.0), # 7 ground
Color(48/255.0, 38/255.0, 20/255.0), # 8 ground
Color(143/255.0, 71/255.0, 112/255.0), # 9 ground
Color(106/255.0, 62/255.0, 57/255.0), # 10 ground
Color(69/255.0, 42/255.0, 31/255.0), # 11 fallout
Color(53/255.0, 46/255.0, 26/255.0), # 12 fallout
]
# Original wall slots ordered light→dark (by luminance). Tile art uses these for highlight/mid/shadow.
# We must assign scheme colors so light→dark is preserved, or shading inverts and looks wrong.
const _WALL_LIGHT_TO_DARK_ORDER: Array = [0, 1, 2, 4, 3, 5]
func _luminance(c: Color) -> float:
return 0.299 * c.r + 0.587 * c.g + 0.114 * c.b
func _reorder_wall_colors_by_luminance(six_colors: Array) -> Array:
assert(six_colors.size() == 6)
var with_lum: Array = []
for i in range(6):
var c = six_colors[i] as Color
with_lum.append({"color": c, "lum": _luminance(c)})
with_lum.sort_custom(func(a, b): return (a["lum"] as float) > (b["lum"] as float))
var sorted_colors: Array = []
for e in with_lum:
sorted_colors.append(e["color"])
var out: Array = []
for i in range(6):
out.append(Color.BLACK)
out[0] = sorted_colors[0]
out[1] = sorted_colors[1]
out[2] = sorted_colors[2]
out[3] = sorted_colors[4]
out[4] = sorted_colors[3]
out[5] = sorted_colors[5]
return out
func _get_dungeon_color_scheme(scheme_index: int) -> Array:
var o = _DUNGEON_ORIGINALS
var walls: Array
var ground_fallout: Array
match scheme_index:
0: # 1⃣ Arcane Blue (magic / night / mana)
walls = [
Color(24/255.0, 59/255.0, 255/255.0), Color(80/255.0, 120/255.0, 255/255.0), Color(140/255.0, 180/255.0, 255/255.0),
Color(10/255.0, 30/255.0, 120/255.0), Color(180/255.0, 200/255.0, 255/255.0), Color(220/255.0, 230/255.0, 255/255.0),
]
ground_fallout = [
Color(0.78, 0.48, 0.24), Color(0.84, 0.54, 0.30), Color(0.58, 0.36, 0.16),
Color(0.72, 0.44, 0.22), Color(0.66, 0.40, 0.20), Color(0.38, 0.30, 0.22),
Color(0.32, 0.28, 0.20),
]
1: # 2⃣ Crimson Void (blood / corruption / danger)
walls = [
Color(120/255.0, 20/255.0, 40/255.0), Color(180/255.0, 40/255.0, 60/255.0), Color(220/255.0, 90/255.0, 110/255.0),
Color(60/255.0, 5/255.0, 20/255.0), Color(255/255.0, 140/255.0, 160/255.0), Color(90/255.0, 10/255.0, 30/255.0),
]
ground_fallout = [
Color(0.22, 0.58, 0.62), Color(0.28, 0.64, 0.66), Color(0.18, 0.48, 0.52),
Color(0.20, 0.54, 0.58), Color(0.20, 0.50, 0.54), Color(0.26, 0.38, 0.34),
Color(0.22, 0.34, 0.30),
]
2: # 3⃣ Toxic Green (poison / nature / alchemy)
walls = [
Color(20/255.0, 120/255.0, 40/255.0), Color(60/255.0, 180/255.0, 90/255.0), Color(120/255.0, 220/255.0, 160/255.0),
Color(10/255.0, 60/255.0, 25/255.0), Color(180/255.0, 255/255.0, 210/255.0), Color(40/255.0, 90/255.0, 55/255.0),
]
ground_fallout = [
Color(0.64, 0.36, 0.72), Color(0.70, 0.42, 0.78), Color(0.48, 0.26, 0.56),
Color(0.58, 0.32, 0.66), Color(0.54, 0.30, 0.62), Color(0.34, 0.26, 0.38),
Color(0.28, 0.22, 0.32),
]
3: # 4⃣ Stone Grey (industrial / ruins / UI neutral)
walls = [
Color(40/255.0, 40/255.0, 45/255.0), Color(80/255.0, 80/255.0, 85/255.0), Color(130/255.0, 130/255.0, 135/255.0),
Color(20/255.0, 20/255.0, 25/255.0), Color(180/255.0, 180/255.0, 185/255.0), Color(220/255.0, 220/255.0, 225/255.0),
]
ground_fallout = [
Color(0.50, 0.50, 0.52), Color(0.55, 0.55, 0.57), Color(0.35, 0.35, 0.38),
Color(0.48, 0.48, 0.50), Color(0.42, 0.42, 0.45), Color(0.28, 0.28, 0.30),
Color(0.24, 0.24, 0.26),
]
4: # 5⃣ Royal Purple (arcane royalty / bosses)
walls = [
Color(80/255.0, 30/255.0, 130/255.0), Color(130/255.0, 70/255.0, 180/255.0), Color(180/255.0, 130/255.0, 220/255.0),
Color(40/255.0, 10/255.0, 80/255.0), Color(220/255.0, 180/255.0, 255/255.0), Color(100/255.0, 60/255.0, 150/255.0),
]
ground_fallout = [
Color(0.90, 0.68, 0.22), Color(0.94, 0.74, 0.28), Color(0.72, 0.52, 0.14),
Color(0.84, 0.60, 0.18), Color(0.78, 0.56, 0.16), Color(0.46, 0.36, 0.20),
Color(0.38, 0.30, 0.18),
]
5: # 6⃣ Desert Gold (sand / temples / sunlight)
walls = [
Color(150/255.0, 110/255.0, 40/255.0), Color(200/255.0, 160/255.0, 80/255.0), Color(240/255.0, 210/255.0, 140/255.0),
Color(90/255.0, 60/255.0, 15/255.0), Color(255/255.0, 230/255.0, 170/255.0), Color(170/255.0, 130/255.0, 60/255.0),
]
ground_fallout = [
Color(0.22, 0.58, 0.62), Color(0.28, 0.64, 0.66), Color(0.18, 0.48, 0.52),
Color(0.20, 0.54, 0.58), Color(0.20, 0.50, 0.54), Color(0.26, 0.38, 0.34),
Color(0.22, 0.34, 0.30),
]
6: # 7⃣ Neon Cyber (sci-fi / UI / hacking)
walls = [
Color(20/255.0, 240/255.0, 220/255.0), Color(240/255.0, 60/255.0, 220/255.0), Color(120/255.0, 120/255.0, 255/255.0),
Color(10/255.0, 20/255.0, 40/255.0), Color(255/255.0, 255/255.0, 255/255.0), Color(80/255.0, 255/255.0, 180/255.0),
]
ground_fallout = [
Color(0.45, 0.28, 0.55), Color(0.52, 0.35, 0.62), Color(0.38, 0.22, 0.48),
Color(0.48, 0.32, 0.58), Color(0.42, 0.26, 0.52), Color(0.28, 0.18, 0.38),
Color(0.22, 0.14, 0.32),
]
7: # 8⃣ Infernal Lava (hell / bosses / damage)
walls = [
Color(180/255.0, 40/255.0, 20/255.0), Color(240/255.0, 90/255.0, 30/255.0), Color(255/255.0, 160/255.0, 80/255.0),
Color(90/255.0, 10/255.0, 5/255.0), Color(255/255.0, 210/255.0, 160/255.0), Color(140/255.0, 30/255.0, 15/255.0),
]
ground_fallout = [
Color(0.32, 0.68, 0.48), Color(0.38, 0.74, 0.54), Color(0.22, 0.52, 0.36),
Color(0.28, 0.62, 0.44), Color(0.26, 0.58, 0.40), Color(0.26, 0.34, 0.28),
Color(0.22, 0.30, 0.24),
]
_:
return o.duplicate()
if walls.size() == 6 and ground_fallout.size() == 7:
walls = _reorder_wall_colors_by_luminance(walls)
var out: Array = []
out.append_array(walls)
out.append_array(ground_fallout)
return out
return o.duplicate()
func _apply_dungeon_color_scheme() -> void:
var scheme_idx = (abs(dungeon_seed) + current_level) % 8
var replace_colors = _get_dungeon_color_scheme(scheme_idx)
var shader_res = load("res://shaders/game_world.gdshader") as Shader
if not shader_res:
return
for layer in [dungeon_tilemap_layer, dungeon_tilemap_layer_above]:
if not layer or not is_instance_valid(layer):
continue
var mat = layer.material
if not mat or not (mat is ShaderMaterial):
mat = ShaderMaterial.new()
mat.shader = shader_res
layer.material = mat
var sm = mat as ShaderMaterial
for i in range(13):
var orig = _DUNGEON_ORIGINALS[i] as Color
var rpl = replace_colors[i] as Color
sm.set_shader_parameter("original_" + str(i), orig)
sm.set_shader_parameter("replace_" + str(i), rpl)
# Index 13 unused; set to no-op (original same as replace, distinct from tile colors)
var neutral = Color(0.0, 0.0, 0.0, 1.0)
sm.set_shader_parameter("original_13", neutral)
sm.set_shader_parameter("replace_13", neutral)
# TileMapLayerAbove: tint ffffff77 for slight transparency
if layer == dungeon_tilemap_layer_above:
sm.set_shader_parameter("tint", Color(1.0, 1.0, 1.0, 0x77 / 255.0))
else:
sm.set_shader_parameter("tint", Color(1.0, 1.0, 1.0, 1.0))
# Apply CanvasModulate-style darkening via ambient (unshaded shader bypasses CanvasModulate)
var cm = get_node_or_null("CanvasModulate")
var ambient_color = Color(1.0, 1.0, 1.0, 1.0)
if cm and is_instance_valid(cm):
ambient_color = cm.color
sm.set_shader_parameter("ambient", ambient_color)
LogManager.log("GameWorld: Applied dungeon color scheme " + str(scheme_idx) + " (seed " + str(dungeon_seed) + ", level " + str(current_level) + ")", LogManager.CATEGORY_DUNGEON)
func _render_dungeon():
if dungeon_data.is_empty():
push_error("ERROR: Cannot render dungeon - no dungeon data!")
@@ -2879,6 +3078,9 @@ func _render_dungeon():
# Create stairs Area2D if stairs data exists
_create_stairs_area()
# Randomize dungeon color scheme (seed-based)
_apply_dungeon_color_scheme()
func _update_spawn_points(target_room: Dictionary = {}, clear_existing: bool = true):
# Update player manager spawn points based on a room
# If target_room is empty, use start room (for initial spawn)

View File

@@ -430,6 +430,25 @@ func take_fire_damage(amount: float, _attacker_position: Vector2) -> void:
else:
_break_into_pieces()
func take_damage(amount: float, _from_position: Vector2) -> void:
"""Generic damage from bomb, frost spike, etc. Any destroyable object."""
if not is_destroyable or is_broken:
return
health -= amount
if health > 0:
return
var game_world = get_tree().get_first_node_in_group("game_world")
if multiplayer.has_multiplayer_peer():
if multiplayer.is_server():
if game_world and game_world.has_method("_rpc_to_ready_peers"):
game_world._rpc_to_ready_peers("_sync_object_break", [name])
_break_into_pieces()
else:
if game_world and game_world.has_method("_sync_object_break"):
game_world._sync_object_break.rpc_id(1, name)
else:
_break_into_pieces()
func on_grabbed(by_player):
# Special handling for chests - open instead of grab
if object_type == "Chest" and not is_chest_opened:

View File

@@ -38,11 +38,40 @@ var equipment_selection_index: int = 0 # Current equipment slot index (0-5: main
@onready var sfx_food: AudioStreamPlayer2D = $SfxFood
@onready var sfx_armour: AudioStreamPlayer2D = $SfxArmour
# Bar layout constants (align X/Y + bar across rows)
const _BAR_WIDTH: int = 100
const _BAR_VALUE_MIN_WIDTH: int = 44 # "999/999"
const _BAR_LABEL_MIN_WIDTH: int = 52 # "Weight:", "Exp:", "HP:", "MP:", "Coin:"
# Weight UI elements (created programmatically)
var weight_container: HBoxContainer = null
var weight_label: Label = null
var weight_value_label: Label = null
var weight_progress_bar: ProgressBar = null
# Exp UI elements (like weight)
var exp_container: HBoxContainer = null
var exp_label: Label = null
var exp_value_label: Label = null
var exp_progress_bar: ProgressBar = null
# HP / MP bar elements
var hp_container: HBoxContainer = null
var hp_label: Label = null
var hp_value_label: Label = null
var hp_progress_bar: ProgressBar = null
var mp_container: HBoxContainer = null
var mp_label: Label = null
var mp_value_label: Label = null
var mp_progress_bar: ProgressBar = null
# Coin UI elements ("Coin:" + 6-frame sprite + "X N")
var coin_container: HBoxContainer = null
var coin_label: Label = null
var coin_sprite: Sprite2D = null
var coin_value_label: Label = null
var coin_anim_time: float = 0.0
# Store button/item mappings for selection highlighting
var inventory_buttons: Dictionary = {} # item -> button
var equipment_buttons: Dictionary = {} # slot_name -> button
@@ -80,8 +109,12 @@ func _ready():
# Create equipment slot buttons (dynamically)
_create_equipment_slots()
# Create weight progress bar
# Create HP/MP bars, then weight, exp, coin (order in stats panel)
_create_hp_ui()
_create_mp_ui()
_create_weight_ui()
_create_exp_ui()
_create_coin_ui()
# Setup selection rectangle (already in scene, just configure it)
_setup_selection_rectangle()
@@ -175,43 +208,75 @@ func _update_stats():
var race_text = char_stats.race
stats_label.text = "Stats - " + race_text
# Update base stats
label_base_stats_value.text = str(char_stats.level) + "\n\n" + \
str(int(char_stats.hp)) + "/" + str(int(char_stats.maxhp)) + "\n" + \
str(int(char_stats.mp)) + "/" + str(int(char_stats.maxmp)) + "\n\n" + \
str(char_stats.baseStats.str) + "\n" + \
str(char_stats.baseStats.dex) + "\n" + \
str(char_stats.baseStats.end) + "\n" + \
str(char_stats.baseStats.int) + "\n" + \
str(char_stats.baseStats.wis) + "\n" + \
str(char_stats.baseStats.lck)
# Base stats: Level, STR, DEX, END, INT, WIS, LCK, PER (HP/MP are bars below)
if label_base_stats:
label_base_stats.text = "Level\n\nSTR\nDEX\nEND\nINT\nWIS\nLCK\nPER"
if label_base_stats_value:
label_base_stats_value.text = str(char_stats.level) + "\n\n" + \
str(char_stats.baseStats.str) + "\n" + \
str(char_stats.baseStats.dex) + "\n" + \
str(char_stats.baseStats.end) + "\n" + \
str(char_stats.baseStats.int) + "\n" + \
str(char_stats.baseStats.wis) + "\n" + \
str(char_stats.baseStats.lck) + "\n" + \
str(char_stats.baseStats.get("per", 10))
# Update derived stats
label_derived_stats_value.text = str(int(char_stats.xp)) + "/" + str(int(char_stats.xp_to_next_level)) + "\n" + \
str(char_stats.coin) + "\n\n\n\n" + \
str(char_stats.damage) + "\n" + \
str(char_stats.defense) + "\n" + \
str(char_stats.move_speed) + "\n" + \
str(char_stats.attack_speed) + "\n" + \
str(char_stats.sight)
# Derived stats: DMG, DEF, MovSpd, AtkSpd, Sight, SpellAmp, Crit% (XP/Coin moved to exp meter & coin UI)
if label_derived_stats:
label_derived_stats.text = "DMG\nDEF\nMovSpd\nAtkSpd\nSight\nSpellAmp\nCrit%"
if label_derived_stats_value:
label_derived_stats_value.text = "%.1f\n%.1f\n%.2f\n%.2f\n%.1f\n%.1f\n%.1f%%" % [
char_stats.damage,
char_stats.defense,
char_stats.move_speed,
char_stats.attack_speed,
char_stats.sight,
char_stats.spell_amp,
char_stats.crit_chance
]
# Update weight progress bar
if weight_progress_bar and weight_label:
# HP bar
if hp_progress_bar and hp_value_label:
hp_progress_bar.max_value = max(1.0, char_stats.maxhp)
hp_progress_bar.value = char_stats.hp
hp_value_label.text = str(int(char_stats.hp)) + "/" + str(int(char_stats.maxhp))
# MP bar
if mp_progress_bar and mp_value_label:
mp_progress_bar.max_value = max(1.0, char_stats.maxmp)
mp_progress_bar.value = char_stats.mp
mp_value_label.text = str(int(char_stats.mp)) + "/" + str(int(char_stats.maxmp))
# Exp meter (like weight)
if exp_progress_bar and exp_value_label:
var xp = char_stats.xp
var xp_next = char_stats.xp_to_next_level
exp_progress_bar.max_value = max(1.0, xp_next)
exp_progress_bar.value = xp
exp_value_label.text = str(int(xp)) + "/" + str(int(xp_next))
var fill_exp = StyleBoxFlat.new()
fill_exp.bg_color = Color(0.55, 0.35, 0.95)
exp_progress_bar.add_theme_stylebox_override("fill", fill_exp)
# Coin: "Coin:" + 6-frame sprite + "X <count>"
if coin_value_label:
coin_value_label.text = "X " + str(char_stats.coin)
# Weight progress bar
if weight_progress_bar and weight_value_label:
var current_weight = char_stats.get_total_weight()
var max_weight = char_stats.get_carrying_capacity()
weight_progress_bar.max_value = max_weight
weight_progress_bar.value = current_weight
weight_label.text = "Weight: " + str(int(current_weight)) + "/" + str(int(max_weight))
# Change color based on weight (green -> yellow -> red)
weight_value_label.text = str(int(current_weight)) + "/" + str(int(max_weight))
var weight_ratio = current_weight / max_weight
var fill_style = StyleBoxFlat.new()
if weight_ratio < 0.7:
fill_style.bg_color = Color(0.6, 0.8, 0.3) # Green
fill_style.bg_color = Color(0.6, 0.8, 0.3)
elif weight_ratio < 0.9:
fill_style.bg_color = Color(0.9, 0.8, 0.2) # Yellow
fill_style.bg_color = Color(0.9, 0.8, 0.2)
else:
fill_style.bg_color = Color(0.9, 0.3, 0.2) # Red
fill_style.bg_color = Color(0.9, 0.3, 0.2)
weight_progress_bar.add_theme_stylebox_override("fill", fill_style)
func _create_equipment_slots():
@@ -269,45 +334,134 @@ func _create_equipment_slots():
equipment_slots[slot_name] = button
equipment_buttons[slot_name] = button
func _create_weight_ui():
# Create weight display (label + progress bar)
func _style_bar_font(lbl: Label) -> void:
lbl.add_theme_font_size_override("font_size", 10)
if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
var fr = load("res://assets/fonts/standard_font.png")
if fr:
lbl.add_theme_font_override("font", fr)
func _make_progress_bar_background() -> StyleBoxFlat:
var bg = StyleBoxFlat.new()
bg.bg_color = Color(0.2, 0.2, 0.2, 0.8)
bg.border_color = Color(0.4, 0.4, 0.4)
bg.set_border_width_all(1)
return bg
func _create_bar_row(p_name: String, p_label_text: String) -> Dictionary:
var row = HBoxContainer.new()
row.name = p_name
row.add_theme_constant_override("separation", 4)
var left = Label.new()
left.text = p_label_text
left.custom_minimum_size.x = _BAR_LABEL_MIN_WIDTH
_style_bar_font(left)
var spacer = Control.new()
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var value_lbl = Label.new()
value_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
value_lbl.custom_minimum_size.x = _BAR_VALUE_MIN_WIDTH
_style_bar_font(value_lbl)
var bar = ProgressBar.new()
bar.custom_minimum_size = Vector2(_BAR_WIDTH, 12)
bar.show_percentage = false
bar.add_theme_stylebox_override("background", _make_progress_bar_background())
row.add_child(left)
row.add_child(spacer)
row.add_child(value_lbl)
row.add_child(bar)
stats_panel.add_child(row)
return {"container": row, "label": left, "value_label": value_lbl, "progress_bar": bar}
func _create_hp_ui():
if not stats_panel:
return
var d = _create_bar_row("HPContainer", "HP:")
hp_container = d.container
hp_label = d.label
hp_value_label = d.value_label
hp_progress_bar = d.progress_bar
var fill = StyleBoxFlat.new()
fill.bg_color = Color(0.85, 0.2, 0.2)
hp_progress_bar.add_theme_stylebox_override("fill", fill)
# Create container for weight UI
weight_container = HBoxContainer.new()
weight_container.name = "WeightContainer"
weight_container.add_theme_constant_override("separation", 4)
func _create_mp_ui():
if not stats_panel:
return
var d = _create_bar_row("MPContainer", "MP:")
mp_container = d.container
mp_label = d.label
mp_value_label = d.value_label
mp_progress_bar = d.progress_bar
var fill = StyleBoxFlat.new()
fill.bg_color = Color(0.25, 0.45, 0.9)
mp_progress_bar.add_theme_stylebox_override("fill", fill)
# Create label
weight_label = Label.new()
weight_label.text = "Weight:"
weight_label.add_theme_font_size_override("font_size", 10)
if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
var font_resource = load("res://assets/fonts/standard_font.png")
if font_resource:
weight_label.add_theme_font_override("font", font_resource)
weight_container.add_child(weight_label)
func _create_weight_ui():
if not stats_panel:
return
var d = _create_bar_row("WeightContainer", "Weight:")
weight_container = d.container
weight_label = d.label
weight_value_label = d.value_label
weight_progress_bar = d.progress_bar
var fill = StyleBoxFlat.new()
fill.bg_color = Color(0.6, 0.8, 0.3)
weight_progress_bar.add_theme_stylebox_override("fill", fill)
# Create progress bar
weight_progress_bar = ProgressBar.new()
weight_progress_bar.custom_minimum_size = Vector2(100, 12)
weight_progress_bar.show_percentage = false
# Style the progress bar
var progress_style = StyleBoxFlat.new()
progress_style.bg_color = Color(0.2, 0.2, 0.2, 0.8)
progress_style.border_color = Color(0.4, 0.4, 0.4)
progress_style.set_border_width_all(1)
weight_progress_bar.add_theme_stylebox_override("background", progress_style)
func _create_exp_ui():
if not stats_panel:
return
var d = _create_bar_row("ExpContainer", "Exp:")
exp_container = d.container
exp_label = d.label
exp_value_label = d.value_label
exp_progress_bar = d.progress_bar
var fill = StyleBoxFlat.new()
fill.bg_color = Color(0.55, 0.35, 0.95)
exp_progress_bar.add_theme_stylebox_override("fill", fill)
var fill_style = StyleBoxFlat.new()
fill_style.bg_color = Color(0.6, 0.8, 0.3) # Green color
weight_progress_bar.add_theme_stylebox_override("fill", fill_style)
weight_container.add_child(weight_progress_bar)
# Add to stats panel (after stats labels)
stats_panel.add_child(weight_container)
func _create_coin_ui():
if not stats_panel:
return
coin_container = HBoxContainer.new()
coin_container.name = "CoinContainer"
coin_container.add_theme_constant_override("separation", 4)
coin_label = Label.new()
coin_label.name = "CoinLabel"
coin_label.text = "Coin:"
coin_label.custom_minimum_size.x = _BAR_LABEL_MIN_WIDTH
_style_bar_font(coin_label)
coin_container.add_child(coin_label)
var coin_spacer = Control.new()
coin_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
coin_container.add_child(coin_spacer)
var coin_wrap = Control.new()
coin_wrap.custom_minimum_size = Vector2(16, 16)
coin_sprite = Sprite2D.new()
coin_sprite.name = "CoinSprite"
var tex = load("res://assets/gfx/pickups/gold_coin.png") as Texture2D
if tex:
coin_sprite.texture = tex
coin_sprite.hframes = 6
coin_sprite.vframes = 1
coin_sprite.frame = 0
coin_sprite.centered = false
# Scale down to fit; texture may be multi-frame
var tw = tex.get_width() / 6.0
var th = tex.get_height()
if tw > 0 and th > 0:
var s = min(16.0 / tw, 16.0 / th)
coin_sprite.scale = Vector2(s, s)
coin_wrap.add_child(coin_sprite)
coin_container.add_child(coin_wrap)
coin_value_label = Label.new()
coin_value_label.name = "CoinValueLabel"
coin_value_label.text = "X 0"
coin_value_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
_style_bar_font(coin_value_label)
coin_container.add_child(coin_value_label)
stats_panel.add_child(coin_container)
func _has_equipment_in_slot(slot_name: String) -> bool:
# Check if there's an item equipped in this slot
@@ -338,11 +492,11 @@ func _on_equipment_slot_pressed(slot_name: String):
if is_updating_ui:
return
# Only select if there's an item equipped
# Only select if there's an item equipped (same as arrow-key navigation)
if not _has_equipment_in_slot(slot_name):
return
# Select this slot
# Select this slot (equivalent to arrow-key selecting this equipment)
selected_slot = slot_name
selected_item = local_player.character_stats.equipment[slot_name]
selected_type = "equipment" if selected_item else ""
@@ -353,6 +507,7 @@ func _on_equipment_slot_pressed(slot_name: String):
_update_selection_highlight()
_update_selection_rectangle()
_update_info_panel()
func _on_equipment_slot_gui_input(event: InputEvent, slot_name: String):
# Handle double-click to unequip
@@ -453,6 +608,11 @@ func _process(delta):
if stylebox:
stylebox.border_color = animated_color
# Animate 6-frame coin sprite
if coin_sprite and coin_sprite.hframes >= 6:
coin_anim_time += delta * 10.0
coin_sprite.frame = int(coin_anim_time) % 6
func _update_ui():
if not local_player or not local_player.character_stats:
return

View File

@@ -42,6 +42,9 @@ var grab_released_while_lifting = false # Track if grab was released while lifti
var grab_start_time = 0.0 # Time when we first grabbed (to detect quick tap)
var grab_tap_threshold = 0.2 # Seconds to distinguish tap from hold
var is_shielding: bool = false # True when holding grab with shield in offhand and nothing to grab/lift
var is_reviving: bool = false # True when holding grab on a corpse and charging revive
var revive_charge: float = 0.0
const REVIVE_DURATION: float = 2.0 # Seconds holding grab to revive
var last_movement_direction = Vector2.DOWN # Track last direction for placing objects
var push_axis = Vector2.ZERO # Locked axis for pushing/pulling
var push_direction_locked: int = Direction.DOWN # Locked facing direction when pushing
@@ -160,6 +163,7 @@ var current_health: float:
character_stats.hp = value
var is_dead: bool = false
var is_processing_death: bool = false # Prevent multiple death sequences
var was_revived: bool = false # Set by reviver; aborts _die() wait-for-all-dead
var respawn_point: Vector2 = Vector2.ZERO
var coins: int:
get:
@@ -299,7 +303,7 @@ const ANIMATIONS = {
"nextAnimation": null
},
"RUN_PULL": {
"frames": [32, 32, 32, 33],
"frames": [33, 32, 33, 34],
"frameDurations": [260, 260, 260, 260],
"loop": true,
"nextAnimation": null
@@ -2255,8 +2259,8 @@ func _handle_input():
if character_stats and character_stats.is_over_encumbered():
current_speed = base_speed * 0.25
# Lock movement if movement_lock_timer is active
if movement_lock_timer > 0.0:
# Lock movement if movement_lock_timer is active or reviving a corpse
if movement_lock_timer > 0.0 or is_reviving:
velocity = Vector2.ZERO
else:
velocity = input_vector * current_speed
@@ -2614,6 +2618,10 @@ func _handle_interactions():
# 1. We just grabbed this frame (prevents immediate release bug) - THIS IS THE MOST IMPORTANT CHECK
# 2. Button is still down (shouldn't happen, but safety check)
# 3. grab_just_pressed is also true (same frame tap)
if grab_just_released:
is_reviving = false
revive_charge = 0.0
if grab_just_released and held_object:
# For bombs that are already lifted, skip the "just grabbed" logic
# and go straight to the normal release handling (drop-on-second-press)
@@ -2675,7 +2683,23 @@ func _handle_interactions():
# Update object position based on mode (only if button is still held)
if held_object and grab_button_down:
if is_lifting:
_update_lifted_object()
var holding_dead_player = _is_player(held_object) and "is_dead" in held_object and held_object.is_dead
var reviver_hp = character_stats.hp if character_stats else 1.0
if holding_dead_player and reviver_hp > 1.0:
is_reviving = true
revive_charge += get_process_delta_time()
if revive_charge >= REVIVE_DURATION:
_do_revive(held_object)
_place_down_object()
is_reviving = false
revive_charge = 0.0
else:
_update_lifted_object()
else:
if holding_dead_player:
is_reviving = false
revive_charge = 0.0
_update_lifted_object()
# Clear the "released while lifting" flag if button is held again
if grab_released_while_lifting:
grab_released_while_lifting = false
@@ -4007,28 +4031,53 @@ func _cast_heal_spell(target: Node):
return
if not character_stats:
return
var gw = get_tree().get_first_node_in_group("game_world")
var dungeon_seed: int = 0
if gw and "dungeon_seed" in gw:
dungeon_seed = gw.dungeon_seed
var seed_val = dungeon_seed + hash(target.name) + int(global_position.x) * 31 + int(global_position.y) * 17 + int(Time.get_ticks_msec() / 50)
var rng = RandomNumberGenerator.new()
rng.seed = seed_val
var int_val = character_stats.baseStats.int + character_stats.get_pass("int")
var base_heal = 10.0
var amount = base_heal + int_val * 0.5
var lck_val = character_stats.baseStats.lck + character_stats.get_pass("lck")
var crit_chance_pct = (character_stats.baseStats.lck + character_stats.get_pass("lck")) * 1.2
var base_heal = 10.0 + int_val * 0.5
var variance = 0.2
var amount = base_heal * (1.0 + (rng.randf() * 2.0 - 1.0) * variance)
amount = max(1.0, floor(amount))
var cap = 0.0
if target.character_stats:
cap = target.character_stats.maxhp - target.character_stats.hp
amount = min(amount, max(0.0, cap))
if amount <= 0:
return
var is_crit = rng.randf() * 100.0 < crit_chance_pct
if is_crit:
amount = floor(amount * 2.0)
var overheal_chance_pct = 1.0 + lck_val * 0.3
var can_overheal = target.character_stats and target.character_stats.hp <= target.character_stats.maxhp
var is_overheal = can_overheal and rng.randf() * 100.0 < overheal_chance_pct
var display_amount = int(amount)
var actual_heal = amount
var allow_overheal = false
if is_overheal:
allow_overheal = true
else:
var cap = 0.0
if target.character_stats:
cap = target.character_stats.maxhp - target.character_stats.hp
actual_heal = min(amount, max(0.0, cap))
var me = multiplayer.get_unique_id()
var tid = target.get_multiplayer_authority()
if me == tid:
target.heal(amount)
_spawn_heal_effect_and_text(target, amount)
if me == tid and actual_heal > 0:
target.heal(actual_heal, allow_overheal)
_spawn_heal_effect_and_text(target, display_amount, is_crit, is_overheal)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
var gw = get_tree().get_first_node_in_group("game_world")
if gw and gw.has_method("_sync_heal_spell"):
_rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, amount])
print(name, " cast heal on ", target.name, " for ", int(amount), " HP")
if gw and gw.has_method("_apply_heal_spell_sync"):
_rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, actual_heal, display_amount, is_crit, is_overheal, allow_overheal])
print(name, " cast heal on ", target.name, " for ", display_amount, " HP", " (actual: ", actual_heal, ", crit: ", is_crit, ", overheal: ", is_overheal, ")")
func _spawn_heal_effect_and_text(target: Node, amount: float):
func _spawn_heal_effect_and_text(target: Node, display_amount: int, is_crit: bool, is_overheal: bool):
if not target or not is_instance_valid(target):
return
var game_world = get_tree().get_first_node_in_group("game_world")
@@ -4042,20 +4091,28 @@ func _spawn_heal_effect_and_text(target: Node, amount: float):
eff.global_position = target.global_position
if eff.has_method("setup"):
eff.setup(target)
var prefix = ""
if is_crit and is_overheal:
prefix = "CRIT OVERHEAL! "
elif is_crit:
prefix = "CRIT! "
elif is_overheal:
prefix = "OVERHEAL! "
var heal_text = prefix + "+" + str(display_amount) + " HP"
var floating_text_scene = preload("res://scenes/floating_text.tscn")
if floating_text_scene:
var ft = floating_text_scene.instantiate()
parent.add_child(ft)
ft.global_position = Vector2(target.global_position.x, target.global_position.y - 20)
ft.setup("+" + str(int(amount)) + " HP", Color.GREEN, 0.5, 0.5, null, 1, 1, 0)
ft.setup(heal_text, Color.GREEN, 0.5, 0.5, null, 1, 1, 0)
@rpc("any_peer", "reliable")
func _sync_heal_spell_via_gw(target_name: String, amount: float):
func _sync_heal_spell_via_gw(target_name: String, amount_to_apply: float, display_amount: int, is_crit: bool, is_overheal: bool, allow_overheal: bool):
if is_multiplayer_authority():
return
var gw = get_tree().get_first_node_in_group("game_world")
if gw and gw.has_method("_apply_heal_spell_sync"):
gw._apply_heal_spell_sync(target_name, amount)
gw._apply_heal_spell_sync(target_name, amount_to_apply, display_amount, is_crit, is_overheal, allow_overheal)
func _is_healing_spell() -> bool:
if not character_stats or not character_stats.equipment.has("offhand"):
@@ -5737,6 +5794,11 @@ func _die():
print(name, " died!")
# Show concussion status effect above head
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
if status_anim and status_anim.has_animation("concussion"):
status_anim.play("concussion")
# Play death sound effect
if sfx_die:
for i in 12:
@@ -5808,14 +5870,29 @@ func _die():
being_held_by = null
# Wait 0.5 seconds after fade before respawning
# If another player is alive, lie dead until ALL players are dead (or we get revived)
while not _are_all_players_dead():
await get_tree().create_timer(0.2).timeout
if was_revived:
return
# Brief delay after last death before respawning
await get_tree().create_timer(0.5).timeout
if was_revived:
return
# Respawn (this will reset is_processing_death)
_respawn()
func _are_all_players_dead() -> bool:
for p in get_tree().get_nodes_in_group("player"):
if "is_dead" in p and not p.is_dead:
return false
return true
func _respawn():
print(name, " respawning!")
was_revived = false
# being_held_by already cleared in _die() before this
# Holder already dropped us 0.2 seconds ago
@@ -5846,6 +5923,10 @@ func _respawn():
if sprite_layer:
sprite_layer.modulate.a = 1.0
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
if status_anim and status_anim.has_animation("idle"):
status_anim.play("idle")
# Get respawn position - use spawn room (start room) for respawning
var new_respawn_pos = respawn_point
var game_world = get_tree().get_first_node_in_group("game_world")
@@ -5958,6 +6039,42 @@ func _force_holder_to_drop_local(holder_name: String):
else:
print(" ✗ Holder not found or invalid")
func _do_revive(corpse: Node):
if not _is_player(corpse) or not "is_dead" in corpse or not corpse.is_dead:
return
var reviver_hp = character_stats.hp if character_stats else 1.0
if reviver_hp <= 1.0:
return
var half_hp = max(1, int(reviver_hp * 0.5))
if character_stats:
character_stats.hp = max(1, character_stats.hp - half_hp)
character_stats.character_changed.emit(character_stats)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
corpse._revive_from_player.rpc_id(corpse.get_multiplayer_authority(), half_hp)
else:
corpse._revive_from_player(half_hp)
@rpc("any_peer", "reliable")
func _revive_from_player(hp_amount: int):
if not is_dead:
return
was_revived = true
is_dead = false
is_processing_death = false
if character_stats:
character_stats.hp = float(hp_amount)
else:
current_health = float(hp_amount)
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
if sprite_layer:
sprite_layer.modulate.a = 1.0
var status_anim = get_node_or_null("Sprite2DStatus/AnimationPlayerStatus")
if status_anim and status_anim.has_animation("idle"):
status_anim.play("idle")
_set_animation("IDLE")
@rpc("any_peer", "reliable")
func _sync_death():
if not is_multiplayer_authority():
@@ -6258,16 +6375,17 @@ func _apply_inventory_and_equipment_from_server(inventory_data: Array, equipment
character_stats.character_changed.emit(character_stats)
print(name, " inventory+equipment applied from server: ", character_stats.inventory.size(), " items")
func heal(amount: float):
func heal(amount: float, allow_overheal: bool = false):
if is_dead:
return
if character_stats:
character_stats.heal(amount)
character_stats.heal(amount, allow_overheal)
print(name, " healed for ", amount, " HP! Health: ", character_stats.hp, "/", character_stats.maxhp)
else:
# Fallback for legacy
current_health = min(current_health + amount, max_health)
var new_hp = current_health + amount
current_health = max(0.0, new_hp) if allow_overheal else clamp(new_hp, 0.0, max_health)
print(name, " healed for ", amount, " HP! Health: ", current_health, "/", max_health)
func add_key(amount: int = 1):

View File

@@ -39,6 +39,11 @@ void fragment() {
COLOR = col * tint;
}
void light() {
// Called for every pixel for every light affecting the CanvasItem.
float cNdotL = max(1.0, dot(NORMAL, LIGHT_DIRECTION));
LIGHT = vec4(LIGHT_COLOR.rgb * COLOR.rgb * LIGHT_ENERGY * cNdotL, LIGHT_COLOR.a);
}

View File

@@ -0,0 +1,105 @@
shader_type canvas_item;
uniform vec4 original_0: source_color;
uniform vec4 original_1: source_color;
uniform vec4 original_2: source_color;
uniform vec4 original_3: source_color;
uniform vec4 original_4: source_color;
uniform vec4 original_5: source_color;
uniform vec4 original_6: source_color;
uniform vec4 original_7: source_color;
uniform vec4 original_8: source_color;
uniform vec4 original_9: source_color;
uniform vec4 original_10: source_color;
uniform vec4 original_11: source_color;
uniform vec4 original_12: source_color;
uniform vec4 original_13: source_color;
uniform vec4 replace_0: source_color;
uniform vec4 replace_1: source_color;
uniform vec4 replace_2: source_color;
uniform vec4 replace_3: source_color;
uniform vec4 replace_4: source_color;
uniform vec4 replace_5: source_color;
uniform vec4 replace_6: source_color;
uniform vec4 replace_7: source_color;
uniform vec4 replace_8: source_color;
uniform vec4 replace_9: source_color;
uniform vec4 replace_10: source_color;
uniform vec4 replace_11: source_color;
uniform vec4 replace_12: source_color;
uniform vec4 replace_13: source_color;
uniform vec4 tint: source_color = vec4(1.0);
uniform vec4 ambient: source_color = vec4(1.0, 1.0, 1.0, 1.0);
const float precision = 0.1;
const int Colz = 14;
vec4 swap_color(vec4 color){
vec4 original_colors[Colz] = vec4[Colz] (
original_0,
original_1,
original_2,
original_3,
original_4,
original_5,
original_6,
original_7,
original_8,
original_9,
original_10,
original_11,
original_12,
original_13
);
vec4 replace_colors[Colz] = vec4[Colz] (
replace_0,
replace_1,
replace_2,
replace_3,
replace_4,
replace_5,
replace_6,
replace_7,
replace_8,
replace_9,
replace_10,
replace_11,
replace_12,
replace_13
);
for (int i = 0; i < Colz; i ++) {
if (distance(color, original_colors[i]) <= precision){
return replace_colors[i];
}
}
return color;
}
void fragment() {
vec4 col = swap_color(texture(TEXTURE, UV));
COLOR = col * tint * ambient;
}
void light() {
// Called for every pixel for every light affecting the CanvasItem.
float cNdotL = max(1.0, dot(NORMAL, LIGHT_DIRECTION));
LIGHT = vec4(LIGHT_COLOR.rgb * COLOR.rgb * LIGHT_ENERGY * cNdotL, LIGHT_COLOR.a);
}

View File

@@ -0,0 +1 @@
uid://dob36l1rwi2en