flan/examples/textures-fog-of-war.flan
Joseph Ferano d7ceec448e Ten more raylib examples, and the three small structs 3D needed
Three shapes, two text, three textures, one models and one core, picked for
binding surface rather than for how they look.

shapes-basic-shapes brings in six draw families nothing had called — the
circle and rectangle gradients, the triangles, all three poly draws — and is
the first call in the corpus to pass two Colors or three Vector2s at once.
shapes-collision-area is get-collision-rec, the only binding that takes two
Rectangles and answers a third, on a frame path. shapes-following-eyes is the
raymath gap measured rather than worked around: every line of it is vector
arithmetic written without a vector library, the way the C writes it.

text-input-box drains get-char-pressed's queue, which no example had read,
and needed a MouseCursor defenum for set-mouse-cursor. text-writing-anim
replaces TextSubtext — unbindable, it answers a pointer into a rotating
static buffer — with (string (slice b 0 n)), which is the same operation
without the shared state.

textures-image-generation runs nine Gen* calls and the
gen/upload/unload-image path, all procedural, no file on disk.
textures-fog-of-war needed a TextureFilter defenum: the smooth fog edge is
entirely :bilinear on a 25x15 render texture, and it is also the first
draw-texture-pro with a negative source height. textures-mouse-painting is
the same render texture used as a document rather than as scratch, plus the
round trip back off the GPU — load-image-from-texture, image-flip-vertical,
export-image — which nothing had run.

models-box-collisions is the counterexample to "a models example is a binding
exercise": nothing in it is a Model, and one BoundingBox defstruct un-refuses
four functions. core-3d-picking is the only caller anywhere for Ray and
RayCollision, and picking is the inverse of the get-world-to-screen the
corpus already had.

Added to vendor/raylib: defstructs BoundingBox, Ray and RayCollision;
defenums MouseCursor and TextureFilter with their mapping lines in bindings;
hand-written declare-c for SetMouseCursor, SetTextureFilter, DrawCubeV,
DrawSphere, DrawSphereWires, DrawRay and GetScreenToWorldRay, each excluded
from the generated half on the rule bindings already states. generated.flan
regenerated against raylib 5.5: 272 declarations, 117 refused, every
defstruct, hand-written declare-c and mapped constant agreeing with the
header.
2026-09-13 14:42:49 +07:00

198 lines
8.6 KiB
Plaintext

