Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 31 additions & 16 deletions xsofy/render.lg
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@
(term/set-bg r g b)
(swap! sgr-pen assoc :bg c))))

;; Synchronized output (DEC 2026) lives in xsofy.screenfx now (shared with the
;; title/descent screens), used here as sfx/sync-begin! and sfx/flush-frame!.

;; --- Appearance buffer (render-perf: skip frame-to-frame unchanged cells) ---
;; The real per-move waste isn't redundant same-color runs (SGR elision above
;; barely fires — per-cell lighting + jitter make neighbors distinct); it's that
Expand Down Expand Up @@ -787,7 +790,8 @@
(term/write ch))))

nil)))
(term/flush))))
;; no flush here — the caller (animate-vfx!) presents the whole frame
)))

(defn tick-vfx [vfx]
"Advance vfx list — decay bolts/flashes, step projectiles. Remove expired."
Expand Down Expand Up @@ -822,18 +826,20 @@
;; restore cells from previous frame that aren't in current frame
to-restore (core/into prev-dirty cur-dirty)]
;; restore all previously dirty cells first
(sfx/sync-begin!)
(when (seq prev-dirty)
(restore-dirty-cells! world r prev-dirty)
(term/flush))
(restore-dirty-cells! world r prev-dirty))
;; render current vfx overlay
(render-vfx-frame (assoc world :vfx vfx) r)
(sfx/flush-frame!)
(sfx/pause! (vfx-frame-delay vfx))
(let [next-vfx (tick-vfx vfx)]
(if (seq next-vfx)
(recur next-vfx cur-dirty)
;; final frame — restore all dirty cells
(do (restore-dirty-cells! world r cur-dirty)
(term/flush)))))))))
;; final frame — restore all dirty cells in one more synced pass
(do (sfx/sync-begin!)
(restore-dirty-cells! world r cur-dirty)
(sfx/flush-frame!)))))))))

;; --- Item Display Helpers ---

Expand Down Expand Up @@ -918,6 +924,7 @@
inv)))))

(defn render-quick-menu [world]
(sfx/sync-begin!)
(let [[tw th] (term-dims)
action (or (:menu-action world) :equip)
items (menu-filter world action)
Expand Down Expand Up @@ -950,11 +957,12 @@
(ui/write-line inner (dec (:h inner))
" [a-z] select [enter] confirm [esc] cancel"
[60 60 60] [20 18 16])
(term/flush)))
(sfx/flush-frame!)))

;; --- Message Log Screen (modal) ---

(defn render-messages-screen [world]
(sfx/sync-begin!)
(let [[tw th] (term-dims)
r (ui/rect 1 1 tw th)
inner (ui/box r "Message Log" [180 170 140] ui/bg)
Expand All @@ -981,7 +989,7 @@
"0/0")
" [k/up older j/down newer PgUp/PgDn g/G Esc]")
[80 80 80] ui/bg)
(term/flush)))
(sfx/flush-frame!)))

;; --- Help Screen (modal) ---

Expand Down Expand Up @@ -1010,6 +1018,7 @@
(swap! row inc)))))

(defn render-help-screen [world]
(sfx/sync-begin!)
(let [[tw th] (term-dims)
r (ui/rect 1 1 tw th)
inner (ui/box r "Keybindings" [180 170 140] ui/bg)
Expand All @@ -1020,7 +1029,7 @@
(ui/write-line inner (dec (:h inner))
" [press any key to close]"
[80 80 80] ui/bg)
(term/flush)))
(sfx/flush-frame!)))

;; --- Inventory Screen (modal) ---

Expand Down Expand Up @@ -1082,6 +1091,7 @@
(ui/write-line r (inc @rune-row) cmds [120 120 100] ui/bg)))))))

