(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] (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 [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]}] (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 (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] 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 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?]}] (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]) scale-fn (get-in entities [:room :scale-fn]) scale (scale-fn [target-x target-y]) height (* scale 36)] (screen! dialogue/talking-screen :on-talk :text text :x (get-in entities [:room :entities target-id :x]) :y (+ (get-in entities [:room :entities target-id :y]) height) :target-id target-id :scale scale) (if animate? (update-in entities [:room :entities target-id ] #(start-animation screen % :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) (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-state [entities f] (run-action entities (begin [this screen entities] (update-in entities [:state] f )) (continue [this screen entities] entities) (done? [this screen entities] true) (terminate [this screen entities] entities) (can-skip? [this screen entities] false))) (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 transition-music ([entities new-music] (transition-music entities (get-in @entities [:room :music]) new-music)) ([entities old-music new-music] (let [current-volume (atom 1.0)] (run-action entities (begin [this screen entities] entities) (continue [this screen entities] (let [new-volume (swap! current-volume #(- % 0.01))] (music! (get-in entities [:musics old-music]) :set-volume new-volume)) entities) (done? [this screen entities] (>= 0.1 @current-volume)) (terminate [this screen entities] (music! (get-in entities [:musics old-music]) :stop) (music! (get-in entities [:musics new-music]) :set-volume 1.0) (music! (get-in entities [:musics new-music]) :play) entities) (can-skip? [this screen entities] false))))) (defn transition-background [entities new-background [x y]] (let [old-music (get-in @entities [:room :music]) new-music (get-in @entities [:rooms new-background :music]) music-changed? (not= old-music new-music)] (run-action entities (begin [this screen entities] (-> entities (assoc-in [:transition] (assoc (texture "black.png") :scale-x 20 :scale-y 20 :baseline 9500 :opacity 0.1)))) (continue [this screen entities] (when music-changed? (music! (get-in entities [:musics old-music]) :set-volume (max (- 1.0 (get-in entities [:transition :opacity])) 0.0))) (update-in entities [:transition :opacity] + 0.05)) (done? [this screen entities] (>= (get-in entities [:transition :opacity]) 1.0)) (terminate [this screen entities] 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-in entities [:room :music]) entities (-> entities (assoc-in [:room] (get-in entities [:rooms new-background])) (assoc-in [:room :entities :ego] ego) (assoc-in [:state :last-room] new-background)) new-music (get-in entities [:room :music]) apply-state (get-in entities [:room :apply-state]) entities (if apply-state (apply-state entities) 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] (when music-changed? (music! (get-in entities [:musics new-music]) :set-volume (max (- 1.0 (get-in entities [:transition :opacity])) 0.0))) (update-in entities [:transition :opacity] - 0.075)) (done? [this screen entities] (<= (get-in entities [:transition :opacity]) 0.0)) (terminate [this screen entities] (dissoc entities :transition)) (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))