(ns advent.actions (:require [play-clj.core :refer :all] [play-clj.ui :refer :all] [play-clj.utils :refer :all] [play-clj.g2d :refer :all] [clojure.pprint] [clojure.string :as s] [clojure.zip :as zip] [clojure.set :as set] [advent.pathfind] [advent.actions :as actions] [advent.screens.dialogue :as dialogue] [advent.utils :as utils] [clojure.core.async :refer [put! ! >!! chan go thread take! alts!!]]) (:import [com.badlogic.gdx.graphics Pixmap Pixmap$Filter Texture Texture$TextureFilter] [com.badlogic.gdx.graphics.g2d TextureRegion Animation] [com.badlogic.gdx Screen])) (defprotocol IAction (begin [this screen entities]) (done? [this screen entities]) (continue [this screen entities]) (terminate [this screen entities]) (can-skip? [this screen entities]) (get-channel [this])) (defn has-item? [entities item] (if (map? entities) ((set (get-in entities [:state :inventory])) item) ((set (get-in @entities [:state :inventory])) item))) (defn has-obtained? [entities item] (if (map? entities) ((get-in entities [:state :obtained-items]) item) ((get-in @entities [:state :obtained-items]) item))) (defn has-obtained-one-of? [entities items] (some (partial has-obtained? entities) items)) (defn has-obtained-all-of? [entities items] (every? (partial has-obtained? entities) items)) (defn has-one-of? [entities items] (if (map? entities) (seq (set/intersection (set (get-in entities [:state :inventory])) (set items))) (seq (set/intersection (set (get-in @entities [:state :inventory])) (set items))))) (defmacro run-action [entities & forms] `(let [c# (chan)] (do (put! (get-in (deref ~entities) [:actions :channel]) (reify IAction (get-channel [_] c#) ~@forms)) (reset! ~entities ( entities :room :scale-fn) entity (assoc entity :x x :y y) entity (if update-baseline? (assoc entity :baseline (- 240 y)) entity)] (if (:scaled entity) (assoc entity :scale-x (scale-fn [x y]) :scale-y (scale-fn [x y])) entity))) (defn find-animation [entity anim] (if (instance? Animation anim) anim (or (get-in entity [(:facing entity) anim]) (get entity anim)))) (defn start-animation ([entity anim] (start-animation {:total-time 0} entity anim)) ([screen entity anim] (let [new-anim (find-animation entity anim)] (if (and anim (not= new-anim (:anim entity))) (assoc entity :anim new-anim :anim-start (:total-time screen)) entity)))) (defn stop [screen entities target-id & {:keys [face]}] (update-in entities [:room :entities target-id] (comp #(start-animation screen % :stand) (if face #(assoc % :facing face) identity)))) (defn walk-straight-to [entities target-id [final-x final-y] & {:keys [update-baseline? face speed anim override-dir stop?]}] (let [{start-x :x start-y :y} (get-in @entities [:room :entities target-id]) final-x (int final-x) final-y (int final-y) update-baseline? (if (nil? update-baseline?) true update-baseline?)] (run-action entities (begin [this screen entities] entities) (continue [this screen entities] (let [{from-x :x from-y :y :keys [left right scale-x] :as target-entity} (get-in entities [:room :entities target-id])] (let [delta-x (- final-x from-x) delta-y (- final-y from-y) distance (utils/dist from-x from-y final-x final-y) speed (* (or scale-x 1.0) (or speed 1.5)) moved-x (if (= 0.0 distance) 0 (* speed (/ delta-x distance))) moved-y (if (= 0.0 distance) 0 (* speed (/ delta-y distance)))] (if (< distance speed) (-> entities (assoc-in [:room :entities target-id :x] final-x) (assoc-in [:room :entities target-id :y] final-y)) (update-in entities [:room :entities target-id] #(start-animation screen (assoc (jump-to screen entities % [(+ moved-x from-x) (+ moved-y from-y)] update-baseline?) :facing (or override-dir (cond (< delta-x 0) :left (> delta-x 0) :right :else (:facing %)))) (or anim :walk) )))))) (done? [this screen entities] (let [{from-x :x from-y :y :keys [left right anim] :as target-entity} (get-in entities [:room :entities target-id])] (< (utils/dist final-x final-y from-x from-y) 1))) (terminate [this screen entities] (if (or (nil? stop?) stop?) (stop screen entities target-id :face face) entities)) (can-skip? [this screen entities] false)))) (defn play-animation [entities target-id anim & {:keys [stop?]}] (run-action entities (begin [this screen entities] (update-in entities [:room :entities target-id] #(start-animation screen % anim) )) (continue [this screen entities] entities) (done? [this screen entities] (animation! (find-animation (get-in entities [:room :entities target-id ]) anim) :is-animation-finished (- (:total-time screen) (get-in entities [:room :entities target-id :anim-start])))) (terminate [this screen entities] (if (or (nil? stop?) stop?) (stop screen entities target-id) (assoc-in entities [:room :entities target-id :anim] nil))) (can-skip? [this screen entities] false))) (defn update-entity [entities target-id f] (run-action entities (begin [this screen entities] (update-in entities [:room :entities target-id] f)) (continue [this screen entities] entities) (done? [this screen entities] true) (terminate [this screen entities] entities) (can-skip? [this screen entities] false))) (defn stop-walking [entities target-id & {:keys [face]}] (run-action entities (begin [this screen entities] (stop screen entities target-id :face face)) (continue [this screen entities] entities) (done? [this screen entities] true) (terminate [this screen entities] entities) (can-skip? [this screen entities] false))) (defn walk-to [entities target-id [final-x final-y] & {:keys [can-skip? face]}] (let [{start-x :x start-y :y} (get-in @entities [:room :entities target-id]) final-x (int final-x) final-y (int final-y) path (vec (take-nth 5 (advent.pathfind/visit-all (:collision (:room @entities)) [(int start-x) (int start-y)] [final-x final-y]))) path (if (seq path) (conj path [final-x final-y]) []) targets-left (atom path)] (if (seq path) (run-action entities (begin [this screen entities] entities) (continue [this screen entities] (let [{from-x :x from-y :y :keys [left right scale-x] :as target-entity} (get-in entities [:room :entities target-id]) [[target-x target-y] remainder] @targets-left] (let [delta-x (- target-x from-x) delta-y (- target-y from-y) distance (utils/dist from-x from-y target-x target-y) speed (* (or scale-x 1.0) 1.5) moved-x (if (= 0.0 distance) 0 (* speed (/ delta-x distance))) moved-y (if (= 0.0 distance) 0 (* speed (/ delta-y distance)))] (if (< distance speed) (do (swap! targets-left rest) (-> entities (assoc-in [:room :entities target-id :x] target-x) (assoc-in [:room :entities target-id :y] target-y))) (update-in entities [:room :entities target-id] #(start-animation screen (assoc (jump-to screen entities % [(+ moved-x from-x) (+ moved-y from-y)] true) :facing (cond (< delta-x 0) :left (> delta-x 0) :right :else (:facing %))) :walk )))))) (done? [this screen entities] (let [{from-x :x from-y :y :keys [left right anim] :as target-entity} (get-in entities [:room :entities target-id])] (< (utils/dist final-x final-y from-x from-y) 1))) (terminate [this screen entities] (stop screen entities target-id :face face)) (can-skip? [this screen entities] (or can-skip? false))) @entities))) (defn get-text-duration [text] (* (count (s/split text #" ")) 0.5)) (defn talk [entities target-id text & {:keys [stop? animate? anim]}] (let [initial-time (atom nil) stop? (if (nil? stop?) true stop?) animate? (if (nil? animate?) true animate?)] (run-action entities (begin [this screen entities] (let [_ (swap! initial-time #(or % (:total-time screen))) target-y (get-in entities [:room :entities target-id :y]) target-x (get-in entities [:room :entities target-id :x]) width (or (get-in entities [:room :entities target-id :width]) (.getRegionWidth (get-in entities [:room :entities target-id :object]))) origin-x (get-in entities [:room :entities target-id :origin-x]) target-x (if (nil? origin-x) (+ target-x (/ width 2)) target-x ) height (or (get-in entities [:room :entities target-id :height]) (.getRegionHeight (get-in entities [:room :entities target-id :object]))) scaled (get-in entities [:room :entities target-id :scaled]) scale-fn (get-in entities [:room :scale-fn]) scale (get-in entities [:room :entities target-id :scale-y] 1) height (* scale height)] (screen! dialogue/talking-screen :on-talk :text text :x target-x :y (+ (get-in entities [:room :entities target-id :y]) height) :color (get-in entities [:room :entities target-id :talk-color]) :target-id target-id :scale scale) (if animate? (update-in entities [:room :entities target-id ] #(start-animation screen % (or anim :talk))) entities))) (continue [this screen entities] entities) (done? [this screen entities] (> (- (:total-time screen) @initial-time) (get-text-duration text))) (terminate [this screen entities] (screen! dialogue/talking-screen :stop-talk :target-id target-id) (if stop? (stop screen entities target-id) entities)) (can-skip? [this screen entities] true)))) (defn something-else [zipper] (-> zipper zip/up zip/up)) (defn previous-choices [zipper] (-> zipper zip/up)) (defn nth-child [zipper i] (loop [so-far 0 zipper (zip/down zipper)] (if (= so-far i) zipper (recur (inc so-far) (zip/right zipper))))) (defn make-zipper [tree] (zip/zipper map? (comp #(map second %) #(filter first (partition 2 %)) :choices) (fn [n c] nil) tree)) (defn present-choices [entities choices] (loop [zipper (make-zipper choices)] (let [selected-index (atom nil) node (zip/node zipper) dialogue-choices (filter first (partition 2 (:choices node)))] (run-action entities (begin [this screen entities] (screen! dialogue/choice-screen :on-present-choices :choices dialogue-choices :callback #(reset! selected-index %)) (screen! @(resolve 'advent.screens.scene/scene) :on-deactivate) (-> entities (assoc-in [:cursor :override] nil) (assoc-in [:cursor :current] :main))) (continue [this screen entities] entities) (done? [this screen entities] (not (nil? @selected-index))) (terminate [this screen entities] (screen! @(resolve 'advent.screens.scene/scene) :on-reactivate) entities) (can-skip? [this screen entities] false)) (let [zipper (nth-child zipper @selected-index) node (zip/node zipper)] (when-let [run (:run node)] (run (-> dialogue-choices (nth @selected-index) first))) (when-let [next-choices (:choices node)] (if (fn? next-choices) (recur (next-choices zipper)) (recur zipper))))))) (defn update-entities [entities f] (run-action entities (begin [this screen entities] (f entities)) (continue [this screen entities] entities) (done? [this screen entities] true) (terminate [this screen entities] entities) (can-skip? [this screen entities] false))) (defn begin-animation [entities target-id anim] (run-action entities (begin [this screen entities] (update-in entities [:room :entities target-id] #(start-animation screen % anim))) (continue [this screen entities] entities) (done? [this screen entities] true) (terminate [this screen entities] entities) (can-skip? [this screen entities] false))) (defn update-state [entities f] (update-entities entities #(update-in % [:state] f))) (defn remove-item [entities item] (run-action entities (begin [this screen entities] (-> entities (update-in [:state :inventory] #(remove (partial = item) %)) (assoc-in [:cursor :current] :main))) (continue [this screen entities] entities) (done? [this screen entities] true) (terminate [this screen entities] entities) (can-skip? [this screen entities] false))) (defn play-sound [entities sound-file] (let [m (music sound-file)] (run-action entities (begin [this screen entities] (music! m :play) entities) (continue [this screen entities] entities) (done? [this screen entities] (not (music! m :is-playing))) (terminate [this screen entities] entities) (can-skip? [this screen entities] false)))) (defn give [entities item] (run-action entities (begin [this screen entities] (sound! (sound "pickup.mp3") :play) (-> entities (update-in [:state :inventory] #(conj % item)) (update-in [:state :obtained-items] #(conj % item)) (assoc-in [:cursor :current] ((:all-items entities) item)))) (continue [this screen entities] entities) (done? [this screen entities] true) (terminate [this screen entities] entities) (can-skip? [this screen entities] false))) (defn remove-entity [entities entity] (run-action entities (begin [this screen entities] (update-in entities [:room :entities] #(dissoc % entity))) (continue [this screen entities] entities) (done? [this screen entities] true) (terminate [this screen entities] entities) (can-skip? [this screen entities] false))) (defn add-entity [entities id entity] (run-action entities (begin [this screen entities] (update-in entities [:room :entities] #(assoc % id entity))) (continue [this screen entities] entities) (done? [this screen entities] true) (terminate [this screen entities] entities) (can-skip? [this screen entities] false))) (defn get-music [music time] (if (keyword? music) music (time music))) (defn transition-music [entities new-music] (let [current-volume (atom 1.0)] (run-action entities (begin [this screen entities] (assoc-in entities [:tweens :fade-out-music] (utils/tween :fade-out-music screen [:volume :value] 1.0 0.0 2.0))) (continue [this screen entities] entities) (done? [this screen entities] (nil? (get-in entities [:tweens :fade-out-music]))) (terminate [this screen entities] (music! (utils/get-current-music entities) :stop) (let [entities (-> entities (assoc-in [:music-override :value] new-music) (assoc-in [:volume :value] 1.0))] (music! (utils/get-current-music entities) :set-volume 1.0) (music! (utils/get-current-music entities) :play) entities)) (can-skip? [this screen entities] false)))) (defn transition-background [entities new-background [x y]] (let [old-music (get-music (get-in @entities [:room :music]) (get-in @entities [:state :time])) new-music (get-music (get-in @entities [:rooms new-background :music]) (get-in @entities [:state :time])) music-changed? (not= old-music new-music)] (run-action entities (begin [this screen entities] (doseq [[k] (get-in entities [:room :timers])] (remove-timer! screen k)) (as-> entities e (assoc-in e [:tweens :fade-out] (utils/tween :fade-out screen [:fade :opacity] 0.0 1.0 0.5)) (if music-changed? (assoc-in e [:tweens :fade-out-music] (utils/tween :fade-out-music screen [:volume :value] 1.0 0.0 0.5)) e) (assoc-in e [:cursor :current] :main))) (continue [this screen entities] (when music-changed? (music! (get-in entities [:musics old-music]) :set-volume (max (- 1.0 (get-in entities [:fade :opacity])) 0.0))) entities) (done? [this screen entities] (>= (get-in entities [:fade :opacity]) 1.0)) (terminate [this screen entities] (if-let [next-time (get-in entities [:state :next-time])] (-> entities (assoc-in [:state :time] next-time) (assoc-in [:state :next-time] nil)) entities)) (can-skip? [this screen entities] false)) (run-action entities (begin [this screen entities] (let [ego (get-in entities [:room :entities :ego]) old-music (get-music (get-in entities [:room :music]) (get-in entities [:state :time])) entities (as-> entities e (assoc-in e [:room] (get-in entities [:rooms new-background])) (assoc-in e [:room :entities :ego] ego) (assoc-in e [:state :last-room] new-background) (assoc-in e [:tweens :fade-in] (utils/tween :fade-in screen [:fade :opacity] 1.0 0.0 0.5)) (if music-changed? (assoc-in e [:tweens :fade-in-music] (utils/tween :fade-in-music screen [:volume :value] 0.0 1.0 0.5)) e)) new-music (get-music (get-in entities [:room :music]) (get-in entities [:state :time])) apply-state (get-in entities [:room :apply-state]) entities (if apply-state (apply-state entities) entities) entities (utils/update-override entities)] (when (not= new-music old-music) (doseq [[k v] (:musics entities) :when (and v (not= new-music k))] (music! v :stop)) (when new-music (music! (get-in entities [:musics new-music]) :set-volume 0) (music! (get-in entities [:musics new-music]) :play))) (-> entities (update-in [:room :entities :ego] #(jump-to screen entities % [x y] true))))) (continue [this screen entities] entities) (done? [this screen entities] (<= (get-in entities [:fade :opacity]) 0.0)) (terminate [this screen entities] (doseq [[k [start time fn]] (get-in entities [:room :timers])] (add-timer! screen k start time)) entities) (can-skip? [this screen entities] false)))) (defn do-dialogue [entities & pairs] (loop [pairs (partition 2 pairs)] (let [[[target line]] pairs next-speaker-is-different (not= target (ffirst (next pairs))) result (talk entities target line :stop? next-speaker-is-different)] (Thread/sleep 200) (if (seq (rest pairs)) (recur (rest pairs)) result)))) (defn respond [entities line & more] (apply do-dialogue entities :ego line more))