Fog lit by rays: shadows cut through tiles, and walls stay visible
ci / verify (push) Successful in 52s
ci / verify (push) Successful in 52s
The fog asked one question per tile -- can I see its centre? -- and drew or skipped the whole tile on the answer. Every shadow was a staircase, tiles snapped in and out as you walked, and a wall beside you vanished whenever its centre happened to sit behind its neighbour. Visibility casts rays from the eye instead: a fixed sweep, plus two aimed either side of every wall corner, which is what makes each shadow edge a straight line rather than a smear between rays. The view now draws terrain and actors first and then darkness over whatever the rays did not reach, so a shadow runs straight through a tile and an enemy rounding a corner appears a slice at a time. Bullets are a child node and are still drawn over the dark; telegraphs are drawn after it, as they must be. "A bit less strict" is three specific things. A wall face you can see is lit 26 units in, measured perpendicular to the face; shadows end in a short fade rather than a line; and sight fades over its last 64 units rather than stopping at a circle. Still hard fog -- nothing remembered. The first version measured that depth along the ray, and a screenshot caught what no test had: a wall you look along got a sliver, so the wall beside you in a tunnel looked missing -- the old fog's own complaint. Perpendicular depth fixes it, and light stops the instant a ray steps back into the open, so it never rounds a corner, never reaches the far side of a one-tile wall, and never leaves a stepped wall through its other face. Two tiles touching only at a corner are closed. About half a millisecond a frame. Measured against the old fog in the same scene: hub 68 fps against 62, dungeon 240 against 240. The hub's own low rate predates this. MapGrid gains a revision counter so the corner list is rebuilt only when the map changes or the eye crosses a tile. screenshot.tscn now captures a dungeon, a tunnel and the tunnel with F1's overlay, which draws where each ray stopped -- and makes SHOT_DIR rather than failing every capture when it does not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
83c3b920fb
commit
855669a665
12 files changed
+871
-45
No files matched your search
+67
-1
@@ -29,7 +29,12 @@ const OPEN_UPGRADES := 330
|
||||
const AT_UPGRADES := 360
|
||||
const OPEN_ROSTER := 370
|
||||
const AT_ROSTER := 400
|
||||
const DONE := 410
|
||||
const ENTER_DUNGEON := 410
|
||||
const AT_DUNGEON := 480
|
||||
const TO_TUNNEL := 490
|
||||
const AT_TUNNEL := 550
|
||||
const AT_TUNNEL_DEBUG := 560
|
||||
const DONE := 570
|
||||
|
||||
var _frame: int = 0
|
||||
var _game: Node = null
|
||||
@@ -44,6 +49,10 @@ func _ready() -> void:
|
||||
var from_env := OS.get_environment("SHOT_DIR")
|
||||
if not from_env.is_empty():
|
||||
_dir = from_env
|
||||
# Made rather than assumed. The example in CLAUDE.md points at a fresh
|
||||
# /tmp/shots, and a missing directory fails every capture one at a time
|
||||
# with an error that names the file rather than the folder.
|
||||
DirAccess.make_dir_recursive_absolute(_dir)
|
||||
get_window().size = Vector2i(1280, 720)
|
||||
_menu = preload("res://src/ui/main_menu.gd").new()
|
||||
add_child(_menu)
|
||||
@@ -118,6 +127,23 @@ func _process(_delta: float) -> void:
|
||||
_show_the_roster()
|
||||
AT_ROSTER:
|
||||
_grab("roster")
|
||||
ENTER_DUNGEON:
|
||||
# The fog is what these two are for, and the hub is the worst place to
|
||||
# judge it: one big room with little in it to cast a shadow.
|
||||
_game.set("_roster_open", false)
|
||||
Net.server._send_to_dungeon(Net.LOCAL_PEER)
|
||||
AT_DUNGEON:
|
||||
_grab("dungeon")
|
||||
TO_TUNNEL:
|
||||
_stand_in_a_tunnel()
|
||||
AT_TUNNEL:
|
||||
_grab("tunnel")
|
||||
# The same view with F1's overlay: the outline is where each fog ray
|
||||
# stopped, which is the quickest way to tell a rendering mistake from
|
||||
# a geometry one.
|
||||
_game.get_node("WorldView").debug_hitboxes = true
|
||||
AT_TUNNEL_DEBUG:
|
||||
_grab("tunnel_debug")
|
||||
DONE:
|
||||
Net.shutdown()
|
||||
get_tree().quit(0)
|
||||
@@ -152,6 +178,46 @@ func _show_the_roster() -> void:
|
||||
_game.get_node("CharacterSelect").refresh(roster, "c4")
|
||||
|
||||
|
||||
## Move the player, on both sides, into the tunnel closest to where it spawned:
|
||||
## somewhere with walls close on two sides and a corner in view, which is where
|
||||
## the old fog looked worst.
|
||||
func _stand_in_a_tunnel() -> void:
|
||||
var inst := Net.server.instance_of(Net.LOCAL_PEER)
|
||||
if inst == null or inst.rooms.is_empty():
|
||||
return
|
||||
var map := inst.world.map
|
||||
var start: Rect2i = inst.rooms[0]
|
||||
var spawn := map.to_tile(inst.world.spawn_point)
|
||||
var best := Vector2i(-1, -1)
|
||||
var best_d := INF
|
||||
for ty in map.height:
|
||||
for tx in map.width:
|
||||
if map.at(tx, ty) != MapGrid.Kind.FLOOR:
|
||||
continue
|
||||
var here := Vector2i(tx, ty)
|
||||
var in_room := inst.boss_room.has_point(here)
|
||||
for r: Rect2i in inst.rooms:
|
||||
if r.grow(1).has_point(here):
|
||||
in_room = true
|
||||
if in_room:
|
||||
continue
|
||||
# A couple of tiles clear of the first room, so the tunnel's walls
|
||||
# are on screen and not just the doorway.
|
||||
if start.grow(3).has_point(here):
|
||||
continue
|
||||
var d := float((here - spawn).length_squared())
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = here
|
||||
if best.x < 0:
|
||||
return
|
||||
var at := map.tile_centre(best.x, best.y)
|
||||
Net.client.predicted_pos = at
|
||||
var p: SimPlayer = inst.world.players.get(Net.LOCAL_PEER)
|
||||
if p != null:
|
||||
p.pos = at
|
||||
|
||||
|
||||
## One roster row in the shape decode_characters produces.
|
||||
func _roster_entry(who: String, level: int, ceiling: int, current: int,
|
||||
tint: Color) -> Dictionary:
|
||||
|
||||
Reference in new issue
Block a user