flan/examples/textures-fog-of-war.flan
Joseph Ferano 52e9c92b0f The enum prefix goes uniform across all eleven
The author's ruling: "I think the prefix reads better, keep it" — so two
prefixed enums out of eleven was the inconsistency, not the prefix.
TraceLogLevel takes log-, CameraProjection projection-, CameraMode camera-,
GamepadButton button-, GamepadAxis axis-, Gesture gesture-, MouseCursor
cursor-, TextureFilter filter-, PixelFormat pixel-.

Two of those are judgement. CameraProjection and CameraMode share raylib's
CAMERA_ and deliberately do not share a Flan prefix: they are two questions
asked of the same struct, and :projection-perspective beside :camera-orbital
says which is being answered. GamepadButton and GamepadAxis take the short
stems rather than a shared gamepad-, which keeps :button-left-face-up and
:axis-left-trigger readable.

It is a reading choice and not a collision fix, and bindings, raylib.flan and
docs/BUILT.md all say so: a keyword resolves against the expected type and
nothing else, so :point at a TextureFilter site was never ambiguous. What the
prefix buys is the call site read on its own.

The three constant exception lines are keyed on the member's full Flan
spelling and moved with it. flan generate-c vendor/raylib is green against
raylib-5.5.h, and the check was confirmed non-vacuous by breaking it:
filter-trilinearr reported TEXTURE_FILTER_TRILINEARR rather than passing.
test_flan pins one member of each of the eleven to the C name the rule
reaches, read out of the real bindings file.

sand.flan line 121 is (rl/set-trace-log-level :warning) and is the author's
to respell. TraceLogLevel carries a warning alias beside log-warning, mapped
by name in bindings, so the suite stays green until he does; FIX.org has the
three-edit removal recipe.
2026-09-21 07:59:48 +07:00

195 lines
8.7 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
;;;; :filter-point 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;
;;;; `:filter-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
;;;; (docs/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.
(defonce 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.
(defonce tile-fog [375 u8])
(defonce player-position rl/Vector2)
(defonce player-tile-x i32)
(defonce player-tile-y i32)
(defonce 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) :filter-bilinear)
(rl/set-target-fps 60)
(until (rl/window-should-close?)
;; Update
(when (rl/key-down? :key-right) (set (.x player-position) (+ (.x player-position) 5.0)))
(when (rl/key-down? :key-left) (set (.x player-position) (- (.x player-position) 5.0)))
(when (rl/key-down? :key-down) (set (.y player-position) (+ (.y player-position) 5.0)))
(when (rl/key-down? :key-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/with-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/with-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))))