(defn render-inventory-screen [world]
(sfx/sync-begin!)
(let [[tw th] (term-dims)
r (ui/rect 1 1 tw th)
inner (ui/box r "Inventory" [180 170 140] ui/bg)
Expand Down Expand Up @@ -1139,7 +1149,7 @@
(ui/write-line inner (- (:h inner) 1)
" [a-z/jk] select [esc/i] close"
[60 60 60] ui/bg)
(term/flush)))
(sfx/flush-frame!)))

;; --- Death Screen ---

Expand Down Expand Up @@ -1170,6 +1180,7 @@
(sfx/scatter-runes tw th 30 [200 50 30])) ;; crimson runes

(defn render-death-screen [world runes scroll-offset frame]
(sfx/sync-begin!)
(let [[tw th] (term-dims)
cx (quot tw 2)
cy (quot th 2)
Expand Down Expand Up @@ -1313,11 +1324,12 @@
(term/set-fg 80 70 60)
(term/set-bg 10 10 10)
(term/write prompt))
(term/flush)))
(sfx/flush-frame!)))

;; --- Rune Codex Screen ---

(defn render-rune-codex [world]
(sfx/sync-begin!)
(let [[tw th] (term-dims)
r (ui/rect 1 1 tw th)
inner (ui/box r "Rune Codex" [170 140 220] ui/bg)
Expand Down Expand Up @@ -1345,11 +1357,12 @@
(ui/write-line inner (dec (:h inner))
" [press any key to close]"
[80 80 80] ui/bg)
(term/flush)))
(sfx/flush-frame!)))

;; --- Inscription Screen ---

(defn render-inscribe-screen [world]
(sfx/sync-begin!)
(let [[tw th] (term-dims)
r (ui/rect 1 1 tw th)
inner (ui/box r "Inscribe Runes" [200 160 240] ui/bg)
Expand Down Expand Up @@ -1404,7 +1417,7 @@
(ui/write-line inner (- (:h inner) 1)
" [jk] select [space] add [x] remove last [enter] confirm [esc] cancel"
[80 80 80] ui/bg)
(term/flush)))
(sfx/flush-frame!)))

;; --- Public API ---

Expand All @@ -1425,6 +1438,7 @@
(let [[tw th] (term-dims)
panels (ui/layout tw th)
mr (with-camera world (:map panels))]
(sfx/sync-begin!)
;; blank entire terminal with bg color
(ui/apply-bg ui/bg)
(ui/apply-fg ui/bg)
Expand All @@ -1438,7 +1452,7 @@
(render-sidebar world (:sidebar panels))
(render-log world (:log panels))
(render-status world (:status panels))
(term/flush)
(sfx/flush-frame!)
(commit-camera! world mr)))

(defn render-dirty [world prev-world]
Expand All @@ -1456,6 +1470,7 @@
old-fov (:fov prev-world)
new-fov (:fov world)
prev-cam (or @last-camera mr)]
(sfx/sync-begin!)
(if (or (not= (:cx prev-cam) (:cx mr))
(not= (:cy prev-cam) (:cy mr)))
;; Camera shifted — viewport contents moved; redraw map fully.
Expand All @@ -1465,7 +1480,7 @@
(render-sidebar world (:sidebar panels))
(render-log world (:log panels))
(render-status world (:status panels))
(term/flush)
(sfx/flush-frame!)
(commit-camera! world mr))
;; Camera held — FOV-delta path. map-cell handles visibility check;
;; no outer bounds guard needed because x,y are world coords.
Expand Down Expand Up @@ -1512,5 +1527,5 @@
(render-sidebar world (:sidebar panels))
(render-log world (:log panels))
(render-status world (:status panels))
(term/flush)
(sfx/flush-frame!)
(commit-camera! world mr)))))
24 changes: 22 additions & 2 deletions xsofy/screenfx.lg
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,32 @@
v (if (< t 30) t (- 60 t))]
(/ (float v) 30.0)))

;; --- Synchronized output (DEC private mode 2026) ---
;; Shared with every full-screen frame emitter — the game render (xsofy.render),
;; the title/descent screens (xsofy.title), and clear-screen below — so a frame
;; presents atomically instead of tearing mid-draw. Lives here (the shared
;; screen-effects ns) rather than in render so title can use it without reaching
;; into core. Mode 2026 is a boolean (never nest — a single ESU closes it), so
;; wrap only top-level frames, one pair each. No feature detection: unsupported
;; terminals ignore the sequences. sync-output? is a source opt-out (the web
;; target wants it on). The terminal owns the release timeout.
(def sync-output? true)
(def begin-sync (str (char 27) "[?2026h"))
(def end-sync (str (char 27) "[?2026l"))
(defn sync-begin! [] (when sync-output? (term/write begin-sync)))
;; End a synchronized frame: ESU then flush. Use in place of a bare (term/flush)
;; at a frame boundary, paired with a preceding (sync-begin!).
(defn flush-frame! []
(when sync-output? (term/write end-sync))
(term/flush))

(defn clear-screen
"Paint the whole tw×th screen the dark background colour."
"Paint the whole tw×th screen the dark background colour, as one synced frame."
[tw th]
(sync-begin!)
(term/set-bg 10 10 10)
(term/set-fg 10 10 10)
(dotimes [y th]
(term/move-cursor 1 (inc y))
(term/write (apply str (repeat tw " "))))
(term/flush))
(flush-frame!))
18 changes: 17 additions & 1 deletion xsofy/test/render_test.lg
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
(ns xsofy.test.render-test
(:require [test :refer [deftest is testing]]
[xsofy.render :as render]))
[xsofy.render :as render]
[xsofy.screenfx :as screenfx]))

;; --- Synchronized output (DEC 2026), shared in xsofy.screenfx ---

(deftest sync-output-escapes-are-dec-2026
;; Guard the exact begin/end sequences — a typo silently disables atomic
;; present or leaves garbage on the wire. They live in screenfx so render, the
;; title/descent screens, and clear-screen all share one implementation.
(testing "begin/end-sync are CSI ?2026 h/l"
(is (= (str (char 27) "[?2026h") screenfx/begin-sync))
(is (= (str (char 27) "[?2026l") screenfx/end-sync))))

(deftest sync-helpers-emit-the-escapes
(testing "sync-begin! writes BSU, flush-frame! writes ESU (default sync-output? on)"
(is (= screenfx/begin-sync (with-out-str (screenfx/sync-begin!))))
(is (= screenfx/end-sync (with-out-str (screenfx/flush-frame!))))))

(deftest message-log-window-shows-newest-entries-by-default
(testing "newest entries are visible when scroll is zero"
Expand Down
6 changes: 4 additions & 2 deletions xsofy/title.lg
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
;; screen.

(defn draw-frame [runes title subtitle tw th frame seed]
(sfx/sync-begin!)
(let [cx (quot tw 2)
cy (quot th 2)
tx (max 1 (- cx (quot (count title) 2)))
Expand Down Expand Up @@ -161,7 +162,7 @@
(term/set-fg 90 85 70)
(term/set-bg 10 10 10)
(term/write sline))
(term/flush)))
(sfx/flush-frame!)))

(defn show-title-screen [rng]
(let [seed (:seed rng) ;; capture before picks advance the rng — this is the run seed
Expand Down Expand Up @@ -229,6 +230,7 @@
(sfx/clear-screen tw th)
;; animate — runes drift upward, erase old position each frame
(dotimes [frame 15]
(sfx/sync-begin!)
(doseq [r runes]
(let [;; current y drifts upward (subtract), wrap around
cur-y (rem (+ th (:y r) (- (quot frame 2))) th)
Expand Down Expand Up @@ -263,5 +265,5 @@
(term/set-fg (int (* 120 mfade)) (int (* 110 mfade)) (int (* 90 mfade)))
(term/set-bg 10 10 10)
(term/write msg))
(term/flush)
(sfx/flush-frame!)
(sfx/pause! 60)))))
Loading