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
43 changes: 33 additions & 10 deletions xsofy/fire.lg
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@
(def fire-spread-chance 30) ;; percent per adjacent flammable per tick
(def fire-default-ttl 5)
(def protected-tiles #{13 14}) ;; stairs must survive dragon fire and rune fire
(def water-tiles #{3 4}) ;; shallow + deep — extinguishes fire, never ignites

(defn water-tile? [tile]
(contains? water-tiles tile))

(defn on-water?
"True if the entity's current tile is shallow or deep water."
[world entity-id]
(if-let [[x y] (get-in world [:entities entity-id :pos])]
(water-tile? (terrain/tget (:terrain world) (:width world) x y))
false))

(defn apply-burning
"Apply :burning unless the entity is standing in water (#162).
Move-time extinguish already handles walking into water; this closes the
standing-still path (e.g. fire-imp shot while already in water)."
[world entity-id ttl]
(if (on-water? world entity-id)
world
(status/apply-status world entity-id :burning ttl)))

;; Fire is light-emitting, so any ignition must invalidate the :lights
;; cache — step-fire's own terrain-changed gate only sees fires it spreads
Expand All @@ -36,17 +56,20 @@
world)))

(defn ignite-forced [world x y ttl]
"Force fire at a position regardless of terrain."
"Force fire at a position regardless of terrain.
Water is the exception: it never ignites and never gains fire-ttl (#162)."
(let [{:keys [terrain width]} world
tile (terrain/tget terrain width x y)
placed? (and (not= tile 0) (not= tile 8) (not= tile 7)
(not (contains? protected-tiles tile)))] ;; not void, wall, lava, stairs
(when placed?
(terrain/tset! terrain width x y fire-tile))
(if (contains? protected-tiles tile)
tile (terrain/tget terrain width x y)]
(if (water-tile? tile)
world
(let [w (assoc-in world [:fire-ttl [x y]] ttl)]
(if placed? (assoc w :lights nil) w)))))
(let [placed? (and (not= tile 0) (not= tile 8) (not= tile 7)
(not (contains? protected-tiles tile)))] ;; not void, wall, lava, stairs
(when placed?
(terrain/tset! terrain width x y fire-tile))
(if (contains? protected-tiles tile)
world
(let [w (assoc-in world [:fire-ttl [x y]] ttl)]
(if placed? (assoc w :lights nil) w)))))))

(defn step-fire [world]
"One tick of fire simulation. Spread, damage, burn out.
Expand Down Expand Up @@ -118,7 +141,7 @@
(ent/damage-entity w (:id e) fire-damage))]
;; only apply burning if still alive
(if (get-in w [:entities (:id e)])
(status/apply-status w (:id e) :burning 10)
(apply-burning w (:id e) 10)
w)))
w targets)))
w @new-ttl)]
Expand Down
2 changes: 1 addition & 1 deletion xsofy/spell.lg
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@
pos nil))
(fn [[world] pos e]
(let [world (fire/ignite-forced world (first pos) (second pos) ttl)]
[(status/apply-status world (:id e) :burning burn-ttl)])))]
[(fire/apply-burning world (:id e) burn-ttl)])))]
(assoc ctx :world w))))

(defn r-ice
Expand Down
47 changes: 45 additions & 2 deletions xsofy/test/fire_test.lg
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
(ns xsofy.test.fire-test
(:require [test :refer [deftest is testing]]
[xsofy.terrain :as terrain]
[xsofy.fire :as fire]))
[xsofy.status :as status]
[xsofy.fire :as fire]
[xsofy.spell :as spell]))

(defn- room-world []
(let [w 12 h 8
Expand All @@ -10,7 +12,7 @@
{:terrain t :width w :height h :fire-ttl {}}))

;; Regression for the :lights cache gate (perf: only invalidate on real
;; terrain mutation). step-fire's terrain-changed gate only sees fires it
;; terrain mutation). step-fire's own terrain-changed gate only sees fires it
;; spreads or burns out — ignitions from burning-entities-ignite / dragon /
;; rune fire run in separate phases, so the ignite helpers must invalidate
;; :lights themselves or a new fire emits no light until it spreads.
Expand All @@ -35,3 +37,44 @@
_ (terrain/tset! (:terrain w) (:width w) 3 3 8)] ;; wall — not flammable
(let [w' (fire/ignite w 3 3)]
(is (= [] (:lights w'))))))

;; --- #162: water prevents burning / forced ignition ---

(deftest ignite-forced-skips-water
(testing "shallow and deep water never become fire"
(doseq [tile [3 4]]
(let [w (assoc (room-world) :lights [])
_ (terrain/tset! (:terrain w) (:width w) 3 3 tile)
w' (fire/ignite-forced w 3 3 5)]
(is (= tile (terrain/tget (:terrain w') (:width w') 3 3)))
(is (nil? (get-in w' [:fire-ttl [3 3]])))
(is (= [] (:lights w')))))))

(deftest apply-burning-skipped-on-water
(testing "entity standing in water does not catch fire"
(doseq [tile [3 4]]
(let [w (-> (room-world)
(assoc :entities {:e {:id :e :pos [3 3] :body {:hp 10}}}))
_ (terrain/tset! (:terrain w) (:width w) 3 3 tile)
w' (fire/apply-burning w :e 12)]
(is (not (status/has-status? w' :e :burning))))))
(testing "entity on floor still burns"
(let [w (-> (room-world)
(assoc :entities {:e {:id :e :pos [3 3] :body {:hp 10}}}))
w' (fire/apply-burning w :e 12)]
(is (status/has-status? w' :e :burning)))))

(deftest fire-imp-spell-on-water-does-not-burn
(testing "an imp's fire rune leaves a player in water unburned"
(doseq [tile [3 4]]
(let [w (-> (room-world)
(assoc :entities {:player {:id :player :pos [3 3]
:body {:hp 20 :max-hp 20}}
:imp {:id :imp :pos [2 3]
:body {:hp 8 :max-hp 8}}}))
_ (terrain/tset! (:terrain w) (:width w) 3 3 tile)
result (spell/eval-spell w :imp [3 3] [:fire])
w' (:world result)]
(is (= tile (terrain/tget (:terrain w') (:width w') 3 3)))
(is (nil? (get-in w' [:fire-ttl [3 3]])))
(is (not (status/has-status? w' :player :burning)))))))
2 changes: 1 addition & 1 deletion xsofy/world.lg
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,7 @@
(opctx/with-context {:source :lava :reason :hazard}
(ent/damage-entity world entity-id 8))
(#(if (get-in % [:entities entity-id])
(status/apply-status % entity-id :burning 12)
(fire/apply-burning % entity-id 12)
%))))
;; chasm: 10 fall damage
6 (opctx/with-context {:source :chasm :reason :hazard}
Expand Down
Loading