fixed a bit better lightning

This commit is contained in:
2026-02-08 02:53:03 +01:00
parent e167451e03
commit 9e2516a5ab
11 changed files with 330 additions and 51 deletions

View File

@@ -5,6 +5,11 @@ resource_name = "Reverb"
room_size = 0.51
wet = 0.28
[sub_resource type="AudioEffectLowPassFilter" id="AudioEffectLowPassFilter_j3pel"]
resource_name = "LowPassFilter"
cutoff_hz = 958.0
resonance = 0.75
[resource]
bus/1/name = &"Sfx"
bus/1/solo = false
@@ -14,3 +19,11 @@ bus/1/volume_db = 0.0
bus/1/send = &"Master"
bus/1/effect/0/effect = SubResource("AudioEffectReverb_j3pel")
bus/1/effect/0/enabled = true
bus/2/name = &"SfxFiltered"
bus/2/solo = false
bus/2/mute = false
bus/2/bypass_fx = false
bus/2/volume_db = 0.0
bus/2/send = &"Sfx"
bus/2/effect/0/effect = SubResource("AudioEffectLowPassFilter_j3pel")
bus/2/effect/0/enabled = true

View File

@@ -110,6 +110,23 @@ theme = SubResource("Theme_standard_font")
text = "00:00"
horizontal_alignment = 1
[node name="VBoxContainerDarknessDebug" type="VBoxContainer" parent="UpperLeft/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="LabelDarknessDebugTitle" type="Label" parent="UpperLeft/HBoxContainer/VBoxContainerDarknessDebug"]
layout_mode = 2
theme = SubResource("Theme_standard_font")
text = "Darkness"
horizontal_alignment = 1
[node name="LabelDarknessDebug" type="Label" parent="UpperLeft/HBoxContainer/VBoxContainerDarknessDebug"]
layout_mode = 2
theme = SubResource("Theme_standard_font")
text = ""
horizontal_alignment = 1
autowrap_mode = 2
[node name="UpperRight" type="MarginContainer" parent="." unique_id=1261821969]
anchors_preset = 1
anchor_left = 1.0

View File

@@ -32,10 +32,19 @@ var blink_start_time: float = 1.0 # Start blinking 1 second before explosion
var can_be_collected: bool = false
var collection_delay: float = 0.2 # Can be collected after 0.2 seconds
# Fallout (landed in quicksand): sink and explode with no visual/damage, but sound + screenshake
# Fallout (landed in quicksand): stop momentum, glide to tile center, sink in place (like player)
var fell_in_fallout: bool = false
var fallout_sink_progress: float = 1.0
const FALLOUT_SINK_DURATION: float = 0.5
const FALLOUT_CENTER_THRESHOLD: float = 2.0
const FALLOUT_GLIDE_SPEED: float = 220.0 # px/s toward tile center when on fallout
const FALLOUT_TILE_HALF_SIZE: float = 8.0
# Bus for fuse/explosion when in fallout (SfxFiltered if it exists, else Sfx so sound still plays)
static func _fallout_sfx_bus() -> String:
if AudioServer.get_bus_index("SfxFiltered") >= 0:
return "SfxFiltered"
return "Sfx"
@onready var sprite = $Sprite2D
@onready var explosion_sprite = $ExplosionSprite
@@ -137,8 +146,11 @@ func _start_fuse():
fuse_timer = 0.0
# Play fuse sound
if has_node("SfxFuse"):
$SfxFuse.play()
var sfx_fuse = get_node_or_null("SfxFuse")
if sfx_fuse:
if fell_in_fallout:
sfx_fuse.bus = _fallout_sfx_bus()
sfx_fuse.play()
# Start fuse particles
if fuse_particles:
@@ -239,8 +251,36 @@ func _physics_process(delta):
shadow.scale = Vector2.ONE
shadow.modulate = Color(0, 0, 0, 0.5)
# Apply friction if on ground
if not is_airborne:
# On ground: check for fallout tile (stop momentum, glide to center, sink in place like player)
var gw = get_tree().get_first_node_in_group("game_world")
if gw and gw.has_method("_is_position_on_fallout_tile") and gw._is_position_on_fallout_tile(global_position):
# Stop all momentum and lock to fallout behavior
throw_velocity = Vector2.ZERO
velocity = Vector2.ZERO
if gw.has_method("_get_tile_center_at"):
var tile_center = gw._get_tile_center_at(global_position)
var dist_to_center = global_position.distance_to(tile_center)
if dist_to_center < FALLOUT_CENTER_THRESHOLD:
# Close enough: snap to center and start sinking
global_position = tile_center
fell_in_fallout = true
fallout_sink_progress = 1.0
can_be_collected = false
if collection_area:
collection_area.set_deferred("monitoring", false)
# Route fuse sound to fallout bus if already playing (e.g. rolled onto fallout)
var sfx_fuse = get_node_or_null("SfxFuse")
if sfx_fuse and sfx_fuse.playing:
sfx_fuse.bus = _fallout_sfx_bus()
else:
# Glide toward tile center (like player on quicksand)
var dir = (tile_center - global_position).normalized()
var edge_t = clamp(dist_to_center / FALLOUT_TILE_HALF_SIZE, 0.0, 1.0)
var edge_drag_factor = lerp(1.0, 0.45, edge_t)
var strength_mult = 1.0 + 0.8 * (1.0 - clamp(dist_to_center / FALLOUT_TILE_HALF_SIZE, 0.0, 1.0))
velocity = dir * FALLOUT_GLIDE_SPEED * strength_mult * edge_drag_factor
else:
# Normal ground: apply friction
throw_velocity = throw_velocity.lerp(Vector2.ZERO, delta * 5.0)
if throw_velocity.length() < 5.0:
throw_velocity = Vector2.ZERO
@@ -272,10 +312,14 @@ func _land():
is_airborne = false
position_z = 0.0
velocity_z = 0.0
velocity = Vector2.ZERO
throw_velocity = Vector2.ZERO
# If landed on fallout tile: sink and will explode with no visual/damage (sound + screenshake only)
# If landed on fallout tile: lock to tile center and sink in place (no visual/damage, sound + screenshake only)
var gw = get_tree().get_first_node_in_group("game_world")
if gw and gw.has_method("_is_position_on_fallout_tile") and gw._is_position_on_fallout_tile(global_position):
if gw.has_method("_get_tile_center_at"):
global_position = gw._get_tile_center_at(global_position)
fell_in_fallout = true
fallout_sink_progress = 1.0
can_be_collected = false
@@ -300,10 +344,17 @@ func _explode():
if fuse_light:
fuse_light.enabled = false
# Fell in fallout: no explosion visual, no damage, but sound + screenshake
# Fell in fallout: no explosion visual, no damage, but sound (filtered) + screenshake
if fell_in_fallout:
if has_node("SfxExplosion"):
$SfxExplosion.play()
var sfx_explosion = get_node_or_null("SfxExplosion")
if sfx_explosion:
sfx_explosion.bus = _fallout_sfx_bus()
# Reparent so sound keeps playing after bomb is freed (otherwise queue_free cuts it off)
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
sfx_explosion.reparent(game_world, true)
sfx_explosion.finished.connect(sfx_explosion.queue_free)
sfx_explosion.play()
_cause_screenshake()
if bomb_area:
bomb_area.set_deferred("monitoring", false)

View File

@@ -201,9 +201,10 @@ var spell_amp: float:
get:
return (baseStats.int + get_pass("int")) * 0.5
# Fixed value; movement speed is not scaled by DEX (player uses its own move_speed)
var move_speed: float:
get:
return 2.0 + ((baseStats.dex + get_pass("dex")) * 0.01)
return 1.0
var attack_speed: float:
get:

View File

@@ -7,13 +7,13 @@ extends Node2D
# "enemy" -> frames 2123-2135, red light (e.g. enemy_hand)
const FRAME_RATE: float = 8.0 # frames per second
const LIFETIME: float = 30.0
const LIFETIME: float = 30.0 # Default (chest, enemy); trap/cracked use EFFECT_CONFIG lifetime
const FADE_DURATION: float = 2.0
# Effect type -> { frames: Array, light_color: Color, optional light_energy: float }
# Effect type -> { frames: Array, light_color: Color, optional light_energy: float, optional lifetime: float }
const EFFECT_CONFIG: Dictionary = {
"chest": {"frames": [169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179], "light_color": Color(0.35, 0.5, 0.95, 1)},
"trap": {"frames": [274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284], "light_color": Color(0.7, 0.35, 0.95, 1)},
"trap": {"frames": [274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284], "light_color": Color(0.7, 0.35, 0.95, 1), "lifetime": 5.0},
"enemy": {"frames": [2123, 2124, 2125, 2126, 2127, 2128, 2129, 2130, 2131, 2132, 2133, 2134, 2135], "light_color": Color(1.0, 0.1, 0.1, 1), "light_energy": 1.4}
}
@@ -22,6 +22,7 @@ var _elapsed: float = 0.0
var _fading: bool = false
var _fade_elapsed: float = 0.0
var _initial_light_energy: float = 0.9
var _lifetime: float = LIFETIME
@onready var fx_sprite: Sprite2D = $FxSprite
@onready var detect_light: PointLight2D = $DetectLight
@@ -34,12 +35,14 @@ func setup(world_pos: Vector2, effect_type: String = "chest") -> void:
if EFFECT_CONFIG.has(effect_type):
var cfg = EFFECT_CONFIG[effect_type]
_frames = cfg.frames
_lifetime = cfg.get("lifetime", LIFETIME)
if detect_light:
detect_light.color = cfg.light_color
if cfg.get("light_energy", 0.0) > 0.0:
detect_light.energy = cfg.light_energy
else:
_frames = EFFECT_CONFIG.chest.frames
_lifetime = LIFETIME
if fx_sprite and _frames.size() > 0:
fx_sprite.frame = _frames[0]
if detect_light:
@@ -63,6 +66,6 @@ func _process(delta: float) -> void:
if fx_sprite and _frames.size() > 0:
var frame_idx = int(_elapsed * FRAME_RATE) % _frames.size()
fx_sprite.frame = _frames[frame_idx]
if _elapsed >= LIFETIME:
if _elapsed >= _lifetime:
_fading = true
_fade_elapsed = 0.0

View File

@@ -763,8 +763,7 @@ func _load_random_headgear():
"Basic Melee": [
"DarkKnightHelm.png", "DragonKnightHelm.png", "GruntHelm.png", "KnightHelm.png",
"NoviceHelm.png", "PaladinHelmCyan.png", "ScoutHelmGreen.png",
"SoldierBronzeHelmBlue.png", "SoldierBronzeHelmRed.png", "SoldierGoldHelmBlue.png",
"SoldierIronHelmBlue.png", "SoldierSteelHelmBlue.png"
"SoldierBronzeHelmBlue.png", "SoldierIronHelmBlue.png", "SoldierSteelHelmBlue.png"
],
"Basic Range": [
"ArcherHatCyan.png", "HunterHatRed.png", "RogueHatGreen.png"

View File

@@ -43,6 +43,7 @@ const FOG_RAY_STEP: float = 0.5
const FOG_RAY_ANGLE_STEP: int = 10
const FOG_UPDATE_INTERVAL: float = 0.25 # Run less often to avoid spikes (was 0.1)
const FOG_UPDATE_INTERVAL_CORRIDOR: float = 0.06 # Update often in corridors so vision feels correct
const FOG_PLAYER_VICINITY_RADIUS: int = 4 # Tiles around player always visible (never dull)
const FOG_DEBUG_DRAW: bool = false
const FOG_SIMPLE_MODE: bool = true # Whole room / corridor+rooms visible (no raycast)
var fog_update_timer: float = 0.0
@@ -85,14 +86,18 @@ var _torch_darken_last_room_id: String = ""
var _torch_darken_target_scale: float = 1.0
var _torch_darken_current_scale: float = 1.0
const _TORCH_DARKEN_LERP_SPEED: float = 4.0
const _TORCH_DARKEN_MIN_SCALE: float = 0.05 # Floor brightness so it's never insanely dark; same for all players
const _TORCH_DARKEN_MIN_SCALE: float = 0.18 # Floor brightness so 0-torch rooms are dark but not broken; same for all players
const _TORCH_DARKEN_DEFAULT_RAW: float = 0.6 # When corridor has no connected rooms, use this (avoid stuck at min brightness)
var _synced_darkness_scale: float = 1.0 # Server syncs this to clients so host and joiner see same darkness
var _last_synced_darkness_sent: float = -1.0 # Server: last value we sent
var _darkness_sync_timer: float = 0.0 # Server: throttle sync RPCs
var _last_darkness_debug_scale: float = -1.0 # For debug: only print when brightness changes
var seen_by_player: Dictionary = {} # player_name -> PackedInt32Array (0 unseen, 1 seen)
var combined_seen: PackedInt32Array = PackedInt32Array()
var explored_map: PackedInt32Array = PackedInt32Array() # 0 unseen, 1 explored
var fog_debug_lines: Array = []
# When true, fog update only marks current room (or corridor tiles), not all corridor-connected rooms (used on level load)
var _restrict_fog_to_current_room_only: bool = false
# Dungeon generation
var dungeon_data: Dictionary = {}
@@ -131,7 +136,7 @@ var _fallout_cache_map_size: Vector2i = Vector2i.ZERO
# Cracked floor: stand too long -> tile breaks and becomes fallout
var cracked_stand_timers: Dictionary = {} # "player_key|tx|ty" -> float (seconds on that tile)
const CRACKED_STAND_DURATION: float = 0.9 # Seconds standing on cracked tile before it breaks
const CRACKED_STAND_DURATION: float = 0.2 # Seconds standing on cracked tile before it breaks
const CRACKED_TILE_ATLAS: Vector2i = Vector2i(15, 16)
# Cracked floor: normally invisible; once per game per tile a player can roll perception when close to reveal
const CRACKED_DETECTION_RADIUS: float = 99.0 # Same as trap detection (pixels)
@@ -3277,6 +3282,68 @@ func _init_fog_of_war():
_synced_darkness_scale = 1.0
_last_synced_darkness_sent = -1.0
_darkness_sync_timer = 0.0
_last_darkness_debug_scale = -1.0
# Clear minimap again for new level so no level-1 room stays visible (in case clear in _clear_level ran before node was ready)
var minimap_draw = get_node_or_null("Minimap/MarginContainer/MinimapView/MinimapDraw")
if minimap_draw and minimap_draw.has_method("clear_for_new_level"):
minimap_draw.clear_for_new_level()
# Apply visibility next frame and again after a short delay (client may get position sync after first frame)
call_deferred("_apply_level_visibility")
get_tree().create_timer(0.2).timeout.connect(_apply_level_visibility)
func _apply_level_visibility() -> void:
if not is_inside_tree():
return
# Run one fog update and reapply torch darkening so new level (e.g. level 2) is visible immediately
if fog_node and is_instance_valid(fog_node) and not dungeon_data.is_empty():
var map_size_apply = dungeon_data.map_size
var total_apply = map_size_apply.x * map_size_apply.y
if combined_seen.size() != total_apply:
combined_seen.resize(total_apply)
if explored_map.size() != total_apply:
explored_map.resize(total_apply)
# Level 2+ with entrance: only reveal start_room + player vicinity. Do NOT run full fog update or corridor BFS,
# otherwise the 24-step BFS from the entrance floods into adjacent rooms and pre-explores them.
var level_load_with_entrance: bool = current_level > 1 and dungeon_data.has("entrance") and not dungeon_data.entrance.is_empty() and dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty()
if level_load_with_entrance:
for i in range(total_apply):
combined_seen[i] = 0
explored_map[i] = 0
var start_room: Dictionary = dungeon_data.start_room
_mark_room_visible(start_room)
_mark_room_explored(start_room)
var local_list = player_manager.get_local_players()
for player in local_list:
if not player or not is_instance_valid(player):
continue
var pt = Vector2i(int(player.global_position.x / FOG_TILE_SIZE), int(player.global_position.y / FOG_TILE_SIZE))
for dx in range(-FOG_PLAYER_VICINITY_RADIUS, FOG_PLAYER_VICINITY_RADIUS + 1):
for dy in range(-FOG_PLAYER_VICINITY_RADIUS, FOG_PLAYER_VICINITY_RADIUS + 1):
var tx = pt.x + dx
var ty = pt.y + dy
if tx >= 0 and ty >= 0 and tx < map_size_apply.x and ty < map_size_apply.y:
var idx = tx + ty * map_size_apply.x
if idx >= 0 and idx < combined_seen.size():
combined_seen[idx] = 1
for i in range(total_apply):
if combined_seen[i] != 0:
explored_map[i] = 1
else:
# Level 1 or no entrance: run normal fog update once
fog_update_timer = FOG_UPDATE_INTERVAL
fog_visual_tick = 1
_restrict_fog_to_current_room_only = true
_update_fog_of_war(0.0)
_restrict_fog_to_current_room_only = false
if current_level > 1 and dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty():
var start_room: Dictionary = dungeon_data.start_room
_mark_room_visible(start_room)
_mark_room_explored(start_room)
# Fog texture: full update both phases so the whole screen isn't half black
if fog_node.has_method("set_maps"):
fog_node.set_maps(explored_map, combined_seen, 0)
fog_node.set_maps(explored_map, combined_seen, 1)
_reapply_torch_darkening()
func _update_fog_of_war(delta: float) -> void:
if not fog_node or dungeon_data.is_empty() or not dungeon_data.has("map_size"):
@@ -3341,10 +3408,27 @@ func _update_fog_of_war(delta: float) -> void:
for idx in range(cached_corridor_mask.size()):
if cached_corridor_mask[idx] == 1:
combined_seen[idx] = 1
for room in cached_corridor_rooms:
_mark_room_in_seen_map(combined_seen, room)
# On level load we only mark corridor tiles, not all connected rooms (avoids minimap pre-showing many rooms)
if not _restrict_fog_to_current_room_only:
for room in cached_corridor_rooms:
_mark_room_in_seen_map(combined_seen, room)
last_corridor_fog_update = Time.get_ticks_msec() / 1000.0
# Guarantee: player's immediate vicinity is always visible (never dull) so "where I am" is never dimmed
var map_size_vis = dungeon_data.map_size
for player in local_player_list:
if not player or not is_instance_valid(player):
continue
var pt = Vector2i(int(player.global_position.x / FOG_TILE_SIZE), int(player.global_position.y / FOG_TILE_SIZE))
for dx in range(-FOG_PLAYER_VICINITY_RADIUS, FOG_PLAYER_VICINITY_RADIUS + 1):
for dy in range(-FOG_PLAYER_VICINITY_RADIUS, FOG_PLAYER_VICINITY_RADIUS + 1):
var tx = pt.x + dx
var ty = pt.y + dy
if tx >= 0 and ty >= 0 and tx < map_size_vis.x and ty < map_size_vis.y:
var idx = tx + ty * map_size_vis.x
if idx >= 0 and idx < combined_seen.size():
combined_seen[idx] = 1
# Merge visible into explored (single pass)
for i in range(total):
if combined_seen[i] != 0:
@@ -3621,6 +3705,18 @@ func _median_torch_scale_from_rooms(rooms: Array) -> float:
func _torch_scale_to_display(raw: float) -> float:
return clampf(_TORCH_DARKEN_MIN_SCALE + (1.0 - _TORCH_DARKEN_MIN_SCALE) * raw, _TORCH_DARKEN_MIN_SCALE, 1.0)
# True if world_pos is inside the entrance area (level > 1). Used so brightness uses start_room when standing in entrance.
func _is_position_in_entrance_area(world_pos: Vector2) -> bool:
if current_level <= 1 or dungeon_data.is_empty() or not dungeon_data.has("entrance") or dungeon_data.entrance.is_empty():
return false
var ed = dungeon_data.entrance
if not ed.has("world_pos") or not ed.has("world_size"):
return false
var c = ed.world_pos
var sz = ed.world_size
var half = Vector2(sz.x * 0.5, sz.y * 0.5)
return world_pos.x >= c.x - half.x and world_pos.x <= c.x + half.x and world_pos.y >= c.y - half.y and world_pos.y <= c.y + half.y
# Compute target darkness scale for a given world position (for sync: server can use any player's position)
func _get_darkness_scale_at_position(world_pos: Vector2) -> float:
var p_tile = Vector2i(int(world_pos.x / FOG_TILE_SIZE), int(world_pos.y / FOG_TILE_SIZE))
@@ -3631,9 +3727,38 @@ func _get_darkness_scale_at_position(world_pos: Vector2) -> float:
var tc = clampi(_count_torches_in_room(current_room), 0, 4)
raw = tc / 4.0
else:
raw = _median_torch_scale_from_rooms(cached_corridor_rooms)
# Level 2+: in entrance use start_room brightness so first room isn't dimmed
if _is_position_in_entrance_area(world_pos) and dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty():
var tc = clampi(_count_torches_in_room(dungeon_data.start_room), 0, 4)
raw = tc / 4.0
else:
raw = _median_torch_scale_from_rooms(cached_corridor_rooms)
if raw <= 0.0:
raw = _TORCH_DARKEN_DEFAULT_RAW
return _torch_scale_to_display(raw)
func _set_darkness_debug(msg: String) -> void:
var hud = get_tree().get_first_node_in_group("ingame_hud")
if hud and hud.has_method("update_darkness_debug"):
hud.update_darkness_debug(msg)
func _sync_ambient_to_canvas_modulate() -> void:
# Tile shader uses "ambient" for darkening; it was set once in _apply_dungeon_color_scheme.
# Keep it in sync with CanvasModulate so brightness=1.0 actually looks bright (tiles + lights).
var cm = get_node_or_null("CanvasModulate")
if not cm or not is_instance_valid(cm):
return
var ambient_color = cm.color
var env_node = get_node_or_null("Environment")
if not env_node:
return
for child in env_node.get_children():
if not child is TileMapLayer or not is_instance_valid(child):
continue
var mat = (child as TileMapLayer).material
if mat is ShaderMaterial:
(mat as ShaderMaterial).set_shader_parameter("ambient", ambient_color)
func _update_canvas_modulate_by_torches() -> void:
if dungeon_data.is_empty() or not dungeon_data.has("torches"):
return
@@ -3647,6 +3772,10 @@ func _update_canvas_modulate_by_torches() -> void:
_torch_darken_current_scale = lerpf(_torch_darken_current_scale, _synced_darkness_scale, clampf(dt * _TORCH_DARKEN_LERP_SPEED, 0.0, 1.0))
var brightness := _torch_darken_current_scale
cm.color = Color(brightness, brightness, brightness)
_sync_ambient_to_canvas_modulate()
if abs(brightness - _last_darkness_debug_scale) > 0.005 or _last_darkness_debug_scale < 0:
_last_darkness_debug_scale = brightness
_set_darkness_debug("CLIENT (synced): brightness=%.3f" % brightness)
return
# Server or single player: compute target scale
var local_list = player_manager.get_local_players() if player_manager else []
@@ -3667,6 +3796,11 @@ func _update_canvas_modulate_by_torches() -> void:
_darkness_sync_timer = 0.0
_last_synced_darkness_sent = target_scale
_sync_darkness_scale.rpc(target_scale)
# Debug: server applies same scale
var s_server := maxf(target_scale, _TORCH_DARKEN_MIN_SCALE)
if abs(s_server - _last_darkness_debug_scale) > 0.005 or _last_darkness_debug_scale < 0:
_last_darkness_debug_scale = s_server
_set_darkness_debug("SERVER: brightness=%.3f (max over %d players)" % [s_server, all_players.size()])
else:
if local_list.is_empty() or not local_list[0]:
return
@@ -3691,7 +3825,10 @@ func _update_canvas_modulate_by_torches() -> void:
var tc = clampi(_count_torches_in_room(current_room), 0, 4)
_torch_darken_target_scale = _torch_scale_to_display(tc / 4.0)
else:
_torch_darken_target_scale = _torch_scale_to_display(_median_torch_scale_from_rooms(cached_corridor_rooms))
var raw = _median_torch_scale_from_rooms(cached_corridor_rooms)
if raw <= 0.0:
raw = _TORCH_DARKEN_DEFAULT_RAW
_torch_darken_target_scale = _torch_scale_to_display(raw)
target_scale = _torch_darken_target_scale
var delta := get_process_delta_time()
if not (multiplayer.has_multiplayer_peer() and multiplayer.is_server()):
@@ -3700,6 +3837,26 @@ func _update_canvas_modulate_by_torches() -> void:
_torch_darken_current_scale = target_scale
var s := maxf(_torch_darken_current_scale, _TORCH_DARKEN_MIN_SCALE)
cm.color = Color(s, s, s)
_sync_ambient_to_canvas_modulate()
# Debug: print when brightness changes (single-player or local view)
if not (multiplayer.has_multiplayer_peer() and multiplayer.is_server()):
if abs(s - _last_darkness_debug_scale) > 0.005 or _last_darkness_debug_scale < 0:
var reason := ""
var p = local_list[0]
var p_tile_d = Vector2i(int(p.global_position.x / FOG_TILE_SIZE), int(p.global_position.y / FOG_TILE_SIZE))
var current_room_d = _find_room_at_tile(p_tile_d)
var in_room_d := not current_room_d.is_empty()
if in_room_d:
var tc = clampi(_count_torches_in_room(current_room_d), 0, 4)
reason = "ROOM tile(%d,%d) torches=%d raw=%.2f" % [p_tile_d.x, p_tile_d.y, tc, tc / 4.0]
else:
var raw_c = _median_torch_scale_from_rooms(cached_corridor_rooms)
var used_default = raw_c <= 0.0
if used_default:
raw_c = _TORCH_DARKEN_DEFAULT_RAW
reason = "CORRIDOR tile(%d,%d) median_raw=%.2f (connected_rooms=%d%s)" % [p_tile_d.x, p_tile_d.y, raw_c, cached_corridor_rooms.size(), " USED_DEFAULT" if used_default else ""]
_last_darkness_debug_scale = s
_set_darkness_debug("brightness=%.3f (target=%.3f)\n%s" % [s, target_scale, reason])
@rpc("authority", "reliable")
func _sync_darkness_scale(darkness_scale: float) -> void:
@@ -3723,7 +3880,14 @@ func _reapply_torch_darkening() -> void:
var tc = clampi(_count_torches_in_room(current_room), 0, 4)
raw = tc / 4.0
else:
raw = _median_torch_scale_from_rooms(cached_corridor_rooms)
# Level 2+: in entrance use start_room so first room brightness is correct
if _is_position_in_entrance_area(p.global_position) and dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty():
var tc = clampi(_count_torches_in_room(dungeon_data.start_room), 0, 4)
raw = tc / 4.0
else:
raw = _median_torch_scale_from_rooms(cached_corridor_rooms)
if raw <= 0.0:
raw = _TORCH_DARKEN_DEFAULT_RAW
var t := _torch_scale_to_display(raw)
_torch_darken_target_scale = t
_torch_darken_current_scale = t
@@ -3734,6 +3898,20 @@ func _reapply_torch_darkening() -> void:
_torch_darken_in_room_last = in_room
_torch_darken_last_room_id = room_id
cm.color = Color(t, t, t)
_sync_ambient_to_canvas_modulate()
if abs(t - _last_darkness_debug_scale) > 0.005 or _last_darkness_debug_scale < 0:
_last_darkness_debug_scale = t
var reason := ""
if in_room:
var tc = clampi(_count_torches_in_room(current_room), 0, 4)
reason = "REAPPLY ROOM tile(%d,%d) torches=%d raw=%.2f" % [p_tile.x, p_tile.y, tc, tc / 4.0]
else:
var raw_r = _median_torch_scale_from_rooms(cached_corridor_rooms)
var used_default = raw_r <= 0.0
if used_default:
raw_r = _TORCH_DARKEN_DEFAULT_RAW
reason = "REAPPLY CORRIDOR tile(%d,%d) raw=%.2f connected_rooms=%d%s" % [p_tile.x, p_tile.y, raw_r, cached_corridor_rooms.size(), " USED_DEFAULT" if used_default else ""]
_set_darkness_debug("%s -> brightness=%.3f" % [reason, t])
func _get_view_angle_weight(view_dir: Vector2, ray_dir: Vector2) -> float:
if view_dir.length() < 0.1 or ray_dir.length() < 0.1:
@@ -3778,7 +3956,7 @@ func _build_corridor_mask(start_tile: Vector2i) -> PackedInt32Array:
queue.append(start_tile)
mask[start_tile.x + start_tile.y * map_size.x] = 1
var max_steps = 10 # Reduced from 24 to prevent corridor branches from reaching far rooms
var max_steps = 10 # Visible corridor stretch; avoid seeing rooms far away
while queue.size() > 0:
var tile = queue.pop_front()
var dist = abs(tile.x - start_tile.x) + abs(tile.y - start_tile.y)
@@ -3811,8 +3989,8 @@ func _get_rooms_connected_to_corridor(corridor_mask: PackedInt32Array, player_ti
if dungeon_data.is_empty() or not dungeon_data.has("rooms") or player_tile.x < 0:
return rooms
var map_size = dungeon_data.map_size
# Only check rooms within a small distance of the player (8 tiles)
var max_room_distance = 8
# Only check rooms within a small distance of the player (4 tiles) so we don't reveal far rooms
var max_room_distance = 4
var room_distances = []
for room in dungeon_data.rooms:
# Calculate room center distance from player - skip if too far
@@ -3860,9 +4038,9 @@ func _get_rooms_connected_to_corridor(corridor_mask: PackedInt32Array, player_ti
if touches_corridor:
room_distances.append({"room": room, "dist": closest_corridor_dist})
# Sort by distance and return only the 2 closest rooms
# Sort by distance and return only the 1 closest room (avoid revealing far rooms)
room_distances.sort_custom(func(a, b): return a.dist < b.dist)
for i in range(min(2, room_distances.size())):
for i in range(min(1, room_distances.size())):
rooms.append(room_distances[i].room)
return rooms
@@ -4402,7 +4580,8 @@ func _render_dungeon():
LogManager.log("GameWorld: Placed " + str(above_tiles_placed) + " tiles on above layer", LogManager.CATEGORY_DUNGEON)
LogManager.log("GameWorld: Dungeon rendered on TileMapLayer", LogManager.CATEGORY_DUNGEON)
_init_fog_of_war()
# _init_fog_of_war() schedules _apply_level_visibility() deferred so level 2 isn't all black
# Create stairs Area2D if stairs data exists
_create_stairs_area()
@@ -7602,6 +7781,8 @@ func _clear_level():
fog_node.queue_free()
fog_node = null
fog_tile_to_room_index.resize(0)
fog_update_timer = 0.0
fog_visual_tick = 0
_cached_closed_door_tiles.clear()
_fallout_tile_cache.resize(0)
_fallout_cache_map_size = Vector2i.ZERO

View File

@@ -26,6 +26,7 @@ var label_disconnected: Label = null
var label_matchbox_status: Label = null
var label_ice_status: Label = null
var label_data_channels_status: Label = null
var label_darkness_debug: Label = null
var game_world: Node = null
var network_manager: Node = null
@@ -37,6 +38,7 @@ var timer_running: bool = true # Flag to stop/start timer
const HUD_BASE_SIZE: Vector2 = Vector2(1280, 720)
func _ready():
add_to_group("ingame_hud")
print("IngameHUD: _ready() called")
# Find nodes safely (using get_node_or_null to avoid crashes)
@@ -72,6 +74,7 @@ func _ready():
label_matchbox_status = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerConnectionStatus/LabelMatchboxStatus")
label_ice_status = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerConnectionStatus/LabelICEStatus")
label_data_channels_status = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerConnectionStatus/LabelDataChannelsStatus")
label_darkness_debug = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerDarknessDebug/LabelDarknessDebug")
# Debug: Log if connection status labels weren't found
if not label_matchbox_status:
@@ -135,6 +138,10 @@ func _ready():
# Add HP value label (curr/max, like inventory) and MP bar
_setup_hp_mp_ui()
func update_darkness_debug(text: String) -> void:
if label_darkness_debug:
label_darkness_debug.text = text
func _on_player_connected(_peer_id: int, _player_info: Dictionary):
_update_host_info()

View File

@@ -100,7 +100,10 @@ func _apply_hidden_state() -> void:
sprite_above.modulate.a = 0.0
if shadow:
shadow.visible = false
# Enable detection area and connect if not already
# No collision while invisible so player doesn't bump into hidden chest
collision_layer = 0
collision_mask = 0
# Enable detection area and connect if not already (perception still runs when player enters)
var det = get_node_or_null("DetectionArea")
if det:
if not det.body_entered.is_connected(_on_detection_area_body_entered):
@@ -115,6 +118,9 @@ func _apply_hidden_state() -> void:
sprite_above.modulate.a = 1.0
if shadow:
shadow.visible = true
# Restore collision when visible (detected or not hidden)
collision_layer = 2
collision_mask = 1 | 2 | 4
func _on_detection_area_body_entered(body: Node) -> void:
if not body.is_in_group("player"):

View File

@@ -78,6 +78,9 @@ func clear_for_new_level() -> void:
_overlay_control.queue_redraw()
func set_maps(explored_map: PackedInt32Array, map_size: Vector2i, grid: Array, player_tile: Vector2i = Vector2i(-1, -1), exit_tile: Vector2i = Vector2i(-1, -1), exit_discovered: bool = false, other_player_tiles: Array = [], rooms: Array = [], visible_tiles: Array = []) -> void:
# New level (map size changed): clear so we don't show level 1 rooms as explored on level 2
if map_size != _map_size and _map_size != Vector2i.ZERO:
clear_for_new_level()
_explored_map = explored_map
_map_size = map_size
_grid = grid

View File

@@ -6,10 +6,11 @@ extends CharacterBody2D
var character_stats: CharacterStats
var appearance_rng: RandomNumberGenerator # Deterministic RNG for appearance/stats
@export var move_speed: float = 80.0
@export var move_speed: float = 65.0 # Base move speed (not affected by DEX)
@export var grab_range: float = 20.0
@export var base_throw_force: float = 80.0 # Base throw force (reduced from 150), scales with STR
@export var cone_light_angle: float = 180.0 # Cone spread angle in degrees (adjustable, default wider)
const CONE_LIGHT_LERP_SPEED: float = 12.0 # How quickly cone rotation follows target (higher = snappier)
# Network identity
var peer_id: int = 1
@@ -1743,16 +1744,13 @@ func _update_facing_from_mouse(mouse_direction: Vector2):
# Mark that mouse control is active (prevents movement keys from overriding attack direction)
mouse_control_active = true
# Store full 360-degree direction for attacks
# Store full 360-degree direction for attacks (cone light uses this for smooth rotation)
if mouse_direction.length() > 0.1:
facing_direction_vector = mouse_direction.normalized()
var new_direction = _get_direction_from_vector(mouse_direction) as Direction
# Update direction and cone light rotation if changed
if new_direction != current_direction:
current_direction = new_direction
_update_cone_light_rotation()
func _set_animation(anim_name: String):
if current_animation != anim_name:
@@ -1782,10 +1780,18 @@ func _direction_to_angle(direction: int) -> float:
_:
return PI / 2.0 # Default to DOWN
# Update cone light rotation based on player's facing direction
func _update_cone_light_rotation():
if cone_light:
cone_light.rotation = _direction_to_angle(current_direction) + (PI / 2)
# Update cone light rotation based on facing (lerps toward target for smooth 360° movement)
func _update_cone_light_rotation(delta: float = 1.0):
if not cone_light:
return
var target_angle: float
if facing_direction_vector.length() > 0.1:
target_angle = facing_direction_vector.angle() + (PI / 2.0)
else:
target_angle = _direction_to_angle(current_direction) + (PI / 2.0)
# Lerp toward target (delta=1.0 in _ready snaps; in _physics_process uses smooth follow)
var t = 1.0 - exp(-CONE_LIGHT_LERP_SPEED * delta)
cone_light.rotation = lerp_angle(cone_light.rotation, target_angle, t)
# Create a cone-shaped light texture programmatically
# Creates a directional cone texture that extends forward and fades to the sides
@@ -2511,25 +2517,17 @@ func _handle_input():
var new_direction = _get_direction_from_vector(shield_block_direction) as Direction
if new_direction != current_direction:
current_direction = new_direction
_update_cone_light_rotation()
elif not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0:
var new_direction = _get_direction_from_vector(input_vector) as Direction
# Update direction and cone light rotation if changed
if new_direction != current_direction:
current_direction = new_direction
_update_cone_light_rotation()
elif direction_lock_timer > 0.0 and locked_facing_direction.length() > 0.0:
# Use locked direction for animations during attack
var new_direction = _get_direction_from_vector(locked_facing_direction) as Direction
if new_direction != current_direction:
current_direction = new_direction
_update_cone_light_rotation()
elif is_pushing or (held_object and not is_lifting):
# Keep direction from when grab started (don't turn to face the object)
if push_direction_locked != current_direction:
current_direction = push_direction_locked as Direction
_update_cone_light_rotation()
# Set animation based on state
if grabbed_by_enemy_hand:
@@ -2571,20 +2569,20 @@ func _handle_input():
elif is_pushing or (held_object and not is_lifting):
if is_pushing:
_set_animation("IDLE_PUSH")
# Keep direction from when grab started
if push_direction_locked != current_direction:
current_direction = push_direction_locked as Direction
_update_cone_light_rotation()
elif is_shielding:
# Keep locked block direction when shielding and idle
var new_direction = _get_direction_from_vector(shield_block_direction) as Direction
if new_direction != current_direction:
current_direction = new_direction
_update_cone_light_rotation()
else:
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "AXE" and current_animation != "PUNCH" and current_animation != "CONJURE" and current_animation != "LIFT" and current_animation != "FINISH_SPELL":
_set_animation("IDLE")
# Cone light lerps toward facing direction every frame (360°)
if is_local_player and cone_light and cone_light.visible:
_update_cone_light_rotation(get_physics_process_delta_time())
# Handle drag sound for interactable objects
var is_dragging_now = false
if held_object and is_pushing and not is_lifting: