-(ns tempest
+(ns ^{:doc "
+Publicly exported functions to embed Tempest game in HTML.
+"}
+ tempest
(:require [tempest.levels :as levels]
- [tempest.util :as util]
+ [tempest.draw :as draw]
+ [tempest.core :as c]
[goog.dom :as dom]
- [goog.Timer :as timer]
[goog.events :as events]
- [goog.math :as math]
- [goog.events.KeyHandler :as key-handler]
- [goog.events.KeyCodes :as key-codes]
- [clojure.browser.repl :as repl])
- (:require-macros [tempest.macros :as macros]))
+ [goog.events.KeyHandler :as key-handler]))
-;;
-;;
-;; Rough design notes:
+;; ## Design notes:
;;
;; * Nearly everything is defined with polar coordinates (length and angle)
;; * "Entities" are players, enemies, projectiles
;; The sign of the stride is direction, with positive strides moving out
;; towards the player.
;;
-;; Obscure design oddities:
+;; ## Obscure design oddities:
;;
;; * draw-path can optionally follow, but not draw, the first line of an
;; entity's path. There is a crazy reason for this. The 'center' of
;; in the front center of the ship.
;;
;;
-
-
-(repl/connect "http://localhost:9000/repl")
-
-;;
-;; TODO:
+;; ## TODO:
;;
-;; * BUG: there's an extra segment at the top of level 6 with no width
-;; * Bullet updates should check if they hit or passed over an enemy
;; * Flippers should.. flip.
;; * Player should be sucked down the level if a flipper touches him
;; * MOAR ENEMIES
;; * Frame timing, and disassociate movement speed from framerate.
;;
-;; Path that defines player.
-(def ^{:doc "Path, in polar coordinates, describing the player's ship."}
- *player-path*
- [[40 90]
- [44 196]
- [27 333]
- [17 135]
- [30 11]
- [30 349]
- [17 225]
- [27 27]
- [44 164]])
-
-(def ^{:doc "Boolean flag to mark if the game is paused."}
- *paused* (atom false))
-
-(defn round-path-math
- "Rounds all numbers in a path (vector of 2-tuples) to nearest integer."
- [path]
- (map (fn [coords]
- [(js/Math.round (first coords))
- (js/Math.round (peek coords))])
- path))
-
-(defn round-path-hack
- "Rounds all numbers in a path (vector of 2-tuples) to nearest integer.
- ONLY WORKS WITH POSITIVE NUMBERS. Faster than round-path-math."
- [path]
- (map (fn [[x y]]
- [(js* "~~" (+ 0.5 x))
- (js* "~~" (+ 0.5 y))])
- path))
-
-(defn round
- "Perform quick rounding of given number. ONLY WORKS WITH POSITIVE NUMBERS."
- [num]
- (js* "~~" (+ 0.5 num)))
-
-;; Use round-path-hack for now, since it's theoretically faster
-(def round-path round-path-hack)
-
-(defn flipper-path-with-width
- "Returns a path to draw a 'flipper' enemy with given width."
- [width]
- (let [r (/ width (js/Math.cos (util/deg-to-rad 16)))]
- [[0 0]
- [(/ r 2) 16]
- [(/ r 4) 214]
- [(/ r 4) 326]
- [r 164]
- [(/ r 4) 326]
- [(/ r 4) 214]
- [(/ r 2) 16]]))
-
-
-(defn projectile-path-with-width
- "Returns a path to draw a projectile with the given width."
- [width]
- (let [r (/ width (* 2 (js/Math.cos (util/deg-to-rad 45))))
- midheight (* r (js/Math.sin (util/deg-to-rad 45)))]
- [[midheight 270]
- [r 45]
- [r 135]
- [r 225]
- [r 315]]))
-
-(defn build-projectile
- "Returns a dictionary describing a projectile (bullet) on the given level,
- in the given segment, with a given stride (steps per update to move, with
- negative meaning in and positive meaning out), and given step to start on."
- [level seg-idx stride & {:keys [step] :or {step 0}}]
- {:step step
- :stride stride
- :segment seg-idx
- :level level
- :path-fn projectile-path-on-level
- })
-
-(defn build-enemy
- "Returns a dictionary describing an enemy on the given level and segment,
- and starting on the given step. Step defaults to 0 (innermost step of
- level) if not specified. TODO: Only makes flippers."
- [level seg-idx & {:keys [step] :or {step 0}}]
- {:step step
- :stride 1
- :segment seg-idx
- :path-fn flipper-path-on-level
- :level level
- :hits-remaining 1})
-
-(defn build-player
- "Returns a dictionary describing a player on the given level and segment."
- [level seg-idx]
- {:segment seg-idx
- :level level
- :step (:steps level)
- :bullet-stride -5})
-
-(defn entity-next-step
- "Returns the next step position of given entity, taking into account
- minimum and maximum positions of the level."
- [entity]
- (let [stride (:stride entity)
- maxstep (:steps (:level entity))
- newstep (+ stride (:step entity))]
- (cond
- (> newstep maxstep) maxstep
- (< newstep 0) 0
- :else newstep)))
-
-(defn test-entity-next-step []
- (and
- (= 11 (entity-next-step (build-enemy level 0 :step 10)))
- (= 6 (entity-next-step (build-projectile level 0 -4 :step 10)))
- (= 0 (entity-next-step (build-projectile level 0 -4 :step 0)))
- (= 0 (entity-next-step (build-projectile level 0 -4 :step 2)))
- (= 100 (entity-next-step (build-projectile level 0 4 :step 100)))
- (= 100 (entity-next-step (build-projectile level 0 4 :step 98)))))
-
-(defn update-entity-position!
- "Return entity updated with a new position based on its current location and
- stride. Won't go lower than 0, or higher than the maximum steps of the
- level."
- [entity]
- (assoc entity :step (entity-next-step entity)))
-
-(defn test-update-entity-position! []
- (and
- (= 11 (:step (update-entity-position! (build-enemy level 0 :step 10))))
- (= 5 (:step (update-entity-position!
- (build-projectile level 0 -5 :step 10))))
- (= 15 (:step (update-entity-position!
- (build-projectile level 0 5 :step 10))))
- (= 0 (:step (update-entity-position!
- (build-projectile level 0 -5 :step 0))))
- (= 100 (:step (update-entity-position!
- (build-projectile level 0 5 :step 100))))))
-
-(defn update-entity-list
- "Recursively call update-entity-position! on all entities in list."
- [entity-list]
- ((fn [oldlist newlist]
- (let [entity (first oldlist)]
- (if (empty? entity)
- newlist
- (recur (rest oldlist)
- (cons (update-entity-position! entity) newlist))))
- ) entity-list []))
-
-(defn scale-polar-coord
- "Return a polar coordinate with the first element (radius) scaled using
- the function scalefn"
- [scalefn coord]
- [(scalefn (first coord)) (peek coord)])
-
-(defn rotate-path
- "Add angle to all polar coordinates in path."
- [angle path]
- (map (fn [coords]
- [(first coords)
- (mod (+ angle (peek coords)) 360)])
- path))
-
-(defn scale-path
- "Multiply all lengths of polar coordinates in path by scale."
- [scale path]
- (map (fn [coords]
- [(* scale (first coords))
- (peek coords)])
- path))
-
-(defn polar-extend
- "Add 'length' to radius of polar coordinate."
- [length coord]
- [(+ length (first coord))
- (peek coord)])
-
-(defn path-extend
- "Add 'length' to all polar coordinates in path"
- [length path]
- (map #(polar-extend length %) path))
-
-(defn polar-entity-coord
- "Returns current polar coordinates to the entity."
- [entity]
- (let [steplen (step-length-segment-midpoint (:level entity)
- (:segment entity))
- offset (* steplen (:step entity))
- midpoint (segment-midpoint (:level entity) (:segment entity))]
- (polar-extend offset midpoint)))
-
-(defn enemy-angle
- "Returns the angle from origin that the enemy needs to be rotated to
- appear in the correct orientation at its current spot on the level.
- In reality, it returns the angle of the line that traverses the segment
- across the midpoint of the enemy. TODO: This should be renamed to
- 'entity-angle', it works with anything on the board."
- [enemy]
- (let [edges (polar-lines-for-segment (:level enemy)
- (:segment enemy)
- false)
- edge-steps (step-lengths-for-segment-lines (:level enemy)
- (:segment enemy))
- offset0 (* (first edge-steps) (:step enemy))
- offset1 (* (peek edge-steps) (:step enemy))
- point0 (polar-extend offset0 (first edges))
- point1 (polar-extend offset1 (peek edges))]
- (util/rad-to-deg
- (apply js/Math.atan2
- (vec (reverse (map - (polar-to-cartesian-coords point0)
- (polar-to-cartesian-coords point1))))))))
-
-
-(defn enemy-desired-width
- "Returns how wide the given enemy should be drawn to span the full width
- of its current location. In reality, that means returning the length of
- the line that spans the level segment, cutting through the enemy's
- midpoint. TODO: rename this entity-desired-width."
- [enemy]
- (let [edges (polar-lines-for-segment (:level enemy)
- (:segment enemy)
- false)
- edge-steps (step-lengths-for-segment-lines (:level enemy)
- (:segment enemy))
- offset0 (* (first edge-steps) (:step enemy))
- offset1 (* (peek edge-steps) (:step enemy))
- point0 (polar-extend offset0 (first edges))
- point1 (polar-extend offset1 (peek edges))]
- (polar-distance point0 point1)))
-
-(defn player-path-on-level
- "Returns the path of polar coordinates to draw the player correctly at its
- current location. It corrects for size and angle."
- [player]
- (let [coord (polar-entity-coord player)]
- (scale-path 0.6 (rotate-path
- (enemy-angle player)
- *player-path*))))
-
-(defn flipper-path-on-level
- "Returns the path of polar coordinates to draw a flipper correctly at its
- current location. It corrects for size and angle."
- [flipper]
- (let [coord (polar-entity-coord flipper)]
- (rotate-path
- (enemy-angle flipper)
- (flipper-path-with-width (* 0.8 (enemy-desired-width flipper))))))
-
-(defn projectile-path-on-level
- "Returns the path of polar coordinates to draw a projectile correctly at its
- current location. It corrects for size and angle."
- [projectile]
- (let [coord (polar-entity-coord projectile)]
- (rotate-path
- (enemy-angle projectile)
- (projectile-path-with-width (* 0.3 (enemy-desired-width projectile))))))
-
-(defn add-sub
- "Given two polar coordinates, returns one polar coordinate with the first
- elements (radii) summed, and the second elements (angles) subtracted.
- i.e. [r1+r0, th1-th0]. This is used to move point0 to be relative to
- point1."
- [point0 point1]
- [(+ (first point1) (first point0))
- (- (peek point1) (peek point0))])
-
-(defn rebase-origin
- "Return cartesian coordinate 'point' in relation to 'origin'."
- [point origin]
- (add-sub point origin))
-
-(defn polar-to-cartesian-centered
- "Converts a polar coordinate (r,theta) into a cartesian coordinate (x,y)
- centered on in a rectangle with given width and height."
- [point {width :width height :height}]
- (rebase-origin (polar-to-cartesian-coords point) [(/ width 2) (/ height 2)]))
-
-(defn draw-path
- "Draws a 'path', a vector of multiple polar coordinates, on an HTML5 2D
- drawing canvas.
-
- context -- The '2D Context' of an HTML5 canvas element
- origin -- The point (cartesian coordinate) to start drawing from
- vecs -- Vector of polar coordinates to draw
- skipfirst? -- Whether the first line described by vecs should be drawn. If
- no, the first line can be used to offset the path, in effect changing the
- 'midpoint' of the entity being drawn. If yes, the 'midpoint' of the
- object is the first vertex from which the first line is drawn.
- "
- [context origin vecs skipfirst?]
- (do
- (.moveTo context (first origin) (peek origin))
- ((fn [origin vecs skip?]
- (if (empty? vecs)
- nil
- (let [line (first vecs)
- point (rebase-origin (polar-to-cartesian-coords line) origin)]
- (if-not skip?
- (.lineTo context (first point) (peek point))
- (.moveTo context (first point) (peek point)))
- (recur point (next vecs) false))))
- origin vecs skipfirst?)
- (.stroke context)))
-
-
-
-(defn polar-to-cartesian-coords
- "Converts polar coordinates to cartesian coordinates. If optional length-fn
- is specified, it is applied to the radius first."
- ([[r angle]] [(math/angleDx angle r) (math/angleDy angle r)])
- ([[r angle] length-fn]
- (let [newr (length-fn r)]
- [(math/angleDx angle newr) (math/angleDy angle newr)])
- )
- )
-
-(defn polar-distance
- "Returns distance between to points specified by polar coordinates."
- [[r0 theta0] [r1 theta1]]
- (js/Math.sqrt
- (+
- (js/Math.pow r0 2)
- (js/Math.pow r1 2)
- (* -2 r0 r1 (js/Math.cos (util/deg-to-rad (- theta1 theta0)))))))
-
-(defn polar-midpoint-r
- "Returns the radius to the midpoint of a line drawn between two polar
- coordinates."
- [[r0 theta0] [r1 theta1]]
- (js/Math.round
- (/
- (js/Math.sqrt
- (+
- (js/Math.pow r0 2)
- (js/Math.pow r1 2)
- (* 2 r0 r1 (js/Math.cos (util/deg-to-rad (- theta1 theta0))))))
- 2)))
-
-(defn polar-midpoint-theta
- "Returns the angle to the midpoint of a line drawn between two polar
- coordinates."
- [[r0 theta0] [r1 theta1]]
- (js/Math.round
- (mod
- (+ (util/rad-to-deg
- (js/Math.atan2
- (+
- (* r0 (js/Math.sin (util/deg-to-rad theta0)))
- (* r1 (js/Math.sin (util/deg-to-rad theta1))))
- (+
- (* r0 (js/Math.cos (util/deg-to-rad theta0)))
- (* r1 (js/Math.cos (util/deg-to-rad theta1))))
- ))
- 360) 360)))
-
-(defn polar-midpoint
- "Returns polar coordinate representing the midpoint between the two
- points specified. This can be used to draw a line down the middle
- of a level segment -- the line that entities should follow."
- [point0 point1]
- [(polar-midpoint-r point0 point1)
- (polar-midpoint-theta point0 point1)]
- )
-
-(defn segment-midpoint
- "Given a level and a segment index, returns the midpoint of the segment.
- scaled? determines whether it gives you the inner (false) or outer (true)
- point."
- [level seg-idx scaled?]
- (apply polar-midpoint
- (polar-lines-for-segment level seg-idx scaled?)))
-
-
-(defn polar-lines-for-segment
- "Returns vector [line0 line1], where lineN is a polar coordinate describing
- the line from origin (canvas midpoint) that would draw the edges of a level
- segment.
-
- 'scaled?' sets whether you want the unscaled, inner point, or the
- outer point scaled with the level's scale function.
-
- To actually draw a level's line, you would move to the unscaled point
- without drawing, and then draw to the scaled point.
- "
- [level seg-idx scaled?]
- (let [[seg0 seg1] (get (:segments level) seg-idx)
- line0 (get (:lines level) seg0)
- line1 (get (:lines level) seg1)]
- (if (true? scaled?)
- [(scale-polar-coord (:length-fn level) line0)
- (scale-polar-coord (:length-fn level) line1)]
- [line0 line1]
- )))
-
-(defn rectangle-for-segment
- "Returns vector [[x0 y0] [x1 y1] [x2 y2] [x3 y3]] describing segment's
- rectangle in cartesian coordinates."
- [level seg-idx]
- (let [[seg0 seg1] (get (:segments level) seg-idx)
- line0 (get (:lines level) seg0)
- line1 (get (:lines level) seg1)]
- [(polar-to-cartesian-coords line0)
- (polar-to-cartesian-coords line0 (:length-fn level))
- (polar-to-cartesian-coords line1 (:length-fn level))
- (polar-to-cartesian-coords line1)]
- ))
-
-(defn point-to-canvas-coords
- "Center a cartesian coordinate centered around (0,0) to be centered around
- the middle of a rectangle with the given width and height. It inverts y,
- assuming that the input y is 'up', and in the output y is 'down', as is
- the case with an HTML5 canvas."
- [{width :width height :height} p]
- (let [xmid (/ width 2)
- ymid (/ height 2)]
- [(+ (first p) xmid) (- ymid (peek p))]
- ))
-
-(defn rectangle-to-canvas-coords
- "Given a rectangle (vector of 4 cartesian coordinates) centered around (0,0),
- this function shifts them to be centered around the center of an HTML5
- canvas with the :width and :height set in dims."
- [dims rect]
- (map #(point-to-canvas-coords dims %) rect)
- )
-
-
-(defn draw-rectangle
- "Draws a rectangle (4 cartesian coordinates in a vector) on the 2D context
- of an HTML5 canvas."
- [context [p0 & points]]
- (.moveTo context (first p0) (peek p0))
- (doseq [p points]
- (.lineTo context (first p) (peek p)))
- (.lineTo context (first p0) (peek p0))
- (.stroke context)
- )
-
-(defn step-length-segment-midpoint
- "Finds the 'step length' of a line through the middle of a level's segment.
- This is how many pixels an entity should move per update to travel one
- step."
- [level seg-idx]
- (/
- (-
- (first (segment-midpoint level seg-idx true))
- (first (segment-midpoint level seg-idx false)))
- (:steps level)))
-
-(defn step-length-segment-edge
- "Finds the 'step length' of a line along the edge of a level's segment."
- [level line]
- (/
- (-
- ((:length-fn level) (first line))
- (first line))
- (:steps level)))
-
-(defn step-length-line
- "Finds the 'step length' of an arbitrary line on the given level."
- [level point0 point1]
- (js/Math.abs
- (/
- (-
- (first point0)
- (first point1))
- (:steps level))))
-
-(defn step-lengths-for-segment-lines
- "Returns a vector [len0 len1] with the 'step length' for the two edge
- lines that mark the boundaries of the given segment."
- [level seg-idx]
- (let [coords (concat (polar-lines-for-segment level seg-idx false)
- (polar-lines-for-segment level seg-idx true))
- line0 (take-nth 2 coords)
- line1 (take-nth 2 (rest coords))]
- [(apply #(step-length-line level %1 %2) line0)
- (apply #(step-length-line level %1 %2) line1)]))
-
-(defn entity-between-steps
- "Returns true of entity is on seg-idx, and between steps step0 and step1,
- inclusive."
- [seg-idx step0 step1 entity]
- (let [min (min step0 step1)
- max (max step0 step1)]
- (and
- (= (:segment entity) seg-idx)
- (>= (:step entity) min)
- (<= (:step entity) max))))
-
-(defn test-entity-between-steps []
- (and
- (true? (entity-between-steps 0 0 10 (build-enemy level 0 :step 5)))
- (false? (entity-between-steps 0 0 10 (build-enemy level 0 :step 15)))
- (false? (entity-between-steps 0 0 10 (build-enemy level 1 :step 5)))
- (false? (entity-between-steps 5 10 20 (build-enemy level 5 :step 5)))
- (true? (entity-between-steps 5 10 20 (build-enemy level 5 :step 15)))
- ))
-
-
-(defn projectiles-after-collision
- "Given an entity and a list of projectiles, returns the entity and updated
- list of projectiles after collisions. The entity's hits-remaining counter
- is decremented on a collision, and the projectile is removed."
- [entity projectile-list]
- ((fn [entity projectiles-in projectiles-out was-hit?]
- (if (empty? projectiles-in)
- {:entity entity :projectiles projectiles-out :was-hit? was-hit?}
- (let [bullet (first projectiles-in)
- collision? (entity-between-steps
- (:segment bullet)
- (:step bullet)
- (entity-next-step bullet)
- entity)]
- (if collision?
- (recur (decrement-enemy-hits entity)
- nil
- (concat projectiles-out (rest projectiles-in))
- true)
- (recur entity
- (rest projectiles-in)
- (cons bullet projectiles-out)
- was-hit?)))))
- entity projectile-list '() false))
-
-(defn test-projectiles-after-collision []
- (let [level (get levels/*levels* 4)
- projectiles (list (build-projectile level 0 -1 :step 9)
- (build-projectile level 0 -5 :step 9)
- (build-projectile level 0 1 :step 9)
- (build-projectile level 0 2 :step 9)
- (build-projectile level 0 5 :step 20)
- (build-projectile level 5 2 :step 9))
- result (projectiles-after-collision
- (build-enemy level 0 :step 10)
- projectiles)]
- (println (str "Projectiles: "
- (pr-str (count (:projectiles result)))
- " of "
- (pr-str (count projectiles))))
- (println (str "Hits left: " (pr-str (:hits-remaining (:entity result)))))
- (println (str "Was hit: " (pr-str (:was-hit? result))))
- ))
-
-(defn entities-after-collisions
- "Given a list of entities and a list of projectiles, returns the lists
- with entity hit counts updated, entities removed if they have no hits
- remaining, and collided projectiles removed.
-
- See projectiles-after-collision, which is called for each entity in
- entity-list."
- [entity-list projectile-list]
- ((fn [entities-in entities-out projectiles-in]
- (if (empty? entities-in)
- {:entities entities-out :projectiles projectiles-in}
- (let [{entity :entity projectiles :projectiles was-hit? :was-hit?}
- (projectiles-after-collision (first entities-in)
- projectiles-in)]
- (if (and was-hit? (<= (:hits-remaining entity) 0))
- (recur (rest entities-in)
- entities-out
- projectiles)
- (recur (rest entities-in)
- (cons entity entities-out)
- projectiles)))))
- entity-list '() projectile-list))
-
-(defn test-entities-after-collisions []
- (let [level (get levels/*levels* 4)
- enemies (list (build-enemy level 0 :step 10)
- (build-enemy level 1 :step 20)
- (build-enemy level 2 :step 30)
- (build-enemy level 3 :step 40)
- (build-enemy level 4 :step 50)
- (build-enemy level 5 :step 60))
- projectiles (list (build-projectile level 0 -5 :step 9)
- (build-projectile level 0 5 :step 9)
- (build-projectile level 1 -5 :step 19)
- (build-projectile level 1 5 :step 19)
- (build-projectile level 3 -5 :step 35)
- (build-projectile level 3 5 :step 35))
- result (entities-after-collisions enemies projectiles)]
- (println (str "Entities: "
- (pr-str (count (:entities result)))
- " of "
- (pr-str (count enemies))))
- (println (str "Projectiles: "
- (pr-str (count (:projectiles result)))
- " of "
- (pr-str (count projectiles))))
- ))
-
-
-
-(defn collisions-with-projectile
- "Returns map with keys true and false. Values under true key have or
- will collide with bullet in the next bullet update. Values under the
- false key will not."
- [enemy-list bullet]
- (group-by (partial entity-between-steps
- (:segment bullet)
- (:step bullet)
- (entity-next-step bullet))
- enemy-list))
-
-
-(defn decrement-enemy-hits
- "Decrement hits-remaining count on given enemy."
- [enemy]
- (assoc enemy :hits-remaining (dec (:hits-remaining enemy))))
-
-
-
-;;
-;; Old, replaced collision detection code
-;;
-(defn remove-collided-enemies!
- [bullet]
- (def *enemy-list*
- (atom (get (collisions-with-projectile @*enemy-list* bullet) false))))
-(defn collisions-with-projectile-list
- "Calls collisions-with-projectile for every projectile in bullet-list.
- Returns a dictionary with true key for all enemies involved in a collision,
- and a false key for all unaffected enemies."
- [enemy-list bullet-list]
- (doall (map remove-collided-enemies! bullet-list)))
-
-
-
-
-
-(defn draw-line
- "Draws a line on the given 2D context of an HTML5 canves element, between
- the two given cartesian coordinates."
- [context point0 point1]
- (.moveTo context (first point0) (peek point0))
- (.lineTo context (first point1) (peek point1))
- (.stroke context))
-
-
-(defn draw-player
- "Draws a player, defined by the given path 'player', on the 2D context of
- an HTML5 canvas, with :height and :width specified in dims, and on the
- given level."
- [context dims level player]
- (doseq []
- (.beginPath context)
- (draw-path context
- (vec (map js/Math.round
- (polar-to-cartesian-centered
- (polar-entity-coord player)
- dims)))
- (round-path (player-path-on-level player))
- true)
- (.closePath context)))
-
-(defn draw-entities
- "Draws all the entities, defined by paths in 'entity-list', on the 2D context
- of an HTML5 canvas, with :height and :width specified in dims, and on the
- given level."
- [context dims level entity-list]
- (doseq [entity entity-list]
- (.beginPath context)
- (draw-path context
- (polar-to-cartesian-centered (polar-entity-coord entity) dims)
- (round-path ((:path-fn entity) entity))
- true)
- (.closePath context)))
-
-(defn draw-enemies
- "Draws all the enemies, defined by paths in 'enemy-list', on the 2D context
- of an HTML5 canvas, with :height and :width specified in dims, and on the
- given level. TODO: This only draws flippers."
- [context dims level]
- (doseq [enemy @*enemy-list*]
- (.beginPath context)
- (draw-path context
- (polar-to-cartesian-centered (polar-entity-coord enemy) dims)
- (flipper-path-on-level enemy)
- true)
- (.closePath context)))
-
-(defn draw-board
- "Draws a level on a 2D context of an HTML5 canvas with :height and :width
- specified in dims."
- [context dims level]
- (doseq []
- (.beginPath context)
- (doseq [idx (range (count (:segments level)))]
- (draw-rectangle
- context
- (round-path (rectangle-to-canvas-coords
- dims (rectangle-for-segment level idx)))))
- (.closePath context)))
-
-(defn projectile-off-level?
- "Returns true if a projectile has reached either boundary of the level."
- [projectile]
- (cond
- (zero? (:step projectile)) true
- (>= (:step projectile) (:steps (:level projectile))) true
- :else false))
-
-
-(defn draw-world
- "Call all of the drawing functions to redraw the scene, and update all
- of the entities on the level."
- [context dims level]
- (doseq []
- (.clearRect context 0 0 (:width dims) (:height dims))
- (comment (.clearRect context
- (/ (:width dims) 4) (/ (:height dims) 4)
- (/ (:width dims) 2) (/ (:height dims) 2)))
- ;;(draw-board context dims level)
- (draw-player context dims level (deref *player*))
- (draw-entities context dims level @*enemy-list*)
- (draw-entities context dims level @*projectile-list*)
-
- (when (not @*paused*)
- (let [new-entities (entities-after-collisions @*enemy-list*
- @*projectile-list*)]
- (def *projectile-list* (atom (:projectiles new-entities)))
- (def *enemy-list* (atom (:entities new-entities))))
-
- (def *projectile-list* (atom (update-entity-list @*projectile-list*)))
- (def *projectile-list* (atom (remove projectile-off-level?
- @*projectile-list*)))
- (def *enemy-list* (atom (update-entity-list @*enemy-list*)))
- (*animMethod* #(draw-world context dims level)))
-
- (def *frame-count* (atom (inc @*frame-count*)))
- (when (= 20 @*frame-count*)
- (let [fps (/ (* 1000 @*frame-count*)
- (- (goog.now) @*frame-time*))]
- (dom/setTextContent (dom/getElement "fps")
- (str "FPS: " (pr-str (js/Math.round fps)))))
-
- (def *frame-count* (atom 0))
- (def *frame-time* (atom (goog.now))))))
-
-(defn add-player-projectile!
- "Add a new projectile to the global list of live projectiles, originating
- from the given player, on the segment he is currently on."
- [player]
- (let [level (:level player)
- seg-idx (:segment player)
- stride (:bullet-stride player)
- step (:steps level)]
- (reset! *projectile-list*
- (conj @*projectile-list*
- (build-projectile level seg-idx stride :step step)))))
-
-(defn segment-player-left
- "Returns the segment to the left of the player. Loops around the level
- on connected levels, and stops at 0 on unconnected levels."
- [player]
- (let [level (:level player)
- seg-max (dec (count (:segments level)))
- cur-seg (:segment player)
- loops? (:loops? level)
- new-seg (dec cur-seg)]
- (if (< new-seg 0)
- (if loops? seg-max 0)
- new-seg)))
-
-
-(defn segment-player-right
- "Returns the segment to the right of the player. Loops around the level
- on connected levels, and stops at max on unconnected levels."
- [player]
- (let [level (:level player)
- seg-max (dec (count (:segments level)))
- cur-seg (:segment player)
- loops? (:loops? level)
- new-seg (inc cur-seg)]
- (if (> new-seg seg-max)
- (if loops? 0 seg-max)
- new-seg)))
-
-(defn set-global-player-segment!
- "Sets global *player*'s segment key to a new value."
- [seg-idx]
- (reset! *player* (assoc @*player* :segment seg-idx)))
-
-(defn keypress
- "Respond to keyboard key presses."
- [event]
- (let [player @*player*
- key (.-keyCode event)]
- (condp = key
- key-codes/RIGHT (set-global-player-segment!
- (segment-player-right player))
- key-codes/LEFT (set-global-player-segment!
- (segment-player-left player))
- key-codes/SPACE (add-player-projectile! player)
- key-codes/ESC (def *paused* (atom (not @*paused*)))
- nil
- )))
-
-(defn animationFrameMethod []
- "Returns a callable javascript function to schedule a frame to be drawn.
- Tries to use requestAnimationFrame, or the browser-specific version of
- it that is available. Falls back on setTimeout if requestAnimationFrame
- is not available on player's browser.
-
- requestAnimationFrame tries to figure out a consistent framerate based
- on how long frame takes to render.
-
- The setTimeout fail-over is hard-coded to attempt 30fps.
- "
- (let [window (dom/getWindow)
- names ["requestAnimationFrame"
- "webkitRequestAnimationFrame"
- "mozRequestAnimationFrame"
- "oRequestAnimationFrame"
- "msRequestAnimationFrame"]
- options (map (fn [name] #(aget window name)) names)]
- ((fn [[current & remaining]]
- (cond
- (nil? current) #((.-setTimeout window) % (/ 1000 30))
- (fn? (current)) (current)
- :else (recur remaining)))
- options)))
-
-(def ^{:doc
- "Stores the frame-scheduling function for the current browser.
- This function should be called at the end of each frame to
- schedule the next frame to be drawn at the appropriate time."}
- *animMethod* (animationFrameMethod))
-
(defn ^:export canvasDraw
- "Begins a camge of tempest. 'level' specified as a string representation
+ "Begins a game of tempest. 'level' specified as a string representation
of an integer."
[level]
(let [document (dom/getDocument)
- timer (goog.Timer. 20)
level (get levels/*levels* (- (js/parseInt level) 1))
canvas (dom/getElement "canv-fg")
context (.getContext canvas "2d")
handler (goog.events.KeyHandler. document)
dims {:width (.-width canvas) :height (.-height canvas)}]
- (.log js/console (str "Animation function: " (pr-str *animMethod*)))
-
- (draw-board bgcontext dims level)
-
- (def *frame-count* (atom 0))
- (def *frame-time* (atom (goog.now)))
-
- (def *enemy-list*
- (atom
- (list
- (build-enemy level 0 :step 0)
- (build-enemy level 1 :step 0)
- (build-enemy level 1 :step 10)
- (build-enemy level 1 :step 20)
- (build-enemy level 1 :step 30)
- (build-enemy level 1 :step 40)
- (build-enemy level 1 :step 50)
- (build-enemy level 1 :step 60)
- (build-enemy level 1 :step 70)
- (build-enemy level 3 :step 0)
- (build-enemy level 3 :step 10)
- (build-enemy level 3 :step 20)
- (build-enemy level 3 :step 30)
- (build-enemy level 3 :step 40)
- (build-enemy level 3 :step 50)
- (build-enemy level 3 :step 60)
- (build-enemy level 3 :step 70)
- (build-enemy level 7 :step 0)
- (build-enemy level 7 :step 10)
- (build-enemy level 7 :step 20)
- (build-enemy level 7 :step 30)
- (build-enemy level 7 :step 40)
- (build-enemy level 7 :step 50)
- (build-enemy level 8 :step 0)
- (build-enemy level 8 :step 10)
- (build-enemy level 8 :step 20)
- (build-enemy level 8 :step 30)
- (build-enemy level 8 :step 40)
- (build-enemy level 8 :step 50)
- (build-enemy level 8 :step 60)
- (build-enemy level 11 :step 10))))
- (def *player*
- (atom (doall (build-player level 7))))
- (def *projectile-list*
- (atom (list)))
+ (.log js/console (str "Animation function: " (pr-str c/*animMethod*)))
+
+ (reset! c/*player* (c/build-player level 7))
+ (reset! c/*enemy-list*
+ (list
+ (c/build-enemy level 0 :step 0)
+ (c/build-enemy level 1 :step 0)
+ (c/build-enemy level 1 :step 10)
+ (c/build-enemy level 1 :step 20)
+ (c/build-enemy level 1 :step 30)
+ (c/build-enemy level 1 :step 40)
+ (c/build-enemy level 1 :step 50)
+ (c/build-enemy level 1 :step 60)
+ (c/build-enemy level 1 :step 70)
+ (c/build-enemy level 3 :step 0)
+ (c/build-enemy level 3 :step 10)
+ (c/build-enemy level 3 :step 20)
+ (c/build-enemy level 3 :step 30)
+ (c/build-enemy level 3 :step 40)
+ (c/build-enemy level 3 :step 50)
+ (c/build-enemy level 3 :step 60)
+ (c/build-enemy level 3 :step 70)
+ (c/build-enemy level 7 :step 0)
+ (c/build-enemy level 7 :step 10)
+ (c/build-enemy level 7 :step 20)
+ (c/build-enemy level 7 :step 30)
+ (c/build-enemy level 7 :step 40)
+ (c/build-enemy level 7 :step 50)
+ (c/build-enemy level 8 :step 0)
+ (c/build-enemy level 8 :step 10)
+ (c/build-enemy level 8 :step 20)
+ (c/build-enemy level 8 :step 30)
+ (c/build-enemy level 8 :step 40)
+ (c/build-enemy level 8 :step 50)
+ (c/build-enemy level 8 :step 60)
+ (c/build-enemy level 11 :step 10)))
+
- (*animMethod* #(draw-world context dims level))
-
- (events/listen handler "key" (fn [e] (keypress e)))))
+ (draw/draw-board bgcontext dims level)
+ (c/*animMethod* #(c/update-game! context dims level))
+ (events/listen handler "key" (fn [e] (c/keypress e)))))
+(ns ^{:doc "
+Functions related to the game of tempest, and game state.
+
+Functions in this module create the game state, and modify it based on
+player actions or time. This includes management of entities such as
+the player's ship, enemies, and projectiles.
+"}
+ tempest.core
+ (:require [tempest.levels :as levels]
+ [tempest.util :as util]
+ [tempest.draw :as draw]
+ [tempest.path :as path]
+ [goog.dom :as dom]
+ [goog.events :as events]
+ [goog.events.KeyCodes :as key-codes]
+ [clojure.browser.repl :as repl]))
+
+(repl/connect "http://localhost:9000/repl")
+
+
+(def ^{:doc "Boolean flag to mark if the game is paused."}
+ *paused* (atom false))
+
+
+(defn build-projectile
+ "Returns a dictionary describing a projectile (bullet) on the given level,
+ in the given segment, with a given stride (steps per update to move, with
+ negative meaning in and positive meaning out), and given step to start on."
+ [level seg-idx stride & {:keys [step] :or {step 0}}]
+ {:step step
+ :stride stride
+ :segment seg-idx
+ :level level
+ :path-fn path/projectile-path-on-level
+ })
+
+(defn build-enemy
+ "Returns a dictionary describing an enemy on the given level and segment,
+ and starting on the given step. Step defaults to 0 (innermost step of
+ level) if not specified. TODO: Only makes flippers."
+ [level seg-idx & {:keys [step] :or {step 0}}]
+ {:step step
+ :stride 1
+ :segment seg-idx
+ :path-fn path/flipper-path-on-level
+ :level level
+ :hits-remaining 1
+ :bounding-fn path/flipper-path-bounding-box
+ })
+
+(defn build-player
+ "Returns a dictionary describing a player on the given level and segment."
+ [level seg-idx]
+ {:segment seg-idx
+ :level level
+ :step (:steps level)
+ :bullet-stride -5})
+
+(defn entity-next-step
+ "Returns the next step position of given entity, taking into account
+ minimum and maximum positions of the level."
+ [entity]
+ (let [stride (:stride entity)
+ maxstep (:steps (:level entity))
+ newstep (+ stride (:step entity))]
+ (cond
+ (> newstep maxstep) maxstep
+ (< newstep 0) 0
+ :else newstep)))
+
+(defn test-entity-next-step []
+ (and
+ (= 11 (entity-next-step (build-enemy level 0 :step 10)))
+ (= 6 (entity-next-step (build-projectile level 0 -4 :step 10)))
+ (= 0 (entity-next-step (build-projectile level 0 -4 :step 0)))
+ (= 0 (entity-next-step (build-projectile level 0 -4 :step 2)))
+ (= 100 (entity-next-step (build-projectile level 0 4 :step 100)))
+ (= 100 (entity-next-step (build-projectile level 0 4 :step 98)))))
+
+(defn update-entity-position!
+ "Return entity updated with a new position based on its current location and
+ stride. Won't go lower than 0, or higher than the maximum steps of the
+ level."
+ [entity]
+ (assoc entity :step (entity-next-step entity)))
+
+(defn test-update-entity-position! []
+ (and
+ (= 11 (:step (update-entity-position! (build-enemy level 0 :step 10))))
+ (= 5 (:step (update-entity-position!
+ (build-projectile level 0 -5 :step 10))))
+ (= 15 (:step (update-entity-position!
+ (build-projectile level 0 5 :step 10))))
+ (= 0 (:step (update-entity-position!
+ (build-projectile level 0 -5 :step 0))))
+ (= 100 (:step (update-entity-position!
+ (build-projectile level 0 5 :step 100))))))
+
+(defn update-entity-list
+ "Recursively call update-entity-position! on all entities in list."
+ [entity-list]
+ ((fn [oldlist newlist]
+ (let [entity (first oldlist)]
+ (if (empty? entity)
+ newlist
+ (recur (rest oldlist)
+ (cons (update-entity-position! entity) newlist))))
+ ) entity-list []))
+
+
+(defn entity-between-steps
+ "Returns true of entity is on seg-idx, and between steps step0 and step1,
+ inclusive."
+ [seg-idx step0 step1 entity]
+ (let [min (min step0 step1)
+ max (max step0 step1)]
+ (and
+ (= (:segment entity) seg-idx)
+ (>= (:step entity) min)
+ (<= (:step entity) max))))
+
+(defn test-entity-between-steps []
+ (and
+ (true? (entity-between-steps 0 0 10 (build-enemy level 0 :step 5)))
+ (false? (entity-between-steps 0 0 10 (build-enemy level 0 :step 15)))
+ (false? (entity-between-steps 0 0 10 (build-enemy level 1 :step 5)))
+ (false? (entity-between-steps 5 10 20 (build-enemy level 5 :step 5)))
+ (true? (entity-between-steps 5 10 20 (build-enemy level 5 :step 15)))
+ ))
+
+
+(defn projectiles-after-collision
+ "Given an entity and a list of projectiles, returns the entity and updated
+ list of projectiles after collisions. The entity's hits-remaining counter
+ is decremented on a collision, and the projectile is removed."
+ [entity projectile-list]
+ ((fn [entity projectiles-in projectiles-out was-hit?]
+ (if (empty? projectiles-in)
+ {:entity entity :projectiles projectiles-out :was-hit? was-hit?}
+ (let [bullet (first projectiles-in)
+ collision? (entity-between-steps
+ (:segment bullet)
+ (:step bullet)
+ (entity-next-step bullet)
+ entity)]
+ (if collision?
+ (recur (decrement-enemy-hits entity)
+ nil
+ (concat projectiles-out (rest projectiles-in))
+ true)
+ (recur entity
+ (rest projectiles-in)
+ (cons bullet projectiles-out)
+ was-hit?)))))
+ entity projectile-list '() false))
+
+(defn test-projectiles-after-collision []
+ (let [level (get levels/*levels* 4)
+ projectiles (list (build-projectile level 0 -1 :step 9)
+ (build-projectile level 0 -5 :step 9)
+ (build-projectile level 0 1 :step 9)
+ (build-projectile level 0 2 :step 9)
+ (build-projectile level 0 5 :step 20)
+ (build-projectile level 5 2 :step 9))
+ result (projectiles-after-collision
+ (build-enemy level 0 :step 10)
+ projectiles)]
+ (println (str "Projectiles: "
+ (pr-str (count (:projectiles result)))
+ " of "
+ (pr-str (count projectiles))))
+ (println (str "Hits left: " (pr-str (:hits-remaining (:entity result)))))
+ (println (str "Was hit: " (pr-str (:was-hit? result))))
+ ))
+
+(defn entities-after-collisions
+ "Given a list of entities and a list of projectiles, returns the lists
+ with entity hit counts updated, entities removed if they have no hits
+ remaining, and collided projectiles removed.
+
+ See projectiles-after-collision, which is called for each entity in
+ entity-list."
+ [entity-list projectile-list]
+ ((fn [entities-in entities-out projectiles-in]
+ (if (empty? entities-in)
+ {:entities entities-out :projectiles projectiles-in}
+ (let [{entity :entity projectiles :projectiles was-hit? :was-hit?}
+ (projectiles-after-collision (first entities-in)
+ projectiles-in)]
+ (if (and was-hit? (<= (:hits-remaining entity) 0))
+ (recur (rest entities-in)
+ entities-out
+ projectiles)
+ (recur (rest entities-in)
+ (cons entity entities-out)
+ projectiles)))))
+ entity-list '() projectile-list))
+
+(defn test-entities-after-collisions []
+ (let [level (get levels/*levels* 4)
+ enemies (list (build-enemy level 0 :step 10)
+ (build-enemy level 1 :step 20)
+ (build-enemy level 2 :step 30)
+ (build-enemy level 3 :step 40)
+ (build-enemy level 4 :step 50)
+ (build-enemy level 5 :step 60))
+ projectiles (list (build-projectile level 0 -5 :step 9)
+ (build-projectile level 0 5 :step 9)
+ (build-projectile level 1 -5 :step 19)
+ (build-projectile level 1 5 :step 19)
+ (build-projectile level 3 -5 :step 35)
+ (build-projectile level 3 5 :step 35))
+ result (entities-after-collisions enemies projectiles)]
+ (println (str "Entities: "
+ (pr-str (count (:entities result)))
+ " of "
+ (pr-str (count enemies))))
+ (println (str "Projectiles: "
+ (pr-str (count (:projectiles result)))
+ " of "
+ (pr-str (count projectiles))))
+ ))
+
+
+
+(defn collisions-with-projectile
+ "Returns map with keys true and false. Values under true key have or
+ will collide with bullet in the next bullet update. Values under the
+ false key will not."
+ [enemy-list bullet]
+ (group-by (partial entity-between-steps
+ (:segment bullet)
+ (:step bullet)
+ (entity-next-step bullet))
+ enemy-list))
+
+
+(defn decrement-enemy-hits
+ "Decrement hits-remaining count on given enemy."
+ [enemy]
+ (assoc enemy :hits-remaining (dec (:hits-remaining enemy))))
+
+
+(defn projectile-off-level?
+ "Returns true if a projectile has reached either boundary of the level."
+ [projectile]
+ (cond
+ (zero? (:step projectile)) true
+ (>= (:step projectile) (:steps (:level projectile))) true
+ :else false))
+
+
+(defn add-player-projectile!
+ "Add a new projectile to the global list of live projectiles, originating
+ from the given player, on the segment he is currently on."
+ [player]
+ (let [level (:level player)
+ seg-idx (:segment player)
+ stride (:bullet-stride player)
+ step (:steps level)]
+ (reset! *projectile-list*
+ (conj @*projectile-list*
+ (build-projectile level seg-idx stride :step step)))))
+
+(defn segment-player-left
+ "Returns the segment to the left of the player. Loops around the level
+ on connected levels, and stops at 0 on unconnected levels."
+ [player]
+ (let [level (:level player)
+ seg-max (dec (count (:segments level)))
+ cur-seg (:segment player)
+ loops? (:loops? level)
+ new-seg (dec cur-seg)]
+ (if (< new-seg 0)
+ (if loops? seg-max 0)
+ new-seg)))
+
+
+(defn segment-player-right
+ "Returns the segment to the right of the player. Loops around the level
+ on connected levels, and stops at max on unconnected levels."
+ [player]
+ (let [level (:level player)
+ seg-max (dec (count (:segments level)))
+ cur-seg (:segment player)
+ loops? (:loops? level)
+ new-seg (inc cur-seg)]
+ (if (> new-seg seg-max)
+ (if loops? 0 seg-max)
+ new-seg)))
+
+(defn set-global-player-segment!
+ "Sets global *player*'s segment key to a new value."
+ [seg-idx]
+ (reset! *player* (assoc @*player* :segment seg-idx)))
+
+(defn keypress
+ "Respond to keyboard key presses."
+ [event]
+ (let [player @*player*
+ key (.-keyCode event)]
+ (condp = key
+ key-codes/RIGHT (set-global-player-segment!
+ (segment-player-right player))
+ key-codes/LEFT (set-global-player-segment!
+ (segment-player-left player))
+ key-codes/SPACE (add-player-projectile! player)
+ key-codes/ESC (def *paused* (atom (not @*paused*)))
+ nil
+ )))
+
+(defn animationFrameMethod []
+ "Returns a callable javascript function to schedule a frame to be drawn.
+ Tries to use requestAnimationFrame, or the browser-specific version of
+ it that is available. Falls back on setTimeout if requestAnimationFrame
+ is not available on player's browser.
+
+ requestAnimationFrame tries to figure out a consistent framerate based
+ on how long frame takes to render.
+
+ The setTimeout fail-over is hard-coded to attempt 30fps.
+ "
+ (let [window (dom/getWindow)
+ names ["requestAnimationFrame"
+ "webkitRequestAnimationFrame"
+ "mozRequestAnimationFrame"
+ "oRequestAnimationFrame"
+ "msRequestAnimationFrame"]
+ options (map (fn [name] #(aget window name)) names)]
+ ((fn [[current & remaining]]
+ (cond
+ (nil? current) #((.-setTimeout window) % (/ 1000 30))
+ (fn? (current)) (current)
+ :else (recur remaining)))
+ options)))
+
+(def ^{:doc
+ "Stores the frame-scheduling function for the current browser.
+ This function should be called at the end of each frame to
+ schedule the next frame to be drawn at the appropriate time."}
+ *animMethod* (animationFrameMethod))
+
+
+
+(def *frame-count* (atom 0))
+(def *frame-time* (atom (goog.now)))
+(def *enemy-list* (atom (list)))
+(def *player* (atom (list)))
+(def *projectile-list* (atom (list)))
+
+(defn build-game-state
+ []
+ (atom
+ {:enemy-list '()
+ :projectile-list '()
+ :player '()
+ :context nil
+ :dims nil
+ :level nil
+ :frame-count 0
+ :frame-time 0
+ :paused? false
+ }))
+
+(defn update-game!
+ "Call all of the drawing functions to redraw the scene, and update all
+ of the entities on the level."
+ [context dims level]
+ (doseq []
+ (.clearRect context 0 0 (:width dims) (:height dims))
+ (draw/draw-player context dims level (deref *player*))
+ (draw/draw-entities context dims level @*enemy-list*)
+ (draw/draw-entities context dims level @*projectile-list*)
+
+ (when (not @*paused*)
+ (let [new-entities (entities-after-collisions @*enemy-list*
+ @*projectile-list*)]
+ (def *projectile-list* (atom (:projectiles new-entities)))
+ (def *enemy-list* (atom (:entities new-entities))))
+
+ (def *projectile-list* (atom (update-entity-list @*projectile-list*)))
+ (def *projectile-list* (atom (remove projectile-off-level?
+ @*projectile-list*)))
+ (def *enemy-list* (atom (update-entity-list @*enemy-list*)))
+ (*animMethod* #(update-game! context dims level)))
+
+ (def *frame-count* (atom (inc @*frame-count*)))
+ (when (= 20 @*frame-count*)
+ (let [fps (/ (* 1000 @*frame-count*)
+ (- (goog.now) @*frame-time*))]
+ (dom/setTextContent (dom/getElement "fps")
+ (str "FPS: " (pr-str (js/Math.round fps)))))
+
+ (def *frame-count* (atom 0))
+ (def *frame-time* (atom (goog.now))))))
+
+
+(ns ^{:doc "
+Functions related to drawing on an HTML5 canvas.
+
+The functions in this module are responsible for drawing paths on an
+HTML5 canvas. This includes both primitive draw functions, and higher
+level functions to draw complete game entities using the primitives.
+"}
+ tempest.draw
+ (:require [tempest.levels :as levels]
+ [tempest.util :as util]
+ [tempest.path :as path]
+ [goog.dom :as dom]))
+
+(defn draw-rectangle
+ "Draws a rectangle (4 cartesian coordinates in a vector) on the 2D context
+ of an HTML5 canvas."
+ [context [p0 & points]]
+ (.moveTo context (first p0) (peek p0))
+ (doseq [p points]
+ (.lineTo context (first p) (peek p)))
+ (.lineTo context (first p0) (peek p0))
+ (.stroke context)
+ )
+
+(defn draw-line
+ "Draws a line on the given 2D context of an HTML5 canves element, between
+ the two given cartesian coordinates."
+ [context point0 point1]
+ (.moveTo context (first point0) (peek point0))
+ (.lineTo context (first point1) (peek point1))
+ (.stroke context))
+
+
+(defn draw-path
+ "Draws a 'path', a vector of multiple polar coordinates, on an HTML5 2D
+ drawing canvas.
+
+ context -- The '2D Context' of an HTML5 canvas element
+ origin -- The point (cartesian coordinate) to start drawing from
+ vecs -- Vector of polar coordinates to draw
+ skipfirst? -- Whether the first line described by vecs should be drawn. If
+ no, the first line can be used to offset the path, in effect changing the
+ 'midpoint' of the entity being drawn. If yes, the 'midpoint' of the
+ object is the first vertex from which the first line is drawn.
+ "
+ [context origin vecs skipfirst?]
+ (do
+ (.moveTo context (first origin) (peek origin))
+ ((fn [origin vecs skip?]
+ (if (empty? vecs)
+ nil
+ (let [line (first vecs)
+ point (path/rebase-origin (path/polar-to-cartesian-coords line)
+ origin)]
+ (if-not skip?
+ (.lineTo context (first point) (peek point))
+ (.moveTo context (first point) (peek point)))
+ (recur point (next vecs) false))))
+ origin vecs skipfirst?)
+ (.stroke context)))
+
+
+(defn draw-player
+ "Draws a player, defined by the given path 'player', on the 2D context of
+ an HTML5 canvas, with :height and :width specified in dims, and on the
+ given level."
+ [context dims level player]
+ (doseq []
+ (.beginPath context)
+ (draw-path context
+ (vec (map js/Math.round
+ (path/polar-to-cartesian-centered
+ (path/polar-entity-coord player)
+ dims)))
+ (path/round-path (path/player-path-on-level player))
+ true)
+ (.closePath context)))
+
+(defn draw-entities
+ "Draws all the entities, defined by paths in 'entity-list', on the 2D context
+ of an HTML5 canvas, with :height and :width specified in dims, and on the
+ given level."
+ [context dims level entity-list]
+ (doseq [entity entity-list]
+ (.beginPath context)
+ (draw-path context
+ (path/polar-to-cartesian-centered
+ (path/polar-entity-coord entity)
+ dims)
+ (path/round-path ((:path-fn entity) entity))
+ true)
+ (.closePath context)))
+
+(defn draw-board
+ "Draws a level on a 2D context of an HTML5 canvas with :height and :width
+ specified in dims."
+ [context dims level]
+ (doseq []
+ (.beginPath context)
+ (doseq [idx (range (count (:segments level)))]
+ (draw-rectangle
+ context
+ (path/round-path (path/rectangle-to-canvas-coords
+ dims (path/rectangle-for-segment level idx)))))
+ (.closePath context)))
+
-(ns tempest.levels
+(ns ^{:doc "
+Functions related to generating paths representing levels.
+"}
+ tempest.levels
(:require [tempest.util :as util]))
-;;;;
-;;;; Levels are defined by a vector of polar coordinates [r theta],
-;;;; which are used to build a vector of 'segments' that form a level.
-;;;;
-;;;; Levels can be manually specified by building a vector of lines
-;;;; manually.
-;;;;
-;;;; Some types of levels can be built automatically by calling helper
-;;;; functions in this module with various parameters.
-;;;;
-;;;; Levels are drawn radially, from the center point of the canvas.
-;;;;
-;;;; Levels are stored in the *levels* vector as a list of maps.
-;;;;
-;;;; Terminology in this module:
-;;;;
-;;;; "length" and "depth" both refer to how far from origin the inner
-;;;; line is drawn, in pixels.
-;;;;
-;;;; "length-fn" is a function to determine how long between inner and
-;;;; outer line. Takes one argument 'r' to the inner line. Returns
-;;;; 'r' to the outer line. Default is 'inner r' multiplied by 4.
-;;;;
-;;;; "width" is how wide, in pixels, the outer line segment is.
-;;;;
-;;;;
-;;;; Enemies travel up segments in steps. A level has the same number
-;;;; of steps per segment, but the size of the steps can vary depending
-;;;; on the dimensions of the segment. Instead of keeping track of its
-;;;; coordinates, an enemy keeps track of which segment it is on, and
-;;;; how many steps up the segment.
+;;
+;; ## Level Terminology:
+;;
+;; *length* and *depth* both refer to how far from origin the inner
+;; line is drawn, in pixels.
+;;
+;; *length-fn* is a function to determine how long between inner and
+;; outer line. Takes one argument 'r' to the inner line. Returns
+;; 'r' to the outer line. Default is 'inner r' multiplied by 4.
+;;
+;; *width* is how wide, in pixels, the outer line segment is.
+;;
+;; ## Level design
+;;
+;; Levels are defined by a vector of polar coordinates [r theta],
+;; which are used to build a vector of 'segments' that form a level.
+;;
+;; Levels can be manually specified by building a vector of lines
+;; manually.
+;;
+;; Some types of levels can be built automatically by calling helper
+;; functions in this module with various parameters.
+;;
+;; Levels are drawn radially, from the center point of the canvas.
+;;
+;; Levels are stored in the \*levels\* vector as a list of maps.
+;;
+;; Enemies travel up segments in steps. A level has the same number
+;; of steps per segment, but the size of the steps can vary depending
+;; on the dimensions of the segment. Instead of keeping track of its
+;; coordinates, an enemy keeps track of which segment it is on, and
+;; how many steps up the segment.
+;;
(def ^{:doc "Default length, in pixels, from origin to inner line."}
-;; "Flat" level functions follow:
+;; ## "Flat" level functions
;;
;; Functions for generating "flat" levels: levels where the edge appears as
;; a straight line. Something like this garbage:
;;
-;; ___________________________
-;; / / / | | | | | \ \
-;; / | | | | | | | \ \
-;; / / | | | | | | | \
-;; / / / | | | | | | \
-;; / / / | | | | | | \
-;; ----------------------------------------
+;; ___________________________
+;; / / / | | | | | \ \
+;; / | | | | | | | \ \
+;; / / | | | | | | | \
+;; / / / | | | | | | \
+;; / / / | | | | | | \
+;; ----------------------------------------
;;
;; Flat levels always start with a line dropped straight down, and build
;; out symmetrically from there. The width of segments at the "outer" edge
-;; "Oblong" level functions follow:
+;; ## "Oblong" level functions
;;
;; Functions for generating oblong triangles using Law of Cosines.
;; Use to generate arbitrary levels from a list of angles, gamma(0)..gamma(N),
-;; where gamma is the angle between the previous line segment 'towards the player'
-;; and the line segment that makes the 'width' of the segment.
-;; ____
-;; / / \
-;; / / \
-;; / / /
-;; / / /
-;; gamma1-/-> / /
-;; /____/ /
-;; \ </--- gamma0
-;; \ /
+;; where gamma is the angle between the previous line segment 'towards the
+;; player' and the line segment that makes the 'width' of the segment.
+;;
+;; ____
+;; / / \
+;; / / \
+;; / / /
+;; / / /
+;; gamma1-/-> / /
+;; /____/ /
+;; \ </--- gamma0
+;; \ /
+;;
;;
;; Named 'oblong' after the triangles formed when the lines are extended to origin.
;; As opposed to the flat-levels, which are constructed of right triangles.
-
-
-
-;;
-;;
-;; BELOW HERE ARE LEVELS
-;;
;;
;;
+;; ## BELOW HERE ARE LEVEL DEFINITIONS
;;
;;
-(ns tempest.macros)
+(ns tempest.macros
+ "Macros for Tempest, in separate namespace because of ClojureScript
+ limitation."
+ )
-(defmacro fntime [& body]
+(defmacro fntime
+ "Log time taken to run given expressions to javascript console."
+ [& body]
`(let [starttime# (goog.now)]
(do
~@body
+(ns ^{:doc "
+Functions related to path and coordinate creation and manipulation.
+
+Functions in this module work with polar coordinates, cartesian coordinate,
+and 'paths' consisting of a sequence of coordinates.
+"}
+ tempest.path
+ (:require [tempest.levels :as levels]
+ [tempest.util :as util]
+ [goog.dom :as dom]
+ [goog.math :as math]))
+
+(defn add-sub
+ "Given two polar coordinates, returns one polar coordinate with the first
+ elements (radii) summed, and the second elements (angles) subtracted.
+ i.e. [r1+r0, th1-th0]. This is used to move point0 to be relative to
+ point1."
+ [point0 point1]
+ [(+ (first point1) (first point0))
+ (- (peek point1) (peek point0))])
+
+
+(defn rebase-origin
+ "Return cartesian coordinate 'point' in relation to 'origin'."
+ [point origin]
+ (add-sub point origin))
+
+(defn polar-to-cartesian-centered
+ "Converts a polar coordinate (r,theta) into a cartesian coordinate (x,y)
+ centered on in a rectangle with given width and height."
+ [point {width :width height :height}]
+ (rebase-origin (polar-to-cartesian-coords point) [(/ width 2) (/ height 2)]))
+
+(defn polar-to-cartesian-coords
+ "Converts polar coordinates to cartesian coordinates. If optional length-fn
+ is specified, it is applied to the radius first."
+ ([[r angle]] [(math/angleDx angle r) (math/angleDy angle r)])
+ ([[r angle] length-fn]
+ (let [newr (length-fn r)]
+ [(math/angleDx angle newr) (math/angleDy angle newr)])
+ )
+ )
+
+(defn round-path-math
+ "Rounds all numbers in a path (vector of 2-tuples) to nearest integer."
+ [path]
+ (map (fn [coords]
+ [(js/Math.round (first coords))
+ (js/Math.round (peek coords))])
+ path))
+
+(defn round-path-hack
+ "Rounds all numbers in a path (vector of 2-tuples) to nearest integer.
+ ONLY WORKS WITH POSITIVE NUMBERS. Faster than round-path-math."
+ [path]
+ (map (fn [[x y]]
+ [(js* "~~" (+ 0.5 x))
+ (js* "~~" (+ 0.5 y))])
+ path))
+
+
+;; Use round-path-hack for now, since it's theoretically faster
+(def round-path round-path-hack)
+
+(defn point-to-canvas-coords
+ "Center a cartesian coordinate centered around (0,0) to be centered around
+ the middle of a rectangle with the given width and height. It inverts y,
+ assuming that the input y is 'up', and in the output y is 'down', as is
+ the case with an HTML5 canvas."
+ [{width :width height :height} p]
+ (let [xmid (/ width 2)
+ ymid (/ height 2)]
+ [(+ (first p) xmid) (- ymid (peek p))]
+ ))
+
+(defn rectangle-to-canvas-coords
+ "Given a rectangle (vector of 4 cartesian coordinates) centered around (0,0),
+ this function shifts them to be centered around the center of an HTML5
+ canvas with the :width and :height set in dims."
+ [dims rect]
+ (map #(point-to-canvas-coords dims %) rect)
+ )
+
+(defn rectangle-for-segment
+ "Returns vector [[x0 y0] [x1 y1] [x2 y2] [x3 y3]] describing segment's
+ rectangle in cartesian coordinates."
+ [level seg-idx]
+ (let [[seg0 seg1] (get (:segments level) seg-idx)
+ line0 (get (:lines level) seg0)
+ line1 (get (:lines level) seg1)]
+ [(polar-to-cartesian-coords line0)
+ (polar-to-cartesian-coords line0 (:length-fn level))
+ (polar-to-cartesian-coords line1 (:length-fn level))
+ (polar-to-cartesian-coords line1)]
+ ))
+
+
+(defn polar-entity-coord
+ "Returns current polar coordinates to the entity."
+ [entity]
+ (let [steplen (step-length-segment-midpoint (:level entity)
+ (:segment entity))
+ offset (* steplen (:step entity))
+ midpoint (segment-midpoint (:level entity) (:segment entity))]
+ (polar-extend offset midpoint)))
+
+(defn step-length-segment-midpoint
+ "Finds the 'step length' of a line through the middle of a level's segment.
+ This is how many pixels an entity should move per update to travel one
+ step."
+ [level seg-idx]
+ (/
+ (-
+ (first (segment-midpoint level seg-idx true))
+ (first (segment-midpoint level seg-idx false)))
+ (:steps level)))
+
+(defn step-length-segment-edge
+ "Finds the 'step length' of a line along the edge of a level's segment."
+ [level line]
+ (/
+ (-
+ ((:length-fn level) (first line))
+ (first line))
+ (:steps level)))
+
+(defn step-length-line
+ "Finds the 'step length' of an arbitrary line on the given level."
+ [level point0 point1]
+ (js/Math.abs
+ (/
+ (-
+ (first point0)
+ (first point1))
+ (:steps level))))
+
+(defn step-lengths-for-segment-lines
+ "Returns a vector [len0 len1] with the 'step length' for the two edge
+ lines that mark the boundaries of the given segment."
+ [level seg-idx]
+ (let [coords (concat (polar-lines-for-segment level seg-idx false)
+ (polar-lines-for-segment level seg-idx true))
+ line0 (take-nth 2 coords)
+ line1 (take-nth 2 (rest coords))]
+ [(apply #(step-length-line level %1 %2) line0)
+ (apply #(step-length-line level %1 %2) line1)]))
+
+(defn polar-distance
+ "Returns distance between to points specified by polar coordinates."
+ [[r0 theta0] [r1 theta1]]
+ (js/Math.sqrt
+ (+
+ (js/Math.pow r0 2)
+ (js/Math.pow r1 2)
+ (* -2 r0 r1 (js/Math.cos (util/deg-to-rad (- theta1 theta0)))))))
+
+(defn polar-midpoint-r
+ "Returns the radius to the midpoint of a line drawn between two polar
+ coordinates."
+ [[r0 theta0] [r1 theta1]]
+ (js/Math.round
+ (/
+ (js/Math.sqrt
+ (+
+ (js/Math.pow r0 2)
+ (js/Math.pow r1 2)
+ (* 2 r0 r1 (js/Math.cos (util/deg-to-rad (- theta1 theta0))))))
+ 2)))
+
+(defn polar-midpoint-theta
+ "Returns the angle to the midpoint of a line drawn between two polar
+ coordinates."
+ [[r0 theta0] [r1 theta1]]
+ (js/Math.round
+ (mod
+ (+ (util/rad-to-deg
+ (js/Math.atan2
+ (+
+ (* r0 (js/Math.sin (util/deg-to-rad theta0)))
+ (* r1 (js/Math.sin (util/deg-to-rad theta1))))
+ (+
+ (* r0 (js/Math.cos (util/deg-to-rad theta0)))
+ (* r1 (js/Math.cos (util/deg-to-rad theta1))))
+ ))
+ 360) 360)))
+
+(defn polar-midpoint
+ "Returns polar coordinate representing the midpoint between the two
+ points specified. This can be used to draw a line down the middle
+ of a level segment -- the line that entities should follow."
+ [point0 point1]
+ [(polar-midpoint-r point0 point1)
+ (polar-midpoint-theta point0 point1)]
+ )
+
+(defn segment-midpoint
+ "Given a level and a segment index, returns the midpoint of the segment.
+ scaled? determines whether it gives you the inner (false) or outer (true)
+ point."
+ [level seg-idx scaled?]
+ (apply polar-midpoint
+ (polar-lines-for-segment level seg-idx scaled?)))
+
+
+(defn scale-polar-coord
+ "Return a polar coordinate with the first element (radius) scaled using
+ the function scalefn"
+ [scalefn coord]
+ [(scalefn (first coord)) (peek coord)])
+
+(defn polar-extend
+ "Add 'length' to radius of polar coordinate."
+ [length coord]
+ [(+ length (first coord))
+ (peek coord)])
+
+(defn polar-lines-for-segment
+ "Returns vector [line0 line1], where lineN is a polar coordinate describing
+ the line from origin (canvas midpoint) that would draw the edges of a level
+ segment.
+
+ 'scaled?' sets whether you want the unscaled, inner point, or the
+ outer point scaled with the level's scale function.
+
+ To actually draw a level's line, you would move to the unscaled point
+ without drawing, and then draw to the scaled point.
+ "
+ [level seg-idx scaled?]
+ (let [[seg0 seg1] (get (:segments level) seg-idx)
+ line0 (get (:lines level) seg0)
+ line1 (get (:lines level) seg1)]
+ (if (true? scaled?)
+ [(scale-polar-coord (:length-fn level) line0)
+ (scale-polar-coord (:length-fn level) line1)]
+ [line0 line1]
+ )))
+
+;; Path that defines player.
+(def ^{:doc "Path, in polar coordinates, describing the player's ship."}
+ *player-path*
+ [[40 90]
+ [44 196]
+ [27 333]
+ [17 135]
+ [30 11]
+ [30 349]
+ [17 225]
+ [27 27]
+ [44 164]])
+
+(defn bounding-box-from-radius
+ [origin radius]
+ (let [d (* radius 2)]
+ {:x (- (first origin) radius)
+ :y (- (peek origin) radius)
+ :width d
+ :height d}))
+
+(defn player-path-on-level
+ "Returns the path of polar coordinates to draw the player correctly at its
+ current location. It corrects for size and angle."
+ [player]
+ (let [coord (polar-entity-coord player)]
+ (scale-path 0.6 (rotate-path
+ (enemy-angle player)
+ *player-path*))))
+
+(defn flipper-path-bounding-circle-radius
+ "Returns the radius of the bounding circle around the given flipper's path."
+ [path]
+ (max (map first path)))
+
+(defn flipper-path-on-level
+ "Returns the path of polar coordinates to draw a flipper correctly at its
+ current location. It corrects for size and angle."
+ [flipper]
+ (let [coord (polar-entity-coord flipper)]
+ (rotate-path
+ (enemy-angle flipper)
+ (flipper-path-with-width (* 0.8 (entity-desired-width flipper))))))
+
+(defn projectile-path-on-level
+ "Returns the path of polar coordinates to draw a projectile correctly at its
+ current location. It corrects for size and angle."
+ [projectile]
+ (let [coord (polar-entity-coord projectile)]
+ (rotate-path
+ (enemy-angle projectile)
+ (projectile-path-with-width (* 0.3 (entity-desired-width projectile))))))
+
+(defn flipper-path-with-width
+ "Returns a path to draw a 'flipper' enemy with given width."
+ [width]
+ (let [r (/ width (js/Math.cos (util/deg-to-rad 16)))]
+ [[0 0]
+ [(/ r 2) 16]
+ [(/ r 4) 214]
+ [(/ r 4) 326]
+ [r 164]
+ [(/ r 4) 326]
+ [(/ r 4) 214]
+ [(/ r 2) 16]]))
+
+
+(defn projectile-path-with-width
+ "Returns a path to draw a projectile with the given width."
+ [width]
+ (let [r (/ width (* 2 (js/Math.cos (util/deg-to-rad 45))))
+ midheight (* r (js/Math.sin (util/deg-to-rad 45)))]
+ [[midheight 270]
+ [r 45]
+ [r 135]
+ [r 225]
+ [r 315]]))
+
+(defn rotate-path
+ "Add angle to all polar coordinates in path."
+ [angle path]
+ (map (fn [coords]
+ [(first coords)
+ (mod (+ angle (peek coords)) 360)])
+ path))
+
+(defn scale-path
+ "Multiply all lengths of polar coordinates in path by scale."
+ [scale path]
+ (map (fn [coords]
+ [(* scale (first coords))
+ (peek coords)])
+ path))
+
+(defn path-extend
+ "Add 'length' to all polar coordinates in path"
+ [length path]
+ (map #(polar-extend length %) path))
+
+(defn enemy-angle
+ "Returns the angle from origin that the enemy needs to be rotated to
+ appear in the correct orientation at its current spot on the level.
+ In reality, it returns the angle of the line that traverses the segment
+ across the midpoint of the enemy. TODO: This should be renamed to
+ 'entity-angle', it works with anything on the board."
+ [enemy]
+ (let [edges (polar-lines-for-segment (:level enemy)
+ (:segment enemy)
+ false)
+ edge-steps (step-lengths-for-segment-lines (:level enemy)
+ (:segment enemy))
+ offset0 (* (first edge-steps) (:step enemy))
+ offset1 (* (peek edge-steps) (:step enemy))
+ point0 (polar-extend offset0 (first edges))
+ point1 (polar-extend offset1 (peek edges))]
+ (util/rad-to-deg
+ (apply js/Math.atan2
+ (vec (reverse (map - (polar-to-cartesian-coords point0)
+ (polar-to-cartesian-coords point1))))))))
+
+(defn entity-desired-width
+ "Returns how wide the given enemy should be drawn to span the full width
+ of its current location. In reality, that means returning the length of
+ the line that spans the level segment, cutting through the enemy's
+ midpoint. TODO: rename this entity-desired-width."
+ [enemy]
+ (let [edges (polar-lines-for-segment (:level enemy)
+ (:segment enemy)
+ false)
+ edge-steps (step-lengths-for-segment-lines (:level enemy)
+ (:segment enemy))
+ offset0 (* (first edge-steps) (:step enemy))
+ offset1 (* (peek edge-steps) (:step enemy))
+ point0 (polar-extend offset0 (first edges))
+ point1 (polar-extend offset1 (peek edges))]
+ (polar-distance point0 point1)))
+
-(ns tempest.util)
+(ns ^{:doc "Small utility and math helper functions."}
+ tempest.util)
-(defn rad-to-deg [rad]
+(defn rad-to-deg
+ "Convert radians to degrees"
+ [rad]
(/ (* rad 180) 3.14159265358979))
-(defn deg-to-rad [deg]
+(defn deg-to-rad
+ "Convert degrees to radians"
+ [deg]
(/ (* deg 3.14159265358979) 180))
+(defn round
+ "Perform quick rounding of given number. ONLY WORKS WITH POSITIVE NUMBERS."
+ [num]
+ (js* "~~" (+ 0.5 num)))