;;;; raylib [textures] example - fog of war
;;;;
;;;; examples/textures/textures_fog_of_war.c. Picked because it is the only
;;;; example in the whole upstream tree where a *texture filter* is the point
;;;; rather than a detail. The fog is drawn into a render texture one pixel
;;;; per tile — 25 by 15 — and then stretched to 800x450 on the way to the
;;;; screen, and the smooth edge between seen and unseen is entirely the
;;;; bilinear sampler doing the interpolation. With the default :point filter
;;;; the same program draws 32-pixel squares.
;;;;
;;;; That is what needed adding: `TextureFilter`, a new defenum in
;;;; vendor/raylib/raylib.flan, with set-texture-filter moved from the
;;;; generated half to the hand-written one and mapped in `bindings` so its
;;;; six members are checked against raylib.h. The header says `int filter`
;;;; and there is nothing in an `int` to say that 1 is the interesting value;
;;;; `:bilinear` says it.
;;;;
;;;; The three other things it exercises, none of which needed a line:
;;;;
;;;; - A render texture used as a *scratch surface* rather than as a
;;;; post-processing pass. core-scissor-test clipped the real framebuffer;
;;;; this draws a whole second frame into a 25x15 target and then samples
;;;; it. begin-texture-mode/end-texture-mode were already bound.
;;;; - draw-texture-pro with a NEGATIVE source height. A render texture's
;;;; rows come out of GL bottom-up, so every program that samples one has
;;;; to flip it, and raylib's convention for that is a source rectangle
;;;; with a negative height rather than a separate flag. Nothing in the
;;;; corpus had passed one. A Rectangle whose `height` field had been
;;;; permuted with `width` would draw the fog mirrored and the wrong way
;;;; up at once.
;;;; - clear-background with `blank` — alpha 0 — inside a texture mode, so
;;;; the untouched parts of the fog target are transparent and the map
;;;; shows through.
;;;;
;;;; The C's Map struct with its two calloc'd `unsigned char *` is two `[375
;;;; u8]` globals here. The dimensions are compile-time constants in the C
;;;; too (the #defines and the two assignments right after), so nothing is
;;;; lost by fixing them, and a global cannot hold a Vec in any case
;;;; (PORTING.md §3). 375 is 25*15, written out because Flan's array length
;;;; must be a literal.
(import rl "vendor:raylib")
(import d "digits.flan")
(defconst screen-width 800)
(defconst screen-height 450)
(defconst map-tile-size 32)
(defconst player-size 16)
;; How far the player sees, in tiles. The C's loop runs from -this to +this
;; exclusive, so the lit patch is 2*this tiles across and not 2*this+1 — an
;; asymmetry in the original that is kept rather than tidied, because tidying
;; it would change the picture.
(defconst player-tile-visibility 2)
(defconst tiles-x 25)
(defconst tiles-y 15)
;; 0 or 1, picked once: which of the two blues a tile is drawn in.
(defvar tile-ids [375 u8])
;; 0 = never seen (solid black), 1 = visible now (no fog), 2 = seen before
;; (mostly black). The three-way state is why this is a byte per tile and not
;; a bit.
(defvar tile-fog [375 u8])
(defvar player-position rl/Vector2)
(defvar player-tile-x i32)
(defvar player-tile-y i32)
(defvar fog-of-war rl/RenderTexture2D)
(defn main [] ()
(rl/init-window screen-width screen-height
"raylib [textures] example - fog of war")
(defer (rl/close-window))
(set tile-ids (array 375 u8))
(set tile-fog (array 375 u8))
(dotimes [i (* tiles-x tiles-y)]
(set (at tile-ids i) (u8 (rl/get-random-value 0 1))))
(set player-position (rl/Vector2 {.x 180.0 .y 130.0}))
(set player-tile-x 0)
(set player-tile-y 0)
;; One texel per tile. The stretch to full size on draw is where the
;; smoothing happens, which is why the target is this small on purpose.
(set fog-of-war (rl/load-render-texture tiles-x tiles-y))
(defer (rl/unload-render-texture fog-of-war))
(rl/set-texture-filter (.texture fog-of-war) :bilinear)
(rl/set-target-fps 60)
(until (rl/window-should-close?)
;; Update
(when (rl/key-down? :right) (set (.x player-position) (+ (.x player-position) 5.0)))
(when (rl/key-down? :left) (set (.x player-position) (- (.x player-position) 5.0)))
(when (rl/key-down? :down) (set (.y player-position) (+ (.y player-position) 5.0)))
(when (rl/key-down? :up) (set (.y player-position) (- (.y player-position) 5.0)))
;; Keep the player inside the tilemap. The far edge is measured against
;; the player's far side, which is why player-size is subtracted.
(let [max-x (f32 (- (* tiles-x map-tile-size) player-size))
max-y (f32 (- (* tiles-y map-tile-size) player-size))]
(set (.x player-position) (clamp (.x player-position) 0.0 max-x))
(set (.y player-position) (clamp (.y player-position) 0.0 max-y)))
;; Everything lit on the previous frame drops to "seen before". The lit
;; tiles are then set back to 1 below, so a tile still in view never
;; spends a frame dimmed.
(dotimes [i (* tiles-x tiles-y)]
(when (= (at tile-fog i) 1) (set (at tile-fog i) 2)))
;; Which tile the player's centre is standing on.
(set player-tile-x
(i32 (/ (+ (.x player-position) (f32 (/ map-tile-size 2)))
(f32 map-tile-size))))
(set player-tile-y
(i32 (/ (+ (.y player-position) (f32 (/ map-tile-size 2)))
(f32 map-tile-size))))
;; Light the square around the player, skipping anything off the map.
;; Without that test this reads and writes outside the array, which in the
;; C is undefined and here is a bounds trap — the same bug, reported.
(let [y (- player-tile-y player-tile-visibility)]
(while (< y (+ player-tile-y player-tile-visibility))
(let [x (- player-tile-x player-tile-visibility)]
(while (< x (+ player-tile-x player-tile-visibility))
(when (and (>= x 0) (< x tiles-x) (>= y 0) (< y tiles-y))
(set (at tile-fog (+ (* y tiles-x) x)) 1))
(set x (+ x 1))))
(set y (+ y 1))))
;; Draw the fog into its own little target first, at one pixel per tile.
;; blank is alpha 0, so a tile that is neither unseen nor remembered
;; leaves nothing behind and the map below shows through unmodified.
(rl/begin-texture-mode fog-of-war)
(rl/clear-background rl/blank)
(dotimes [y tiles-y]
(dotimes [x tiles-x]
;; A tile in view (1) draws nothing at all and stays transparent.
(let [f (at tile-fog (+ (* y tiles-x) x))]
(when (!= f 1)
(rl/draw-rectangle x y 1 1
(if (= f 0) rl/black (rl/fade rl/black 0.8)))))))
(rl/end-texture-mode)
(rl/begin-drawing)
(rl/clear-background rl/raywhite)
;; The map itself, in full size.
(dotimes [y tiles-y]
(dotimes [x tiles-x]
(rl/draw-rectangle (* x map-tile-size) (* y map-tile-size)
map-tile-size map-tile-size
(if (= (at tile-ids (+ (* y tiles-x) x)) 0)
rl/blue
(rl/fade rl/blue 0.9)))
(rl/draw-rectangle-lines (* x map-tile-size) (* y map-tile-size)
map-tile-size map-tile-size
(rl/fade rl/darkblue 0.5))))
(rl/draw-rectangle-v player-position
(rl/Vector2 {.x (f32 player-size) .y (f32 player-size)})
rl/red)
;; The fog, stretched over the whole map. The negative source height is
;; the flip — see the header comment.
(rl/draw-texture-pro
(.texture fog-of-war)
(rl/Rectangle {.x 0.0 .y 0.0
.width (f32 (.width (.texture fog-of-war)))
.height (- 0.0 (f32 (.height (.texture fog-of-war))))})
(rl/Rectangle {.x 0.0 .y 0.0
.width (f32 (* tiles-x map-tile-size))
.height (f32 (* tiles-y map-tile-size))})
(rl/Vector2 {.x 0.0 .y 0.0})
0.0
rl/white)
;; The C's TextFormat("Current tile: [%i,%i]", ...). Each number is drawn
;; before the next is formatted, which examples/digits.flan requires:
;; both share one static buffer in the runtime.
(let [x 10]
(set x (+ x (d/draw-piece "Current tile: [" x 10 20 rl/raywhite)))
(set x (+ x (d/draw-int player-tile-x x 10 20 rl/raywhite)))
(set x (+ x (d/draw-piece "," x 10 20 rl/raywhite)))
(set x (+ x (d/draw-int player-tile-y x 10 20 rl/raywhite)))
(d/draw-piece "]" x 10 20 rl/raywhite))
(rl/draw-text "ARROW KEYS to move" 10 (- screen-height 25) 20 rl/raywhite)
(rl/end-drawing)))