lots of changeru
This commit is contained in:
BIN
src/assets/gfx/enemies/hand_monster.png
Normal file
BIN
src/assets/gfx/enemies/hand_monster.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
40
src/assets/gfx/enemies/hand_monster.png.import
Normal file
40
src/assets/gfx/enemies/hand_monster.png.import
Normal 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
|
||||
@@ -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]
|
||||
|
||||
@@ -366,6 +366,12 @@ func _deal_explosion_damage():
|
||||
body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false, false)
|
||||
|
||||
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")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
extends Node2D
|
||||
|
||||
# Frostspike spell — instant damage, no debuff. Frames 4413–4416, 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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
@@ -296,6 +302,8 @@ func _process(delta: float) -> void:
|
||||
position = closed_position
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
# Create container for weight UI
|
||||
weight_container = HBoxContainer.new()
|
||||
weight_container.name = "WeightContainer"
|
||||
weight_container.add_theme_constant_override("separation", 4)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
@@ -452,6 +607,11 @@ func _process(delta):
|
||||
var stylebox = selected_button.get_meta("highlight_stylebox") as StyleBoxFlat
|
||||
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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
105
src/shaders/game_world.gdshader
Normal file
105
src/shaders/game_world.gdshader
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1
src/shaders/game_world.gdshader.uid
Normal file
1
src/shaders/game_world.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dob36l1rwi2en
|
||||
Reference in New Issue
Block a user