;;;; Copyright (C) 2018 Jakob L. Kreuze, All Rights Reserved. ;;;; ;;;; This file is part of slime-the-world. ;;;; ;;;; slime-the-world is free software: you can redistribute it and/or modify it ;;;; under the terms of the GNU General Public License as published by the Free ;;;; Software Foundation, either version 3 of the License, or (at your option) ;;;; any later version. ;;;; ;;;; slime-the-world is distributed in the hope that it will be useful, but ;;;; WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ;;;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for ;;;; more details. ;;;; ;;;; You should have received a copy of the GNU General Public License along ;;;; with slime-the-world. If not, see . (local bump (require :lib.bump)) (local lume (require :lib.lume)) (local draw (require :draw)) ;; Updates the position of the player contained in `world' to the world ;; coordinates, (`x', `y'). (fn position-player-at [world x y] (let [player (. world :player) collision-map (. world :collision-map)] (tset player :x-pos x) (tset player :y-pos y) (: collision-map :update player x y))) ;; Returns the tile at the given grid coordinates. (fn tile-at [world x y] (if (and (>= x 0) (< x (. world :width)) (>= y 0) (< y (. world :height))) (. world :tiles (+ 1 y) (+ 1 x)) nil)) (local tile-sheet {:img (love.graphics.newImage "art/tiles.png") :width 32 :height 32 :padding-x 8 :padding-y 8 :offsets {:bricks 0}}) (local slime-sheet {:img (love.graphics.newImage "art/slime.png") :width 32 :height 32 :padding-x 8 :padding-y 8 :offsets {:top 0 :bottom 1 :left 2 :right 3}}) ;; Draws the slime decal for a given orientation at the screen coordinates, ;; (`x', `y'). (fn draw-slime-decal [orientation x y] (let [tile-offset (. slime-sheet :offsets orientation)] (draw.tile x y slime-sheet tile-offset 0))) ;; Draws all slimed surfaces for a tile at screen coordinates, (`x', `y'). (fn draw-slime [tile x y] (each [orientation slimed (pairs (. tile :slimed))] (when slimed (draw-slime-decal orientation x y)))) ;; Draws a single tile at the screen coordinates, (`x', `y'). (fn draw-tile [tile x y] (let [tile-offset (. tile-sheet :offsets (. tile :type))] (draw.tile x y tile-sheet tile-offset 0) (draw-slime tile x y))) ;; ;; Drawing routine for rendering the tiles visible from a given camera offset. (fn draw-map [world camera-x camera-y screen-width screen-height] (let [tile-width (. tile-sheet :width) tile-height (. tile-sheet :height) how-many-x (+ 1 (/ screen-width tile-width)) how-many-y (+ 1 (/ screen-height tile-height)) start-x (math.floor (/ camera-x tile-width)) start-y (math.floor (/ camera-y tile-height)) width (. world :width) height (. world :height)] (for [x-offset 0 how-many-x] (for [y-offset 0 how-many-y] (when (and (and (>= (+ start-x x-offset) 0) (< (+ start-x x-offset) width)) (and (>= (+ start-y y-offset) 0) (< (+ start-y y-offset) height))) (let [tile (tile-at world (+ start-x x-offset) (+ start-y y-offset)) screen-x (- (* x-offset tile-width) (% camera-x tile-width)) screen-x (lume.round screen-x) screen-y (- (* y-offset tile-height) (% camera-y tile-height)) screen-y (lume.round screen-y)] (when (~= :empty (. tile :type)) (draw-tile tile screen-x screen-y)))))))) (fn draw-hud [world screen-width screen-height] (let [cur-health (. world :player :cur-health) max-health (. world :player :max-health) font-height (: (love.graphics.getFont) :getHeight) surfaces-slimed (. world :surfaces-slimed) surfaces-total (. world :surfaces-total) health-msg (string.format "h: %d/%d" cur-health max-health) slime-msg (string.format "s: %d/%d" surfaces-slimed surfaces-total)] (love.graphics.print health-msg 0 (- screen-height (* 2 font-height))) (love.graphics.print slime-msg 0 (- screen-height font-height)))) (fn draw-object [object camera-x camera-y] (let [x-pos (. object :x-pos) y-pos (. object :y-pos) x (- x-pos camera-x) y (- y-pos camera-y)] (: object :draw x y))) (fn draw-world [world camera-x camera-y screen-width screen-height] (love.graphics.clear 0.1 0.1 0.1) (draw-map world camera-x camera-y screen-width screen-height) (draw-object (. world :player) camera-x camera-y) (each [_ slimeball (ipairs (. world :slimeballs))] (draw-object slimeball camera-x camera-y)) (draw-hud world screen-width screen-height)) ;; Returns whether or not `tile' exists in `checked'. (fn tile-checked [tile checked] (var res false) (each [_ other (ipairs checked)] (when (and (= (. tile :x-pos) (. other :x-pos)) (= (. tile :y-pos) (. other :y-pos))) (set res true))) res) ;; Modified implementation of . (fn count-surfaces-recur [world tile checked] (var surfaces 0) (when (not (tile-checked tile checked)) (table.insert checked tile) (let [x (/ (. tile :x-pos) (. tile-sheet :width)) y (/ (. tile :y-pos) (. tile-sheet :height)) width (. world :width) height (. world :height)] (when (>= x 0) (let [tile (tile-at world (- x 1) y)] (if (= :empty (. tile :type)) (set surfaces (+ surfaces (count-surfaces-recur world tile checked))) (set surfaces (+ 1 surfaces))))) (when (< x width) (let [tile (tile-at world (+ x 1) y)] (if (= :empty (. tile :type)) (set surfaces (+ surfaces (count-surfaces-recur world tile checked))) (set surfaces (+ 1 surfaces))))) (when (>= y 0) (let [tile (tile-at world x (- y 1))] (if (= :empty (. tile :type)) (set surfaces (+ surfaces (count-surfaces-recur world tile checked))) (set surfaces (+ 1 surfaces))))) (when (< y height) (let [tile (tile-at world x (+ y 1))] (if (= :empty (. tile :type)) (set surfaces (+ surfaces (count-surfaces-recur world tile checked))) (set surfaces (+ 1 surfaces))))))) surfaces) ;; Returns some tile in `world' of type `tile-type', or nil if no such tile is ;; present. (fn find-any [tile-type world] (var res nil) (let [width (. world :width) height (. world :height)] (for [x 0 (- width 1)] (for [y 0 (- height 1)] (let [tile (tile-at world x y)] (when (= tile-type (. tile :type)) (set res tile)))))) res) ;; Returns the number of slime-able surfaces in the given grid of tiles. (fn count-surfaces [world] (let [seed (find-any :empty world)] (when seed (count-surfaces-recur world seed [])))) (fn add-object-to-collision-map [collision-map object] (let [x (. object :x-pos) y (. object :y-pos) width (. object :width) height (. object :height)] (: collision-map :add object x y width height))) (fn add-tile-to-collision-map [collision-map tile] (let [x (. tile :x-pos) y (. tile :y-pos) width (. tile :width) height (. tile :height)] (: collision-map :add tile x y width height))) ;; Returns a new collision map containing `player' and everything in `map'. (fn new-collision-map [world player] (let [collision-map (bump.newWorld 32) width (. world :width) height (. world :height)] (add-object-to-collision-map collision-map player) (for [x 0 (- width 1)] (for [y 0 (- height 1)] (let [tile (tile-at world x y)] (when (~= :empty (. tile :type)) (add-tile-to-collision-map collision-map tile))))) collision-map)) ;; Creates a new tile object of the given `type-type' positioned at world ;; coordinates (`x', `y'). (fn new-tile [tile-type x y] {:is-tile true :width (. tile-sheet :width) :height (. tile-sheet :height) :x-pos x :y-pos y :type tile-type :slimed {:top false :bottom false :left false :right false}}) ;; Returns the tile object for a given pixel in `image'. (fn get-tile [image x y] (local tile-values {:empty [1 0 0 0] :bricks [0 0 0 1]}) (var res (new-tile :empty (* x (. tile-sheet :width)) (* y (. tile-sheet :height)))) (let [(r g b a) (: image :getPixel x y)] (each [tile-type colors (pairs tile-values)] (when (and (= (. colors 1) r) (= (. colors 2) g) (= (. colors 3) b) (= (. colors 4) a)) (tset res :type tile-type)))) res) ;; Loads the tile layout of the level from the given ImageData, returning a ;; rectangular two-dimensional array. (fn load-tiles [image] (var tiles []) (let [width (: image :getWidth) height (: image :getHeight)] (for [y 0 (- height 1)] (var row []) (for [x 0 (- width 1)] (table.insert row (get-tile image x y))) (table.insert tiles row)) {:tiles tiles :width width :height height})) ;; Returns the metadata and objects stored in the metadata file at the given ;; path. (fn load-meta [path] (fennel.dofile path)) (fn slime-tile [world tile orientation] (when (not (. tile :slimed orientation)) (let [surfaces-slimed (. world :surfaces-slimed)] (tset world :surfaces-slimed (+ 1 surfaces-slimed)) (tset tile :slimed orientation true)))) (fn move-slimeball [world slimeball dt] (let [collision-map (. world :collision-map) (goal-x goal-y) (: slimeball :next-position dt) filter (fn [] "cross") (x y collisions _) (: collision-map :move slimeball goal-x goal-y filter)] (each [_ collision (ipairs collisions)] (let [other (. collision :other) x-normal (. collision :normal :x) y-normal (. collision :normal :y)] (when (. other :is-tile) (if (> 0 y-normal) (slime-tile world other :top) (< 0 y-normal) (slime-tile world other :bottom) (> 0 x-normal) (slime-tile world other :left) (< 0 x-normal) (slime-tile world other :right)) (for [i 1 (# (. world :slimeballs))] (when (= slimeball (. (. world :slimeballs) i)) (table.remove (. world :slimeballs) i)))))) (tset slimeball :x-pos x) (tset slimeball :y-pos y))) (fn update-world [world dt] (let [player (. world :player) slimeballs (. world :slimeballs) collision-map (. world :collision-map) (goal-x goal-y) (: player :next-position dt) filter (fn [_ other] (if (. other :is-tile) "slide" "cross")) (x y collisions _) (: collision-map :move player goal-x goal-y filter)] (each [_ slimeball (ipairs slimeballs)] (move-slimeball world slimeball dt) (: slimeball :update dt)) (tset player :x-pos x) (tset player :y-pos y) (tset player :grounded false) (each [_ collision (ipairs collisions)] (let [other (. collision :other) x-normal (. collision :normal :x) y-normal (. collision :normal :y)] (when (. other :is-tile) ;; Touching top of surface. (when (> 0 y-normal) (tset player :grounded true) (slime-tile world other :top)) ;; Touching bottom of surface. (when (< 0 y-normal) (slime-tile world other :bottom)) ;; Touching left of surface. (when (> 0 x-normal) (slime-tile world other :left) (: player :bounce x-normal y-normal)) ;; Touching right of surface. (when (< 0 x-normal) (slime-tile world other :right) (: player :bounce x-normal y-normal))))) (tset player :x-vel (: player :next-x-vel dt)) (tset player :y-vel (: player :next-y-vel dt)))) (fn add-slimeball [world slimeball] (table.insert (. world :slimeballs) slimeball) (add-object-to-collision-map (. world :collision-map) slimeball)) ;; Loads the map of the given name into a new world containing `player'. Will ;; error out if either 'maps/${name}.png' or 'maps/${name}.fnl' do not exist. (fn new-world [name player] (let [tiles-path (.. "maps/" name ".png") meta-path (.. "maps/" name ".fnl") res (load-tiles (love.image.newImageData tiles-path)) res (lume.extend res {:player player}) res (lume.extend res {:slimeballs []}) res (lume.extend res {:surfaces-slimed 0 :meta (load-meta meta-path)}) res (lume.extend res {:surfaces-total (count-surfaces res)}) res (lume.extend res {:collision-map (new-collision-map res player)}) res (lume.extend res {:position-player-at position-player-at}) res (lume.extend res {:add-slimeball add-slimeball}) res (lume.extend res {:draw draw-world}) res (lume.extend res {:update update-world})] res)) {:tile-sheet tile-sheet :tile-at tile-at :new new-world}