flan/examples/textures-fog-of-war.fln

171 lines
8.1 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 enum in
;;;; vendor/raylib/raylib.fln, 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.fln"
const screen-width = 800
const screen-height = 450
const map-tile-size = 32
const 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.
const player-tile-visibility = 2
const tiles-x = 25
const tiles-y = 15
;; 0 or 1, picked once: which of the two blues a tile is drawn in.
once 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.
once tile-fog: [375 u8]
once player-position: rl/Vector2
once player-tile-x: i32
once player-tile-y: i32
once fog-of-war: rl/RenderTexture2D
fn main() -> ()
rl/init-window(screen-width, screen-height,
"raylib [textures] example - fog of war")
defer rl/close-window()
tile-ids = array(375, u8)
tile-fog = array(375, u8)
for i in range(tiles-x * tiles-y)
tile-ids[i] = u8(rl/get-random-value(0, 1))
player-position = rl/Vector2{.x 180.0 .y 130.0}
player-tile-x = 0
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.
fog-of-war = rl/load-render-texture(tiles-x, tiles-y)
defer rl/unload-render-texture(fog-of-war)
rl/set-texture-filter(fog-of-war.texture, :filter-bilinear)
rl/set-target-fps(60)
until rl/window-should-close()
;; Update
if rl/is-key-down(:key-right)
player-position.x += 5.0
if rl/is-key-down(:key-left)
player-position.x -= 5.0
if rl/is-key-down(:key-down)
player-position.y += 5.0
if rl/is-key-down(:key-up)
player-position.y -= 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)
let max-y = f32(tiles-y * map-tile-size - player-size)
player-position.x = clamp(player-position.x, 0.0, max-x)
player-position.y = clamp(player-position.y, 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.
for i in range(tiles-x * tiles-y)
if tile-fog[i] == 1
tile-fog[i] = 2
;; Which tile the player's centre is standing on.
player-tile-x = i32((player-position.x + f32(map-tile-size / 2)) / f32(map-tile-size))
player-tile-y = i32((player-position.y + 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 vis-y = player-tile-y - player-tile-visibility
while vis-y < player-tile-y + player-tile-visibility
let x = player-tile-x - player-tile-visibility
while x < player-tile-x + player-tile-visibility
if x >= 0 and x < tiles-x and vis-y >= 0 and vis-y < tiles-y
tile-fog[vis-y * tiles-x + x] = 1
x += 1
vis-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)
for y in range(tiles-y)
for x in range(tiles-x)
;; A tile in view (1) draws nothing at all and stays transparent.
let f = tile-fog[y * tiles-x + x]
if f != 1
rl/draw-rectangle(x, y, 1, 1,
if f == 0 then rl/black else rl/fade(rl/black, 0.8))
rl/with-drawing:
rl/clear-background(rl/raywhite)
;; The map itself, in full size.
for y in range(tiles-y)
for x in range(tiles-x)
rl/draw-rectangle(x * map-tile-size, y * map-tile-size, map-tile-size,
map-tile-size,
if tile-ids[y * tiles-x + x] == 0 then rl/blue else 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(fog-of-war.texture,
rl/Rectangle{.x 0.0, .y 0.0, .width f32(fog-of-war.texture.width), .height 0.0 - f32(fog-of-war.texture.height)},
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.fln requires:
;; both share one static buffer in the runtime.
let x = 10
x += d/draw-piece("Current tile: [", x, 10, 20, rl/raywhite)
x += d/draw-int(player-tile-x, x, 10, 20, rl/raywhite)
x += d/draw-piece(",", x, 10, 20, rl/raywhite)
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)