fixed a bit better lightning
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
# 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))
|
||||
@@ -3630,10 +3726,39 @@ func _get_darkness_scale_at_position(world_pos: Vector2) -> float:
|
||||
if in_room:
|
||||
var tc = clampi(_count_torches_in_room(current_room), 0, 4)
|
||||
raw = tc / 4.0
|
||||
else:
|
||||
# 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:
|
||||
@@ -3722,8 +3879,15 @@ func _reapply_torch_darkening() -> void:
|
||||
if in_room:
|
||||
var tc = clampi(_count_torches_in_room(current_room), 0, 4)
|
||||
raw = tc / 4.0
|
||||
else:
|
||||
# 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,6 +4580,7 @@ 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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user