2024-05-17 13:08:43 -04:00
|
|
|
;;; Copyright (C) 2024 David Thompson <dave@spritely.institute>
|
2024-05-26 12:52:03 -04:00
|
|
|
;;; Copyright (C) 2024 Juliana Sims <juli@incana.org>
|
2024-05-17 13:08:43 -04:00
|
|
|
;;;
|
|
|
|
;;; Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
;;; you may not use this file except in compliance with the License.
|
|
|
|
;;; You may obtain a copy of the License at
|
|
|
|
;;;
|
|
|
|
;;; http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
;;;
|
|
|
|
;;; Unless required by applicable law or agreed to in writing, software
|
|
|
|
;;; distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
;;; See the License for the specific language governing permissions and
|
|
|
|
;;; limitations under the License.
|
|
|
|
|
|
|
|
;;; Commentary:
|
|
|
|
;;;
|
|
|
|
;;; Example game showing off several common game programming things.
|
|
|
|
;;;
|
|
|
|
;;; Code:
|
|
|
|
|
2024-05-17 17:49:43 -04:00
|
|
|
(use-modules (dom canvas)
|
|
|
|
(dom document)
|
|
|
|
(dom element)
|
|
|
|
(dom event)
|
2024-12-10 11:32:14 -05:00
|
|
|
(dom gamepad)
|
2024-05-17 17:49:43 -04:00
|
|
|
(dom image)
|
|
|
|
(dom media)
|
|
|
|
(dom window)
|
2024-05-18 14:04:35 -04:00
|
|
|
(game actors)
|
2024-05-25 06:52:59 -04:00
|
|
|
(game animation)
|
2024-05-22 21:30:06 -04:00
|
|
|
(game audio)
|
2024-05-23 11:43:59 -04:00
|
|
|
(game effects)
|
2024-05-20 12:15:39 -04:00
|
|
|
(game level)
|
2024-05-24 12:09:26 -04:00
|
|
|
(game levels tutorial-1)
|
|
|
|
(game levels tutorial-2)
|
|
|
|
(game levels tutorial-3)
|
|
|
|
(game levels tutorial-4)
|
|
|
|
(game levels tutorial-5)
|
|
|
|
(game levels tutorial-6)
|
2024-05-24 17:11:11 -04:00
|
|
|
(game levels tutorial-7)
|
2024-05-24 18:07:29 -04:00
|
|
|
(game levels tutorial-8)
|
2024-05-25 09:52:02 -04:00
|
|
|
(game levels tutorial-9)
|
|
|
|
(game levels tutorial-10)
|
2024-05-24 18:36:57 -04:00
|
|
|
(game levels rat-1)
|
2024-05-24 19:19:50 -04:00
|
|
|
(game levels rat-2)
|
2024-05-24 19:55:34 -04:00
|
|
|
(game levels rat-3)
|
2024-05-25 14:49:30 -04:00
|
|
|
(game levels catboss-1)
|
2024-05-25 17:31:02 -04:00
|
|
|
(game levels catboss-2)
|
2024-05-26 08:36:20 -04:00
|
|
|
(game levels catboss-3)
|
2024-05-23 14:05:13 -04:00
|
|
|
(game levels credits)
|
2024-05-26 16:46:50 -04:00
|
|
|
(game particles)
|
2024-05-23 11:43:59 -04:00
|
|
|
(game scripts)
|
2024-05-18 18:51:45 -04:00
|
|
|
(game tileset)
|
2024-05-25 06:52:59 -04:00
|
|
|
(game time)
|
2024-05-18 14:04:35 -04:00
|
|
|
(goblins core)
|
2024-05-19 17:30:36 -04:00
|
|
|
(hoot bytevectors)
|
2024-05-17 17:49:43 -04:00
|
|
|
(hoot ffi)
|
|
|
|
(hoot hashtables)
|
|
|
|
(ice-9 match)
|
2024-05-20 22:12:35 -04:00
|
|
|
(local-storage)
|
2024-05-17 17:49:43 -04:00
|
|
|
(math)
|
|
|
|
(math rect)
|
2024-12-12 09:18:02 -05:00
|
|
|
(math vector)
|
|
|
|
(scheme base))
|
2024-05-17 17:49:43 -04:00
|
|
|
|
|
|
|
(define game-width 320.0)
|
|
|
|
(define game-height 240.0)
|
2024-05-18 14:04:35 -04:00
|
|
|
(define level-width (inexact->exact (floor (/ game-width tile-width))))
|
|
|
|
(define level-height (inexact->exact (floor (/ game-height tile-height))))
|
|
|
|
|
|
|
|
(define *canvas-scale* 0.0)
|
|
|
|
(define *canvas-width* 0)
|
|
|
|
(define *canvas-height* 0)
|
2024-05-17 17:58:43 -04:00
|
|
|
|
2024-05-19 06:15:27 -04:00
|
|
|
;; Assets
|
2024-05-18 18:51:45 -04:00
|
|
|
(define tileset
|
2024-05-26 15:08:37 -04:00
|
|
|
(make-tileset (make-image "assets/images/cirkoban-onesheet.png")
|
|
|
|
320 240
|
2024-05-18 18:51:45 -04:00
|
|
|
(inexact->exact tile-width)
|
|
|
|
(inexact->exact tile-height)))
|
2024-05-26 15:23:41 -04:00
|
|
|
(define* (load-sound-effect name #:key (volume 0.75))
|
2024-05-22 21:30:06 -04:00
|
|
|
(make-sound-effect (string-append "assets/sounds/" name ".wav")))
|
2024-05-19 17:30:36 -04:00
|
|
|
(define audio:bump (load-sound-effect "bump"))
|
|
|
|
(define audio:push (load-sound-effect "push"))
|
|
|
|
(define audio:undo (load-sound-effect "undo"))
|
|
|
|
(define audio:no (load-sound-effect "no"))
|
2024-05-20 12:15:39 -04:00
|
|
|
(define audio:exit (load-sound-effect "exit"))
|
2024-05-21 14:10:57 -04:00
|
|
|
(define audio:pickup (load-sound-effect "pickup"))
|
2024-05-23 09:10:25 -04:00
|
|
|
(define audio:emit (load-sound-effect "emit"))
|
2024-05-23 15:07:48 -04:00
|
|
|
(define audio:emitter-on (load-sound-effect "emitter-on"))
|
|
|
|
(define audio:emitter-off (load-sound-effect "emitter-off"))
|
2024-05-22 09:29:44 -04:00
|
|
|
(define audio:die (load-sound-effect "die"))
|
2024-05-23 07:48:53 -04:00
|
|
|
(define audio:gate (load-sound-effect "gate"))
|
|
|
|
(define audio:warp (load-sound-effect "warp"))
|
2024-05-23 09:01:47 -04:00
|
|
|
(define audio:floor-switch (load-sound-effect "floor-switch"))
|
2024-05-22 21:53:17 -04:00
|
|
|
(define audio:electric-switch-on (load-sound-effect "electric-switch-on"))
|
|
|
|
(define audio:electric-switch-off (load-sound-effect "electric-switch-off"))
|
2024-05-25 16:42:39 -04:00
|
|
|
(define audio:explosion (load-sound-effect "explosion"))
|
2024-05-17 13:08:43 -04:00
|
|
|
|
2024-05-26 15:42:34 -04:00
|
|
|
(define* (load-music name #:key (volume 0.3))
|
2024-05-26 15:23:41 -04:00
|
|
|
(let ((music (make-audio (string-append "assets/music/" name ".ogg"))))
|
|
|
|
(set-media-loop! music 1)
|
|
|
|
(set-media-volume! music volume)
|
|
|
|
music))
|
|
|
|
(define audio:bg-music (load-music "cirkoban"))
|
|
|
|
|
2024-05-18 14:04:35 -04:00
|
|
|
;; Game state
|
2024-12-31 13:14:17 -05:00
|
|
|
(define *state* '(initial))
|
2024-12-17 13:59:47 -05:00
|
|
|
|
|
|
|
(define (push-game-state! state)
|
|
|
|
(set! *state* (cons state *state*)))
|
|
|
|
(define (pop-game-state!)
|
2024-12-20 09:09:12 -05:00
|
|
|
(when (pair? *state*)
|
|
|
|
(set! *state* (cdr *state*))))
|
|
|
|
(define (replace-game-state! state)
|
2024-12-31 13:14:17 -05:00
|
|
|
(match *state*
|
|
|
|
((_ . rest)
|
|
|
|
(set! *state* (cons state rest)))))
|
2024-12-20 09:09:12 -05:00
|
|
|
(define (current-game-state)
|
2024-12-31 13:14:17 -05:00
|
|
|
(match *state*
|
|
|
|
((state . _) state)))
|
2024-05-20 13:34:59 -04:00
|
|
|
|
2024-05-18 14:04:35 -04:00
|
|
|
(define *actormap* (make-whactormap))
|
|
|
|
(define (call-with-goblins thunk)
|
|
|
|
(actormap-churn-run! *actormap* thunk))
|
|
|
|
(define-syntax-rule (with-goblins body ...)
|
|
|
|
(call-with-goblins (lambda () body ...)))
|
|
|
|
|
2024-05-20 13:34:59 -04:00
|
|
|
(define levels
|
|
|
|
(vector
|
2024-05-24 12:09:26 -04:00
|
|
|
load-tutorial-1
|
|
|
|
load-tutorial-2
|
|
|
|
load-tutorial-3
|
|
|
|
load-tutorial-4
|
|
|
|
load-tutorial-5
|
|
|
|
load-tutorial-6
|
2024-05-24 17:11:11 -04:00
|
|
|
load-tutorial-7
|
2024-05-24 18:07:29 -04:00
|
|
|
load-tutorial-8
|
2024-05-25 09:52:02 -04:00
|
|
|
load-tutorial-9
|
|
|
|
load-tutorial-10
|
2024-05-24 18:36:57 -04:00
|
|
|
load-rat-1
|
2024-05-26 20:43:13 -04:00
|
|
|
;; load-rat-2
|
2024-05-24 19:55:34 -04:00
|
|
|
load-rat-3
|
2024-05-25 14:49:30 -04:00
|
|
|
load-catboss-1
|
2024-05-25 17:31:02 -04:00
|
|
|
load-catboss-2
|
2024-05-26 15:54:45 -04:00
|
|
|
load-catboss-3))
|
2024-05-20 13:34:59 -04:00
|
|
|
(define *level-idx* #f)
|
2024-12-12 13:28:48 -05:00
|
|
|
;; Last level for restoring after visiting credits via the menu
|
|
|
|
(define *level-last* #f)
|
2024-05-21 14:10:57 -04:00
|
|
|
(define *gems* #f)
|
2024-05-18 14:04:35 -04:00
|
|
|
(define *level* #f)
|
|
|
|
;; Latest representation of all actors in level
|
2024-05-20 22:12:35 -04:00
|
|
|
(define *objects* #f)
|
2024-05-18 14:04:35 -04:00
|
|
|
|
|
|
|
(define *snapshots* '())
|
|
|
|
(define (clear-snapshots!)
|
|
|
|
(set! *snapshots* '()))
|
|
|
|
(define (save-snapshot!)
|
|
|
|
(set! *snapshots* (cons (copy-whactormap *actormap*) *snapshots*)))
|
2024-05-18 18:51:45 -04:00
|
|
|
(define (rollback-snapshot!)
|
|
|
|
(match *snapshots*
|
2024-05-22 21:30:06 -04:00
|
|
|
(() (play-sound-effect audio:no))
|
2024-05-18 18:51:45 -04:00
|
|
|
((snapshot . older-snapshots)
|
|
|
|
(set! *actormap* snapshot)
|
2024-05-19 17:30:36 -04:00
|
|
|
(set! *snapshots* older-snapshots)
|
2024-05-23 12:06:42 -04:00
|
|
|
(play-sound-effect audio:undo)
|
2024-05-23 16:41:11 -04:00
|
|
|
(unless *current-effect*
|
2024-05-26 14:55:04 -04:00
|
|
|
(show-effect! (make-wipe-effect 0.25))))))
|
2024-05-18 14:04:35 -04:00
|
|
|
|
2024-05-22 06:52:36 -04:00
|
|
|
(define (sort lst compare)
|
2024-05-21 17:28:57 -04:00
|
|
|
(match lst
|
|
|
|
(() '())
|
|
|
|
((_) lst)
|
2024-05-22 06:52:36 -04:00
|
|
|
(_
|
|
|
|
;; Insertion sort because I am lazy!
|
|
|
|
(let ((vec (list->vector lst)))
|
|
|
|
(let outer ((i 1))
|
|
|
|
(when (< i (vector-length vec))
|
|
|
|
(let inner ((j i))
|
|
|
|
(when (> j 0)
|
|
|
|
(let ((a (vector-ref vec j))
|
|
|
|
(b (vector-ref vec (1- j))))
|
|
|
|
(when (compare a b)
|
|
|
|
(vector-set! vec j b)
|
|
|
|
(vector-set! vec (1- j) a)
|
|
|
|
(inner (1- j))))))
|
|
|
|
(outer (1+ i))))
|
|
|
|
(vector->list vec)))))
|
2024-05-21 17:28:57 -04:00
|
|
|
|
|
|
|
(define (filter-map proc lst)
|
|
|
|
(let lp ((lst lst))
|
|
|
|
(match lst
|
|
|
|
(() '())
|
|
|
|
((head . tail)
|
|
|
|
(let ((head* (proc head)))
|
|
|
|
(if head*
|
|
|
|
(cons head* (lp tail))
|
|
|
|
(lp tail)))))))
|
|
|
|
|
2024-05-20 22:12:35 -04:00
|
|
|
(define (update-objects!)
|
|
|
|
(set! *objects*
|
2024-05-21 22:12:21 -04:00
|
|
|
;; z-sort the list so we render in the correct order. Then
|
|
|
|
;; convert tile positions to vec2s of pixel coordinates for
|
|
|
|
;; more efficient rendering.
|
2024-05-20 22:12:35 -04:00
|
|
|
(map (match-lambda
|
2024-05-21 17:28:57 -04:00
|
|
|
((type #(x y _) . properties)
|
2024-05-21 22:12:21 -04:00
|
|
|
`(,type ,(vec2 (* x tile-width) (* y tile-height)) ,@properties)))
|
|
|
|
(sort ($ (level-actor *level*) 'describe)
|
2024-05-21 17:28:57 -04:00
|
|
|
(lambda (a b)
|
|
|
|
(match a
|
|
|
|
((_ #(_ _ az) . _)
|
|
|
|
(match b
|
|
|
|
((_ #(_ _ bz) . _)
|
|
|
|
(<= az bz))))))))))
|
|
|
|
|
|
|
|
(define (collected-gem? idx)
|
2024-05-22 18:34:29 -04:00
|
|
|
(memq idx *gems*))
|
2024-05-18 14:04:35 -04:00
|
|
|
|
2024-12-12 13:28:48 -05:00
|
|
|
(define (set-level! idx)
|
2024-12-20 09:09:12 -05:00
|
|
|
(replace-game-state! 'play)
|
2024-05-20 22:12:35 -04:00
|
|
|
(set! *actormap* (make-whactormap))
|
2024-05-20 13:34:59 -04:00
|
|
|
(clear-snapshots!)
|
2024-05-20 22:12:35 -04:00
|
|
|
(with-goblins
|
2024-05-21 17:28:57 -04:00
|
|
|
(set! *level* ((vector-ref levels idx) (collected-gem? idx)))
|
2024-05-20 22:12:35 -04:00
|
|
|
(update-objects!)))
|
|
|
|
|
2024-05-23 14:05:13 -04:00
|
|
|
(define (load-credits!)
|
2024-12-20 10:14:44 -05:00
|
|
|
(replace-game-state! 'credits)
|
2024-05-23 14:05:13 -04:00
|
|
|
(set! *actormap* (make-whactormap))
|
|
|
|
(set-vec2-y! *credits-scroll* 0.0)
|
|
|
|
(clear-snapshots!)
|
|
|
|
(with-goblins
|
2024-05-25 21:23:25 -04:00
|
|
|
(set! *level* (load-credits #f))
|
2024-05-23 14:05:13 -04:00
|
|
|
(update-objects!)))
|
|
|
|
|
2024-12-12 13:28:48 -05:00
|
|
|
(define (load-level! idx)
|
2024-05-24 13:23:36 -04:00
|
|
|
;; TODO: Maybe show a little achievement popup when all gems
|
|
|
|
;; are collected?
|
|
|
|
(when (with-goblins ($ (level-actor *level*) 'gem-collected?))
|
|
|
|
(set! *gems* (cons *level-idx* *gems*)))
|
2024-05-20 13:34:59 -04:00
|
|
|
(if (< idx (vector-length levels))
|
2024-05-20 22:12:35 -04:00
|
|
|
(begin
|
2024-05-23 11:43:59 -04:00
|
|
|
(run-script
|
|
|
|
(lambda ()
|
2024-12-20 09:09:12 -05:00
|
|
|
(replace-game-state! 'interstitial)
|
2024-05-26 12:35:46 -04:00
|
|
|
(yield
|
|
|
|
(lambda (k)
|
|
|
|
(show-effect! (make-fade-out+in-effect 1.0 k))))
|
2024-05-26 20:51:13 -04:00
|
|
|
(set! *level-idx* idx)
|
|
|
|
(save-game!)
|
2024-12-12 13:28:48 -05:00
|
|
|
(set-level! idx))))
|
2024-05-23 14:05:13 -04:00
|
|
|
(begin
|
|
|
|
(run-script
|
|
|
|
(lambda ()
|
2024-12-20 09:09:12 -05:00
|
|
|
(replace-game-state! 'interstitial)
|
2024-05-26 12:35:46 -04:00
|
|
|
(yield
|
|
|
|
(lambda (k)
|
|
|
|
(show-effect! (make-fade-out+in-effect 2.0 k))))
|
2024-05-26 20:51:13 -04:00
|
|
|
(set! *level-idx* 0)
|
|
|
|
(save-game!)
|
|
|
|
;; HACK: To keep the intro level's control explanation
|
|
|
|
;; text from showing up when resetting the game, set
|
|
|
|
;; level idx to non-zero during the credits.
|
|
|
|
(set! *level-idx* -1)
|
2024-12-12 13:28:48 -05:00
|
|
|
(load-credits!))))))
|
|
|
|
|
|
|
|
(define (next-level!)
|
|
|
|
(load-level! (+ *level-idx* 1)))
|
2024-05-20 13:34:59 -04:00
|
|
|
|
2024-05-20 22:12:35 -04:00
|
|
|
;; Auto-save/load to local storage.
|
|
|
|
(define (save-game!)
|
2024-05-21 14:10:57 -04:00
|
|
|
(local-storage-set! "cirkoban-save"
|
|
|
|
(call-with-output-string
|
|
|
|
(lambda (port)
|
|
|
|
(write (list *level-idx* *gems*) port)))))
|
2024-05-20 22:12:35 -04:00
|
|
|
|
|
|
|
(define (load-game!)
|
2024-05-21 14:10:57 -04:00
|
|
|
(let ((saved
|
|
|
|
(match (local-storage-ref "cirkoban-save")
|
|
|
|
("" '(0 ())) ; initial save state
|
|
|
|
(str (call-with-input-string str read)))))
|
|
|
|
(match saved
|
|
|
|
((idx gems)
|
|
|
|
(set! *level-idx* idx)
|
|
|
|
(set! *gems* gems)
|
2024-12-12 13:28:48 -05:00
|
|
|
(set-level! *level-idx*)
|
2024-05-26 15:23:41 -04:00
|
|
|
(media-play audio:bg-music)))))
|
2024-05-20 22:12:35 -04:00
|
|
|
|
2024-12-12 12:47:54 -05:00
|
|
|
;; Menu types
|
2024-12-13 15:31:19 -05:00
|
|
|
|
2024-12-12 12:47:54 -05:00
|
|
|
(define-record-type <menu>
|
|
|
|
(make-menu name items)
|
|
|
|
menu?
|
|
|
|
(name menu-name)
|
|
|
|
(items menu-items))
|
|
|
|
|
2024-12-13 15:31:19 -05:00
|
|
|
;; Menu state
|
|
|
|
|
|
|
|
(define-record-type <menu-state>
|
|
|
|
(make-menu-state current index page history)
|
|
|
|
menu-state?
|
|
|
|
(current menu-state-current set-menu-state-current!)
|
|
|
|
(index menu-state-index set-menu-state-index!)
|
|
|
|
(page menu-state-page set-menu-state-page!)
|
|
|
|
(history menu-state-history set-menu-state-history!))
|
|
|
|
|
2024-12-20 09:16:30 -05:00
|
|
|
(define (menu-action:submenu menu)
|
|
|
|
(lambda ()
|
|
|
|
(push-menu-history!)
|
|
|
|
(set-menu! menu)
|
|
|
|
(set-menu-index! -1)))
|
|
|
|
|
|
|
|
(define (menu-action:load-level level)
|
|
|
|
(lambda ()
|
|
|
|
(hide-menu!)
|
|
|
|
(load-level! level)))
|
|
|
|
|
|
|
|
(define (menu-action:credits)
|
|
|
|
(hide-menu!)
|
|
|
|
(set! *level-last* *level-idx*)
|
|
|
|
(load-level! (vector-length levels)))
|
2024-12-13 15:31:19 -05:00
|
|
|
|
2024-12-12 12:47:54 -05:00
|
|
|
;; Menu constants
|
2024-12-13 15:31:19 -05:00
|
|
|
(define center (vec2 (* 10.0 tile-width) (* 7.0 tile-height)))
|
2024-12-12 12:47:54 -05:00
|
|
|
(define menu:max-items 10)
|
|
|
|
(define menu:level-select
|
|
|
|
(let ((items (make-vector (vector-length levels))))
|
|
|
|
(do ((i 0 (1+ i)))
|
|
|
|
((= i (vector-length levels)))
|
2024-12-20 09:16:30 -05:00
|
|
|
(vector-set! items i (cons (string-append "Level " (number->string i))
|
|
|
|
(menu-action:load-level i))))
|
2024-12-12 12:47:54 -05:00
|
|
|
(make-menu "Select Level" items)))
|
|
|
|
(define menu:main
|
2024-12-20 09:16:30 -05:00
|
|
|
(make-menu "Menu" (vector (cons "Select Level"
|
|
|
|
(menu-action:submenu menu:level-select))
|
|
|
|
(cons "Credits" menu-action:credits))))
|
2024-12-12 12:47:54 -05:00
|
|
|
|
2024-12-20 08:50:40 -05:00
|
|
|
;; -1 for the index means 'Back' will be indicated first
|
2024-12-17 13:59:47 -05:00
|
|
|
(define *menu* (make-menu-state menu:main -1 0 '()))
|
|
|
|
|
|
|
|
(define (current-menu)
|
|
|
|
(menu-state-current *menu*))
|
|
|
|
(define (current-menu-index)
|
|
|
|
(menu-state-index *menu*))
|
|
|
|
(define (current-menu-page)
|
|
|
|
(menu-state-page *menu*))
|
|
|
|
(define (current-menu-history)
|
|
|
|
(menu-state-history *menu*))
|
|
|
|
(define (set-menu! menu)
|
|
|
|
(set-menu-state-current! *menu* menu))
|
|
|
|
(define (set-menu-index! index)
|
|
|
|
(set-menu-state-index! *menu* index))
|
|
|
|
(define (set-menu-page! page)
|
|
|
|
(set-menu-state-page! *menu* page))
|
|
|
|
(define (set-menu-history! history)
|
|
|
|
(set-menu-state-history! *menu* history))
|
|
|
|
|
2024-12-12 12:47:54 -05:00
|
|
|
;; Menu commands
|
2024-12-20 10:21:10 -05:00
|
|
|
(define* (show-menu! #:optional (menu menu:main))
|
2024-12-17 13:59:47 -05:00
|
|
|
(push-game-state! 'menu)
|
2024-12-20 10:21:10 -05:00
|
|
|
(set-menu! menu)
|
2024-12-13 15:31:19 -05:00
|
|
|
(set-menu-index! -1)
|
|
|
|
(set-menu-page! 0)
|
|
|
|
(set-menu-history! '()))
|
|
|
|
|
|
|
|
(define (hide-menu!)
|
2024-12-17 13:59:47 -05:00
|
|
|
(pop-game-state!))
|
2024-12-11 17:44:00 -05:00
|
|
|
|
|
|
|
(define (menu-up!)
|
2024-12-13 15:31:19 -05:00
|
|
|
(set-menu-index! (max -1 (1- (current-menu-index))))
|
2024-12-20 10:14:44 -05:00
|
|
|
(when (and (> (current-menu-page) 0)
|
|
|
|
(= (current-menu-index) (- (* (current-menu-page) menu:max-items) (current-menu-page) 1)))
|
|
|
|
(set-menu-page! (1- (current-menu-page)))))
|
2024-12-11 17:44:00 -05:00
|
|
|
|
|
|
|
(define (menu-down!)
|
2024-12-13 15:31:19 -05:00
|
|
|
(set-menu-index! (min (1- (vector-length (menu-items (current-menu)))) (1+ (current-menu-index))))
|
2024-12-20 10:14:44 -05:00
|
|
|
(when (= (current-menu-index) (- (* (1+ (current-menu-page)) menu:max-items) (current-menu-page)))
|
|
|
|
(set-menu-page! (1+ (current-menu-page)))))
|
2024-12-13 15:31:19 -05:00
|
|
|
|
|
|
|
(define (push-menu-history!)
|
|
|
|
(set-menu-history! (cons (cons (current-menu)
|
|
|
|
(current-menu-index))
|
|
|
|
(current-menu-history))))
|
|
|
|
|
|
|
|
(define (pop-menu-history!)
|
|
|
|
(match (current-menu-history)
|
|
|
|
(() (hide-menu!))
|
|
|
|
(((prev . index) . rest)
|
|
|
|
(set-menu! prev)
|
|
|
|
(set-menu-index! index)
|
|
|
|
(set-menu-history! rest))))
|
2024-12-11 17:44:00 -05:00
|
|
|
|
|
|
|
(define (menu-select!)
|
2024-12-20 09:16:30 -05:00
|
|
|
(if (= (current-menu-index) -1) ;; back button pressed
|
2024-12-13 15:31:19 -05:00
|
|
|
(pop-menu-history!)
|
2024-12-20 09:16:30 -05:00
|
|
|
((cdr (vector-ref (menu-items (current-menu)) (current-menu-index))))))
|
2024-12-11 10:13:20 -05:00
|
|
|
|
2024-05-18 14:04:35 -04:00
|
|
|
(define (reset-game!)
|
2024-05-23 16:53:23 -04:00
|
|
|
(run-script
|
|
|
|
(lambda ()
|
2024-12-31 13:14:17 -05:00
|
|
|
(replace-game-state! 'interstitial)
|
2024-05-26 12:35:46 -04:00
|
|
|
(yield
|
|
|
|
(lambda (k)
|
|
|
|
(show-effect! (make-fade-out+in-effect 2.0 k))))
|
2024-05-26 20:51:13 -04:00
|
|
|
(set! *level-idx* 0)
|
2024-12-12 13:28:48 -05:00
|
|
|
(set-level! 0))))
|
2024-05-18 14:04:35 -04:00
|
|
|
|
2024-05-26 20:40:42 -04:00
|
|
|
(define (emit-pickup-particles x y)
|
|
|
|
(run-script
|
|
|
|
(lambda ()
|
|
|
|
(do ((i 0 (1+ i)))
|
|
|
|
((= i 24))
|
|
|
|
(let ((angle (* (random) 2.0 pi))
|
|
|
|
(r (/ tile-width 2.0)))
|
|
|
|
(particle-pool-add! particles 140 2
|
|
|
|
(+ (* x tile-width)
|
|
|
|
(/ tile-width 2.0)
|
|
|
|
(* (cos angle) r))
|
|
|
|
(+ (* y tile-height)
|
|
|
|
(/ tile-height 2.0)
|
|
|
|
(* (sin angle) r))
|
|
|
|
0.0 0.0))
|
|
|
|
(wait 1)))))
|
|
|
|
|
|
|
|
(define (emit-electric-switch-particles x y)
|
|
|
|
(run-script
|
|
|
|
(lambda ()
|
|
|
|
(do ((i 0 (1+ i)))
|
|
|
|
((= i 4))
|
|
|
|
(let ((angle (+ (* (random) pi) pi))
|
|
|
|
(speed (+ (random) 3.0)))
|
|
|
|
(particle-pool-add! particles 141 6
|
|
|
|
(+ (* x tile-width)
|
|
|
|
(/ tile-width 2.0))
|
|
|
|
(+ (* y tile-height) 3.0)
|
|
|
|
(* (cos angle) speed)
|
|
|
|
(* (sin angle) speed)))
|
|
|
|
(wait 1)))))
|
|
|
|
|
|
|
|
(define (emit-warp-particles x y)
|
|
|
|
(run-script
|
|
|
|
(lambda ()
|
|
|
|
(do ((i 0 (1+ i)))
|
|
|
|
((= i 4))
|
|
|
|
(particle-pool-add! particles 142 6
|
|
|
|
(+ (* x tile-width)
|
|
|
|
(/ tile-width 2.0)
|
|
|
|
(- (* (random) 6.0) 3.0))
|
|
|
|
(+ (* y tile-height) tile-height)
|
|
|
|
0.0
|
|
|
|
(- (* (random) -2.0) 3.0))
|
|
|
|
(wait 2)))))
|
|
|
|
|
|
|
|
(define (emit-explosion-particles x y)
|
|
|
|
(run-script
|
|
|
|
(lambda ()
|
|
|
|
(do ((i 0 (1+ i)))
|
|
|
|
((= i 16))
|
|
|
|
(do ((j 0 (1+ j)))
|
|
|
|
((= j 2))
|
|
|
|
(let ((dx (- (* (random) tile-width 3.0) tile-width))
|
|
|
|
(dy (- (* (random) tile-height 3.0) tile-height)))
|
|
|
|
(particle-pool-add! particles 51 8
|
|
|
|
(+ (* x tile-width) dx)
|
|
|
|
(+ (* y tile-height) dy)
|
|
|
|
0.0 0.0)))
|
|
|
|
(wait 1)))))
|
|
|
|
|
2024-05-18 14:04:35 -04:00
|
|
|
;; Update loop
|
|
|
|
(define (move-player dir)
|
2024-05-22 21:53:17 -04:00
|
|
|
(define level-complete? #f)
|
|
|
|
(with-goblins
|
|
|
|
(let ((player (level-player *level*))
|
|
|
|
(level (level-actor *level*)))
|
|
|
|
(cond
|
|
|
|
(($ player 'alive?)
|
|
|
|
(begin
|
|
|
|
($ player 'move dir)
|
|
|
|
($ level 'tick)
|
|
|
|
(let lp ((events ($ level 'flush-events)))
|
|
|
|
(match events
|
|
|
|
(() (values))
|
|
|
|
((event . rest)
|
2024-05-26 19:49:11 -04:00
|
|
|
(match event
|
2024-05-22 21:53:17 -04:00
|
|
|
(('bump x y)
|
|
|
|
(play-sound-effect audio:bump))
|
|
|
|
(('push x y)
|
|
|
|
(play-sound-effect audio:push))
|
|
|
|
(('exit x y)
|
|
|
|
(play-sound-effect audio:exit)
|
|
|
|
(set! level-complete? #t))
|
|
|
|
(('player-death x y)
|
|
|
|
(play-sound-effect audio:die))
|
|
|
|
(('pickup x y)
|
2024-05-26 20:40:42 -04:00
|
|
|
(play-sound-effect audio:pickup)
|
|
|
|
(emit-pickup-particles x y))
|
2024-05-23 09:10:25 -04:00
|
|
|
(('emit x y)
|
|
|
|
(play-sound-effect audio:emit))
|
2024-05-23 15:07:48 -04:00
|
|
|
(('emitter-on x y)
|
|
|
|
(play-sound-effect audio:emitter-on))
|
|
|
|
(('emitter-off x y)
|
|
|
|
(play-sound-effect audio:emitter-off))
|
2024-05-23 11:43:59 -04:00
|
|
|
(('gate-open x y)
|
2024-05-23 07:48:53 -04:00
|
|
|
(play-sound-effect audio:gate))
|
2024-05-23 11:43:59 -04:00
|
|
|
(('gate-close x y)
|
|
|
|
(play-sound-effect audio:gate)
|
|
|
|
(show-effect! (make-screen-shake-effect 0.05)))
|
2024-05-23 09:01:47 -04:00
|
|
|
((or ('floor-switch-on x y) ('floor-switch-off x y))
|
|
|
|
(play-sound-effect audio:floor-switch))
|
2024-05-22 21:53:17 -04:00
|
|
|
(('electric-switch-on x y)
|
2024-05-26 20:40:42 -04:00
|
|
|
(play-sound-effect audio:electric-switch-on)
|
|
|
|
(emit-electric-switch-particles x y))
|
2024-05-22 21:53:17 -04:00
|
|
|
(('electric-switch-off x y)
|
|
|
|
(play-sound-effect audio:electric-switch-off))
|
2024-05-23 07:48:53 -04:00
|
|
|
(('receive-electron x y)
|
2024-05-26 20:40:42 -04:00
|
|
|
(play-sound-effect audio:warp 0.25)
|
|
|
|
(emit-warp-particles x y))
|
2024-05-24 15:23:34 -04:00
|
|
|
(('explosion x y)
|
2024-05-25 16:42:39 -04:00
|
|
|
(play-sound-effect audio:explosion)
|
2024-05-26 16:46:50 -04:00
|
|
|
(show-effect! (make-screen-shake-effect 0.2))
|
2024-05-26 20:40:42 -04:00
|
|
|
(emit-explosion-particles x y))
|
2024-05-22 21:53:17 -04:00
|
|
|
(_ (values)))
|
|
|
|
(lp rest))))
|
|
|
|
(update-objects!)
|
|
|
|
(save-snapshot!)))
|
|
|
|
(else
|
|
|
|
(play-sound-effect audio:no)))))
|
|
|
|
(when level-complete? (next-level!)))
|
2024-05-18 14:04:35 -04:00
|
|
|
|
2024-05-17 13:08:43 -04:00
|
|
|
(define dt (/ 1000.0 60.0)) ; aim for updating at 60Hz
|
|
|
|
(define (update)
|
2024-12-10 11:32:14 -05:00
|
|
|
(maybe-poll-gamepad)
|
2024-05-23 11:43:59 -04:00
|
|
|
(scheduler-tick! (current-scheduler))
|
2024-05-26 16:46:50 -04:00
|
|
|
(particle-pool-update! particles)
|
2024-05-17 13:08:43 -04:00
|
|
|
(timeout update-callback dt))
|
|
|
|
(define update-callback (procedure->external update))
|
|
|
|
|
2024-05-26 16:10:46 -04:00
|
|
|
;; Rendering
|
2024-05-23 11:43:59 -04:00
|
|
|
(define *current-effect* #f)
|
|
|
|
(define (show-effect! effect)
|
|
|
|
(set! *current-effect* effect)
|
|
|
|
(effect-start! effect))
|
|
|
|
(define (draw-current-effect type)
|
|
|
|
(when (and *current-effect*
|
|
|
|
(eq? type (effect-type *current-effect*)))
|
|
|
|
(draw-effect context *current-effect*)
|
|
|
|
(unless (effect-started? *current-effect*)
|
|
|
|
(set! *current-effect* #f))))
|
|
|
|
|
2024-05-25 06:52:59 -04:00
|
|
|
(define-syntax-rule (define-animation name (tile duration) ...)
|
|
|
|
(define name
|
|
|
|
(make-animation tileset (vector (make-frame tile duration) ...))))
|
|
|
|
(define-animation anim:player
|
2024-05-26 15:08:37 -04:00
|
|
|
(0 6.75)
|
|
|
|
(40 0.15))
|
2024-05-25 06:52:59 -04:00
|
|
|
(define-animation anim:electron-head
|
2024-05-26 15:08:37 -04:00
|
|
|
(4 .25)
|
|
|
|
(57 .25)
|
|
|
|
(77 .25)
|
|
|
|
(97 .25))
|
2024-05-25 06:52:59 -04:00
|
|
|
(define-animation anim:electron-tail
|
2024-05-26 15:08:37 -04:00
|
|
|
(5 .25)
|
|
|
|
(58 .25)
|
|
|
|
(78 .25)
|
|
|
|
(98 .25))
|
2024-05-25 06:52:59 -04:00
|
|
|
(define-animation anim:gem
|
2024-05-26 15:08:37 -04:00
|
|
|
(28 .25)
|
|
|
|
(55 .25)
|
|
|
|
(75 .25)
|
|
|
|
(95 .25))
|
2024-05-25 06:52:59 -04:00
|
|
|
(define-animation anim:ghost-gem
|
2024-05-26 15:08:37 -04:00
|
|
|
(49 .25)
|
|
|
|
(56 .25)
|
|
|
|
(76 .25)
|
|
|
|
(96 .25))
|
2024-05-26 15:36:39 -04:00
|
|
|
(define-animation anim:and-gate
|
|
|
|
(42 .4)
|
|
|
|
(115 .4)
|
|
|
|
(135 .4)
|
|
|
|
(155 .4))
|
|
|
|
(define-animation anim:or-gate
|
|
|
|
(43 .4)
|
|
|
|
(116 .4)
|
|
|
|
(136 .4)
|
|
|
|
(156 .4))
|
|
|
|
(define-animation anim:xor-gate
|
|
|
|
(44 .4)
|
|
|
|
(117 .4)
|
|
|
|
(137 .4)
|
|
|
|
(157 .4))
|
2024-05-26 16:10:46 -04:00
|
|
|
(define-animation anim:bomb-lit
|
2024-06-04 16:20:33 -04:00
|
|
|
(53 .2)
|
|
|
|
(73 .2)
|
|
|
|
(93 .2)
|
|
|
|
(113 .2))
|
2024-05-26 16:10:46 -04:00
|
|
|
(define-animation anim:bomb-uh-oh
|
2024-06-04 16:20:33 -04:00
|
|
|
(54 .1)
|
|
|
|
(74 .1)
|
|
|
|
(94 .1)
|
|
|
|
(114 .1))
|
2024-05-26 16:10:46 -04:00
|
|
|
|
2024-05-26 16:46:50 -04:00
|
|
|
(define particles (make-particle-pool 512 tileset))
|
2024-05-25 06:52:59 -04:00
|
|
|
|
2024-05-17 13:08:43 -04:00
|
|
|
(define number->string*
|
2024-05-23 11:43:59 -04:00
|
|
|
(let ((cache (make-eq-hashtable))) ; assuming fixnums only
|
2024-05-17 13:08:43 -04:00
|
|
|
(lambda (x)
|
|
|
|
(or (hashtable-ref cache x)
|
|
|
|
(let ((str (number->string x)))
|
|
|
|
(hashtable-set! cache x str)
|
|
|
|
str)))))
|
|
|
|
|
2024-05-26 15:36:39 -04:00
|
|
|
(define (draw-rotated-animation anim pos angle)
|
2024-05-23 16:08:25 -04:00
|
|
|
(let ((hw (* tile-width 0.5))
|
|
|
|
(hh (* tile-height 0.5)))
|
|
|
|
(save! context)
|
|
|
|
(translate! context (+ (vec2-x pos) hw) (+ (vec2-y pos) hh))
|
|
|
|
(rotate! context angle)
|
2024-05-26 15:36:39 -04:00
|
|
|
(draw-animation context anim (- hw) (- hh))
|
2024-05-23 16:08:25 -04:00
|
|
|
(restore! context)))
|
|
|
|
|
2024-05-22 18:21:45 -04:00
|
|
|
(define (draw-player pos alive?)
|
2024-05-25 06:52:59 -04:00
|
|
|
(if alive?
|
|
|
|
(draw-animation context anim:player (vec2-x pos) (vec2-y pos))
|
|
|
|
(draw-tile context tileset 20 (vec2-x pos) (vec2-y pos))))
|
2024-05-20 22:12:35 -04:00
|
|
|
|
|
|
|
(define (draw-exit pos)
|
|
|
|
(draw-tile context tileset 27 (vec2-x pos) (vec2-y pos)))
|
|
|
|
|
2024-05-22 14:42:27 -04:00
|
|
|
(define (draw-wire-state pos state)
|
2024-05-20 22:12:35 -04:00
|
|
|
(let ((x (vec2-x pos))
|
|
|
|
(y (vec2-y pos)))
|
2024-05-22 14:42:27 -04:00
|
|
|
(match state
|
2024-05-20 22:12:35 -04:00
|
|
|
('electron-head
|
2024-05-25 06:52:59 -04:00
|
|
|
(draw-animation context anim:electron-head x y))
|
2024-05-20 22:12:35 -04:00
|
|
|
('electron-tail
|
2024-05-25 06:52:59 -04:00
|
|
|
(draw-animation context anim:electron-tail x y))
|
2024-05-20 22:12:35 -04:00
|
|
|
(_ #f))))
|
|
|
|
|
2024-05-22 14:42:27 -04:00
|
|
|
(define (draw-wall pos type)
|
|
|
|
(draw-wire-state pos type))
|
|
|
|
|
2024-05-21 22:12:21 -04:00
|
|
|
(define (draw-block pos type)
|
2024-05-20 22:12:35 -04:00
|
|
|
(let ((x (vec2-x pos))
|
|
|
|
(y (vec2-y pos)))
|
|
|
|
(match type
|
|
|
|
('crate (draw-tile context tileset 29 x y))
|
|
|
|
(_ (draw-tile context tileset 3 x y)))
|
2024-05-22 14:42:27 -04:00
|
|
|
(draw-wire-state pos type)))
|
2024-05-20 22:12:35 -04:00
|
|
|
|
2024-05-24 15:23:34 -04:00
|
|
|
(define (draw-brick pos)
|
2024-05-24 12:46:15 -04:00
|
|
|
(draw-tile context tileset 22 (vec2-x pos) (vec2-y pos)))
|
|
|
|
|
2024-05-24 15:27:24 -04:00
|
|
|
(define (draw-clock-emitter pos state)
|
|
|
|
(draw-tile context tileset 48 (vec2-x pos) (vec2-y pos))
|
|
|
|
(set-global-alpha! context 0.5)
|
|
|
|
(draw-wire-state pos state)
|
|
|
|
(set-global-alpha! context 1.0))
|
2024-05-20 22:12:35 -04:00
|
|
|
|
2024-05-24 15:27:24 -04:00
|
|
|
(define (draw-switched-emitter pos state)
|
|
|
|
(draw-tile context tileset (if state 48 47) (vec2-x pos) (vec2-y pos))
|
|
|
|
(set-global-alpha! context 0.5)
|
|
|
|
(draw-wire-state pos state)
|
|
|
|
(set-global-alpha! context 1.0))
|
2024-05-22 15:28:42 -04:00
|
|
|
|
2024-05-21 14:10:57 -04:00
|
|
|
(define (draw-floor-switch pos on?)
|
|
|
|
(draw-tile context tileset (if on? 25 24) (vec2-x pos) (vec2-y pos)))
|
|
|
|
|
2024-05-24 15:23:34 -04:00
|
|
|
(define (draw-bomb pos countdown)
|
2024-05-26 16:10:46 -04:00
|
|
|
(let ((x (vec2-x pos))
|
|
|
|
(y (vec2-y pos)))
|
|
|
|
(match countdown
|
|
|
|
(-1 (draw-tile context tileset 50 x y))
|
|
|
|
(1 (draw-animation context anim:bomb-uh-oh x y))
|
|
|
|
(_ (draw-animation context anim:bomb-lit x y)))))
|
2024-05-24 12:46:15 -04:00
|
|
|
|
2024-05-21 14:10:57 -04:00
|
|
|
(define (draw-gem pos)
|
2024-05-25 06:52:59 -04:00
|
|
|
(draw-animation context anim:gem (vec2-x pos) (vec2-y pos)))
|
2024-05-21 14:10:57 -04:00
|
|
|
|
2024-05-22 18:34:29 -04:00
|
|
|
(define (draw-ghost-gem pos)
|
|
|
|
(set-global-alpha! context 0.5)
|
2024-05-25 06:52:59 -04:00
|
|
|
(draw-animation context anim:ghost-gem (vec2-x pos) (vec2-y pos))
|
2024-05-22 18:34:29 -04:00
|
|
|
(set-global-alpha! context 1.0))
|
|
|
|
|
2024-05-21 17:28:57 -04:00
|
|
|
(define (draw-gate pos open?)
|
|
|
|
(draw-tile context tileset (if open? 46 45) (vec2-x pos) (vec2-y pos)))
|
|
|
|
|
2024-05-26 15:36:39 -04:00
|
|
|
(define (draw-logic-gate pos direction state anim)
|
2024-05-22 14:42:27 -04:00
|
|
|
(let ((x (vec2-x pos))
|
|
|
|
(y (vec2-y pos)))
|
|
|
|
(draw-tile context tileset 2 x y)
|
2024-05-23 16:08:25 -04:00
|
|
|
(match direction
|
2024-05-26 15:36:39 -04:00
|
|
|
('right (draw-animation context anim x y))
|
|
|
|
('left (draw-rotated-animation anim pos pi))
|
|
|
|
('up (draw-rotated-animation anim pos (* pi 1.5)))
|
|
|
|
('down (draw-rotated-animation anim pos (* pi 0.5))))
|
2024-05-22 14:42:27 -04:00
|
|
|
(draw-wire-state pos state)))
|
2024-05-22 12:07:41 -04:00
|
|
|
|
2024-05-22 08:00:39 -04:00
|
|
|
(define (draw-electric-switch pos on?)
|
|
|
|
(draw-tile context tileset (if on? 7 6) (vec2-x pos) (vec2-y pos)))
|
|
|
|
|
2024-05-22 12:35:56 -04:00
|
|
|
(define (draw-electron-warp pos state)
|
|
|
|
(draw-tile context tileset 71 (vec2-x pos) (vec2-y pos))
|
|
|
|
(draw-wire-state pos state))
|
|
|
|
|
2024-05-20 22:12:35 -04:00
|
|
|
(define (draw-object obj)
|
|
|
|
(match obj
|
|
|
|
(#f #f)
|
2024-05-22 18:21:45 -04:00
|
|
|
(('player pos alive?) (draw-player pos alive?))
|
2024-05-22 08:00:39 -04:00
|
|
|
(('exit pos) #t) ; drawn via background
|
2024-05-21 22:12:21 -04:00
|
|
|
(('wall pos type) (draw-wall pos type))
|
|
|
|
(('block pos type) (draw-block pos type))
|
2024-05-24 15:23:34 -04:00
|
|
|
(('brick pos) (draw-brick pos))
|
2024-05-24 15:27:24 -04:00
|
|
|
(('clock-emitter pos state) (draw-clock-emitter pos state))
|
|
|
|
(('switched-emitter pos state) (draw-switched-emitter pos state))
|
2024-05-21 14:10:57 -04:00
|
|
|
(('floor-switch pos on?) (draw-floor-switch pos on?))
|
2024-05-24 15:23:34 -04:00
|
|
|
(('bomb pos countdown) (draw-bomb pos countdown))
|
2024-05-21 17:28:57 -04:00
|
|
|
(('gem pos) (draw-gem pos))
|
2024-05-22 18:34:29 -04:00
|
|
|
(('ghost-gem pos) (draw-ghost-gem pos))
|
2024-05-22 08:00:39 -04:00
|
|
|
(('gate pos open?) (draw-gate pos open?))
|
2024-05-26 15:36:39 -04:00
|
|
|
(('and-gate pos direction state) (draw-logic-gate pos direction state anim:and-gate))
|
|
|
|
(('or-gate pos direction state) (draw-logic-gate pos direction state anim:or-gate))
|
|
|
|
(('xor-gate pos direction state) (draw-logic-gate pos direction state anim:xor-gate))
|
2024-05-22 12:35:56 -04:00
|
|
|
(('electric-switch pos on?) (draw-electric-switch pos on?))
|
|
|
|
(('electron-warp pos state) (draw-electron-warp pos state))))
|
2024-05-18 14:04:35 -04:00
|
|
|
|
2024-05-19 17:30:36 -04:00
|
|
|
(define (draw-background)
|
2024-05-22 14:22:27 -04:00
|
|
|
(let ((bg (level-background *level*))
|
|
|
|
(k (* level-width level-height)))
|
|
|
|
(do ((i 0 (1+ i)))
|
|
|
|
((= i k))
|
|
|
|
(let* ((tile (vector-ref bg i))
|
|
|
|
(pos (level-tile-position tile))
|
|
|
|
(id (level-tile-id tile)))
|
|
|
|
(draw-tile context tileset id (vec2-x pos) (vec2-y pos))))))
|
2024-05-19 17:30:36 -04:00
|
|
|
|
2024-12-11 17:44:00 -05:00
|
|
|
|
2024-12-11 10:13:20 -05:00
|
|
|
(define (draw-menu)
|
2024-12-11 17:44:00 -05:00
|
|
|
;; Height (in tiles) will be 1 for the menu title + the y padding
|
2024-12-12 12:47:54 -05:00
|
|
|
;; + 1 for the back button if on the first page or ellipses otherwise
|
|
|
|
;; + num menu items + 1 for ellipses if num items is too big
|
|
|
|
;; + the y padding again
|
2024-12-11 17:44:00 -05:00
|
|
|
(let* ((padding-y 1)
|
|
|
|
(text-offset-y (* 0.75 tile-height))
|
2024-12-31 13:15:03 -05:00
|
|
|
(width 8.0)
|
2024-12-13 15:31:19 -05:00
|
|
|
(num-items (vector-length (menu-items (current-menu))))
|
2024-12-12 12:47:54 -05:00
|
|
|
(height (+ 2 ;; Menu title + back/ellipses
|
|
|
|
(* 2 padding-y) ;; Padding
|
2024-12-13 15:31:19 -05:00
|
|
|
(if (> num-items menu:max-items)
|
2024-12-20 08:50:40 -05:00
|
|
|
(1+ menu:max-items) ;; bottom ellipses
|
2024-12-13 15:31:19 -05:00
|
|
|
num-items)))
|
2024-12-20 08:50:40 -05:00
|
|
|
(y-start (- (vec2-y center) (* tile-height
|
|
|
|
(floor (/ height 2)))))
|
|
|
|
(x-start (- (vec2-x center) (* tile-width
|
|
|
|
(floor (/ width 2))))))
|
|
|
|
|
|
|
|
;; Draw menu background
|
|
|
|
(let ((w (* tile-width width))
|
|
|
|
(h (* tile-height height)))
|
|
|
|
(set-fill-color! context "#000")
|
|
|
|
(fill-rect context x-start y-start w h)
|
|
|
|
(set-stroke-color! context "blue")
|
|
|
|
(stroke-rect context x-start y-start w h))
|
|
|
|
|
|
|
|
;; Draw menu text
|
|
|
|
(set-font! context "normal 16px monogram")
|
|
|
|
(set-fill-color! context "#fff")
|
|
|
|
(let* (;; The first menu item is at index 0. The 'Back' or ellipses are
|
|
|
|
;; directly above the first menu item at index -1. The menu title
|
|
|
|
;; is above the 'Back' button and separated by the padding-y
|
|
|
|
(r-start (- -2 padding-y))
|
|
|
|
;; end of text
|
|
|
|
(r-end (- (+ r-start height) padding-y))
|
|
|
|
;; r will not take into account which page you are on
|
|
|
|
(r-page-offset (- (* (current-menu-page) menu:max-items) (current-menu-page)))
|
2024-12-31 13:15:03 -05:00
|
|
|
(x-gutter (+ tile-width x-start))
|
|
|
|
(x-text (+ tile-width x-gutter)))
|
2024-12-20 08:50:40 -05:00
|
|
|
(do ((r r-start (1+ r)) (y y-start (+ tile-height y)))
|
|
|
|
((or (>= r r-end) (>= (+ r r-page-offset) num-items)))
|
2024-12-31 13:15:03 -05:00
|
|
|
(let ((y-text (+ y text-offset-y))
|
|
|
|
(r-page (+ r r-page-offset)))
|
|
|
|
;; Draw menu title
|
|
|
|
(when (= r r-start)
|
|
|
|
(set-text-align! context "center")
|
|
|
|
(fill-text context (menu-name (current-menu))
|
|
|
|
(vec2-x center) y-text))
|
|
|
|
(set-text-align! context "left")
|
|
|
|
;; indicator
|
|
|
|
(when (= (+ r r-page-offset) (current-menu-index))
|
|
|
|
(fill-text context "▸" x-gutter y-text))
|
|
|
|
;; Menu items
|
|
|
|
(when (>= r -1)
|
|
|
|
(fill-text
|
|
|
|
context
|
|
|
|
(cond
|
|
|
|
((= r-page -1) "Back")
|
|
|
|
((or (= r -1) (and (= r (1- r-end))
|
|
|
|
(< r-page (1- num-items))))
|
|
|
|
"...")
|
|
|
|
(else
|
|
|
|
(car (vector-ref (menu-items (current-menu))
|
|
|
|
r-page))))
|
|
|
|
x-text y-text)))))))
|
2024-12-11 10:13:20 -05:00
|
|
|
|
2024-12-20 10:14:44 -05:00
|
|
|
(define (draw-controls)
|
|
|
|
(let ((cx1 (/ game-width 4.0))
|
|
|
|
(cx2 (* game-width 0.75))
|
|
|
|
(baseline (/ game-height 2.0)))
|
|
|
|
(set-fill-color! context "#ffffff")
|
|
|
|
(set-text-align! context "center")
|
|
|
|
(set-font! context "normal 16px monogram")
|
|
|
|
(fill-text context "keyboard:"
|
|
|
|
cx1 (- baseline 32.0))
|
|
|
|
(fill-text context "arrows -> move"
|
|
|
|
cx1 (- baseline 16.0))
|
|
|
|
(fill-text context "Z -> undo"
|
|
|
|
cx1 baseline)
|
|
|
|
(fill-text context "touchscreen:"
|
|
|
|
cx2 (- baseline 32.0))
|
|
|
|
(fill-text context "dpad -> move"
|
|
|
|
cx2 (- baseline 16.0))
|
|
|
|
(fill-text context "A -> undo"
|
|
|
|
cx2 baseline)))
|
|
|
|
|
2024-05-18 14:04:35 -04:00
|
|
|
(define (draw-level)
|
2024-05-19 17:30:36 -04:00
|
|
|
(draw-background)
|
2024-05-22 18:21:45 -04:00
|
|
|
(for-each draw-object *objects*)
|
2024-05-26 16:46:50 -04:00
|
|
|
(draw-particles context particles)
|
2024-05-22 18:21:45 -04:00
|
|
|
(let ((alive? (with-goblins ($ (level-player *level*) 'alive?))))
|
|
|
|
(unless alive?
|
2024-05-24 12:09:26 -04:00
|
|
|
(set-global-alpha! context 0.7)
|
|
|
|
(set-fill-color! context "#222034")
|
2024-05-22 18:21:45 -04:00
|
|
|
(fill-rect context 0.0 0.0 game-width game-height)
|
2024-05-24 12:09:26 -04:00
|
|
|
(set-global-alpha! context 1.0)
|
|
|
|
(set-font! context "normal 32px monogram")
|
2024-05-22 18:21:45 -04:00
|
|
|
(set-fill-color! context "#ffffff")
|
|
|
|
(set-text-align! context "center")
|
2024-12-20 10:14:44 -05:00
|
|
|
(fill-text context "OUCH... x_x" (/ game-width 2.0) (/ game-height 2.0))))
|
|
|
|
(when (= *level-idx* 0)
|
|
|
|
(draw-controls)))
|
2024-05-18 14:04:35 -04:00
|
|
|
|
2024-05-23 11:43:59 -04:00
|
|
|
(define (draw-interstitial)
|
|
|
|
(draw-level))
|
|
|
|
|
2024-05-23 14:05:13 -04:00
|
|
|
(define *credits-scroll* (vec2 0.0 0.0))
|
|
|
|
(define credits
|
2024-05-25 21:23:52 -04:00
|
|
|
#("Phew, you made it!"
|
|
|
|
"Time to relax."
|
2024-05-23 14:05:13 -04:00
|
|
|
#f
|
|
|
|
#f
|
|
|
|
"Cirkoban was made by the"
|
|
|
|
"Spritely Institute"
|
|
|
|
#f
|
|
|
|
"https://spritely.institute"
|
|
|
|
#f
|
2024-05-25 21:23:52 -04:00
|
|
|
"Game Design"
|
|
|
|
"-----------"
|
|
|
|
"Christine Lemmer-Webber"
|
2024-05-23 14:05:13 -04:00
|
|
|
#f
|
2024-05-25 21:23:52 -04:00
|
|
|
"Level Design"
|
|
|
|
"------------"
|
|
|
|
"Christine Lemmer-Webber"
|
2024-05-23 14:05:13 -04:00
|
|
|
"Juliana Sims"
|
2024-05-25 21:23:52 -04:00
|
|
|
"David Thompson"
|
2024-05-23 14:05:13 -04:00
|
|
|
#f
|
2024-05-25 21:23:52 -04:00
|
|
|
"Pixel Art"
|
|
|
|
"---------"
|
2024-05-23 14:05:13 -04:00
|
|
|
"Christine Lemmer-Webber"
|
|
|
|
#f
|
2024-05-25 21:23:52 -04:00
|
|
|
"Music"
|
|
|
|
"-----"
|
2024-05-26 12:49:27 -04:00
|
|
|
"EncryptedWhispers"
|
|
|
|
"Christine Lemmer-Webber"
|
2024-05-25 21:23:52 -04:00
|
|
|
#f
|
|
|
|
"Programming"
|
|
|
|
"-----------"
|
|
|
|
"Juliana Sims"
|
|
|
|
"David Thompson"
|
|
|
|
#f
|
|
|
|
"Other"
|
|
|
|
"-----"
|
2024-05-23 14:05:13 -04:00
|
|
|
"monogram font by datagoblin"
|
|
|
|
#f
|
|
|
|
#f
|
|
|
|
#f
|
|
|
|
#f
|
|
|
|
#f
|
|
|
|
#f
|
|
|
|
#f
|
|
|
|
#f
|
|
|
|
"Thank you for playing!"))
|
|
|
|
(define credits-line-spacing 16.0)
|
|
|
|
(define max-credits-scroll
|
|
|
|
(+ game-height (* (- (vector-length credits) 9) credits-line-spacing)))
|
2024-12-20 10:14:44 -05:00
|
|
|
(define (draw-credits)
|
2024-05-23 14:05:13 -04:00
|
|
|
(draw-level)
|
|
|
|
(set-fill-color! context "#ffffff")
|
|
|
|
(set-text-align! context "center")
|
2024-05-23 16:48:46 -04:00
|
|
|
(set-font! context "normal 16px monogram")
|
2024-05-23 14:05:13 -04:00
|
|
|
(set-vec2-y! *credits-scroll*
|
2024-05-23 14:08:02 -04:00
|
|
|
(min (+ (vec2-y *credits-scroll*) 0.5)
|
2024-05-23 14:05:13 -04:00
|
|
|
max-credits-scroll))
|
|
|
|
(let* ((x (* game-width 0.7))
|
|
|
|
(lines-on-screen 15)
|
|
|
|
(scroll-y (vec2-y *credits-scroll*))
|
|
|
|
;; TODO: Only render the lines on screen.
|
|
|
|
(start 0)
|
|
|
|
(end (vector-length credits)))
|
|
|
|
(let lp ((i start) (y (- game-height scroll-y)))
|
|
|
|
(when (< i end)
|
|
|
|
(match (vector-ref credits i)
|
|
|
|
(#f #f)
|
|
|
|
(str (fill-text context str x y)))
|
|
|
|
(lp (1+ i) (+ y credits-line-spacing))))))
|
2024-05-20 13:34:59 -04:00
|
|
|
|
2024-05-25 06:52:59 -04:00
|
|
|
(define *frame-time* (current-time*))
|
|
|
|
(define (draw time)
|
|
|
|
(unless (and (real? time) (inexact? time))
|
|
|
|
(error "expected flonum" time))
|
|
|
|
(let* ((time (/ time 1000.0))
|
|
|
|
(dt (- time *frame-time*)))
|
|
|
|
(set! *frame-time* time)
|
2024-05-25 07:27:08 -04:00
|
|
|
;; Prevent SUPER SPEED animations when the user switches away from
|
|
|
|
;; the browser for awhile. To my surprise,
|
|
|
|
;; blur/focus/visibilitychanged events *DO NOT* trigger when the
|
|
|
|
;; user switches to another program window, at least on my
|
|
|
|
;; machine, so they are useless to prevent this problem. Instead,
|
|
|
|
;; we hack: Don't update animations if dt is unreasonably high,
|
|
|
|
;; for some definition of unreasonable.
|
|
|
|
(unless (> dt 0.2)
|
|
|
|
(update-animation anim:player dt)
|
|
|
|
(update-animation anim:electron-head dt)
|
|
|
|
(update-animation anim:electron-tail dt)
|
|
|
|
(update-animation anim:gem dt)
|
2024-05-26 15:36:39 -04:00
|
|
|
(update-animation anim:ghost-gem dt)
|
|
|
|
(update-animation anim:and-gate dt)
|
|
|
|
(update-animation anim:or-gate dt)
|
2024-05-26 16:10:46 -04:00
|
|
|
(update-animation anim:xor-gate dt)
|
|
|
|
(update-animation anim:bomb-lit dt)
|
|
|
|
(update-animation anim:bomb-uh-oh dt))
|
2024-05-25 06:52:59 -04:00
|
|
|
(clear-rect context 0.0 0.0 *canvas-width* *canvas-height*)
|
|
|
|
(set-transform! context 1.0 0.0 0.0 1.0 0.0 0.0)
|
|
|
|
(scale! context *canvas-scale* *canvas-scale*)
|
|
|
|
(draw-current-effect 'pre)
|
2024-05-26 17:46:48 -04:00
|
|
|
|
2024-12-20 10:14:44 -05:00
|
|
|
(match (current-game-state)
|
|
|
|
((or 'play 'interstitial)
|
|
|
|
(draw-level))
|
|
|
|
('menu
|
|
|
|
(draw-level)
|
|
|
|
(draw-menu))
|
|
|
|
('credits (draw-credits)))
|
2024-05-25 06:52:59 -04:00
|
|
|
(draw-current-effect 'post)
|
|
|
|
(request-animation-frame draw-callback)))
|
2024-05-17 13:08:43 -04:00
|
|
|
(define draw-callback (procedure->external draw))
|
|
|
|
|
|
|
|
;; Input
|
|
|
|
(define key:left "ArrowLeft")
|
|
|
|
(define key:right "ArrowRight")
|
2024-05-18 14:04:35 -04:00
|
|
|
(define key:down "ArrowDown")
|
|
|
|
(define key:up "ArrowUp")
|
2024-05-17 13:08:43 -04:00
|
|
|
(define key:confirm "Enter")
|
2024-05-18 18:51:45 -04:00
|
|
|
(define key:undo "KeyZ")
|
2024-12-11 10:13:20 -05:00
|
|
|
(define key:menu "Space")
|
2024-05-17 13:08:43 -04:00
|
|
|
|
|
|
|
(define (on-key-down event)
|
|
|
|
(let ((key (keyboard-event-code event)))
|
2024-05-26 14:55:04 -04:00
|
|
|
(cond
|
|
|
|
((string=? key key:left)
|
|
|
|
(on-input-down 'left))
|
|
|
|
((string=? key key:right)
|
|
|
|
(on-input-down 'right))
|
|
|
|
((string=? key key:up)
|
|
|
|
(on-input-down 'up))
|
|
|
|
((string=? key key:down)
|
|
|
|
(on-input-down 'down))
|
|
|
|
((string=? key key:undo)
|
|
|
|
(on-input-down 'undo))
|
|
|
|
((string=? key key:confirm)
|
2024-12-11 10:13:20 -05:00
|
|
|
(on-input-down 'confirm))
|
|
|
|
((string=? key key:menu)
|
|
|
|
(on-input-down 'menu)))))
|
2024-05-26 14:55:04 -04:00
|
|
|
|
2024-12-10 11:32:14 -05:00
|
|
|
(define *gamepad* #f)
|
|
|
|
(define *button:left* #f)
|
|
|
|
(define *button:right* #f)
|
|
|
|
(define *button:up* #f)
|
|
|
|
(define *button:down* #f)
|
|
|
|
(define *button:undo* #f)
|
|
|
|
(define *button-state* #f)
|
|
|
|
|
|
|
|
(define (connect-gamepad! gamepad)
|
|
|
|
(when (zero? (gamepad-index gamepad))
|
|
|
|
(set! *gamepad* gamepad)
|
|
|
|
;; These indices correspond to the "standard" gamepad mapping:
|
|
|
|
;; https://w3c.github.io/gamepad/#remapping
|
|
|
|
(set! *button:left* (gamepad-button-ref gamepad 14))
|
|
|
|
(set! *button:right* (gamepad-button-ref gamepad 15))
|
|
|
|
(set! *button:up* (gamepad-button-ref gamepad 12))
|
|
|
|
(set! *button:down* (gamepad-button-ref gamepad 13))
|
|
|
|
(set! *button:undo* (gamepad-button-ref gamepad 0))
|
|
|
|
(set! *button-state* (vector #f #f #f #f #f))))
|
|
|
|
|
|
|
|
(define (disconnect-gamepad! gamepad)
|
|
|
|
(when (zero? (gamepad-index gamepad))
|
|
|
|
(set! *gamepad* #f)
|
|
|
|
(set! *button:left* #f)
|
|
|
|
(set! *button:right* #f)
|
|
|
|
(set! *button:up* #f)
|
|
|
|
(set! *button:down* #f)
|
|
|
|
(set! *button:undo* #f)
|
|
|
|
(set! *button-state* #f)))
|
|
|
|
|
|
|
|
(define (maybe-poll-gamepad)
|
|
|
|
(define (press? old new)
|
|
|
|
(and (not old) new))
|
|
|
|
(when *gamepad*
|
|
|
|
(let ((state *button-state*))
|
|
|
|
(match *button-state*
|
|
|
|
(#(prev-left prev-right prev-up prev-down prev-undo)
|
|
|
|
(let ((left (gamepad-button-pressed? *button:left*))
|
|
|
|
(right (gamepad-button-pressed? *button:right*))
|
|
|
|
(up (gamepad-button-pressed? *button:up*))
|
|
|
|
(down (gamepad-button-pressed? *button:down*))
|
|
|
|
(undo (gamepad-button-pressed? *button:undo*)))
|
|
|
|
(vector-set! *button-state* 0 left)
|
|
|
|
(vector-set! *button-state* 1 right)
|
|
|
|
(vector-set! *button-state* 2 up)
|
|
|
|
(vector-set! *button-state* 3 down)
|
|
|
|
(vector-set! *button-state* 4 undo)
|
|
|
|
(when (press? prev-left left)
|
|
|
|
(on-input-down 'left))
|
|
|
|
(when (press? prev-right right)
|
|
|
|
(on-input-down 'right))
|
|
|
|
(when (press? prev-up up)
|
|
|
|
(on-input-down 'up))
|
|
|
|
(when (press? prev-down down)
|
|
|
|
(on-input-down 'down))
|
|
|
|
(when (press? prev-undo undo)
|
|
|
|
(on-input-down 'undo))))))))
|
|
|
|
|
2024-05-26 14:55:04 -04:00
|
|
|
(define (on-input-down input)
|
2024-12-17 13:59:47 -05:00
|
|
|
(match (current-game-state)
|
|
|
|
('play
|
|
|
|
(match input
|
|
|
|
('left (move-player 'left))
|
|
|
|
('right (move-player 'right))
|
|
|
|
('up (move-player 'up))
|
|
|
|
('down (move-player 'down))
|
|
|
|
('undo
|
|
|
|
(rollback-snapshot!)
|
|
|
|
(with-goblins (update-objects!)))
|
2024-12-31 13:14:17 -05:00
|
|
|
;; REMOVE BEFORE RELEASE!!!!
|
2024-12-17 13:59:47 -05:00
|
|
|
;; ('confirm (next-level!))
|
2024-12-31 13:14:17 -05:00
|
|
|
('menu (show-menu!))
|
|
|
|
(_ #f)))
|
2024-12-17 13:59:47 -05:00
|
|
|
('menu
|
|
|
|
(match input
|
|
|
|
('up (menu-up!))
|
|
|
|
('down (menu-down!))
|
|
|
|
('confirm (menu-select!))
|
|
|
|
('menu (hide-menu!))
|
|
|
|
(_ #f)))
|
|
|
|
;; Pressing any bound input resets the game.
|
2024-12-20 10:14:44 -05:00
|
|
|
;; If traveling to the credits via the menu, go back to '*level-last*'
|
2024-12-31 13:14:17 -05:00
|
|
|
('credits
|
|
|
|
(cond
|
|
|
|
(*level-last*
|
|
|
|
(load-level! *level-last*)
|
|
|
|
(set! *level-last* #f))
|
|
|
|
(else (reset-game!))))))
|
2024-05-17 13:08:43 -04:00
|
|
|
|
|
|
|
;; Canvas and event loop setup
|
|
|
|
(define canvas (get-element-by-id "canvas"))
|
|
|
|
(define context (get-context canvas "2d"))
|
2024-05-18 14:04:35 -04:00
|
|
|
|
|
|
|
(define (resize-canvas)
|
|
|
|
(let* ((win (current-window))
|
|
|
|
(w (window-inner-width win))
|
|
|
|
(h (window-inner-height win))
|
|
|
|
(gw (inexact->exact game-width))
|
|
|
|
(gh (inexact->exact game-height))
|
|
|
|
(scale (max (min (quotient w gw) (quotient h gh)) 1))
|
|
|
|
(cw (* gw scale))
|
|
|
|
(ch (* gh scale)))
|
|
|
|
(set-element-width! canvas cw)
|
|
|
|
(set-element-height! canvas ch)
|
|
|
|
(set-image-smoothing-enabled! context 0)
|
|
|
|
(set! *canvas-scale* (exact->inexact scale))
|
|
|
|
(set! *canvas-width* (* game-width *canvas-scale*))
|
2024-05-26 19:49:11 -04:00
|
|
|
(set! *canvas-height* (* game-height *canvas-scale*))))
|
2024-05-18 14:04:35 -04:00
|
|
|
|
2024-05-17 17:49:43 -04:00
|
|
|
(set-element-width! canvas (inexact->exact game-width))
|
|
|
|
(set-element-height! canvas (inexact->exact game-height))
|
2024-05-18 14:04:35 -04:00
|
|
|
(add-event-listener! (current-window) "resize"
|
|
|
|
(procedure->external (lambda (_) (resize-canvas))))
|
2024-12-10 11:32:14 -05:00
|
|
|
(add-event-listener! (current-window) "gamepadconnected"
|
|
|
|
(procedure->external
|
|
|
|
(lambda (e)
|
|
|
|
(connect-gamepad! (gamepad-event-gamepad e)))))
|
|
|
|
(add-event-listener! (current-window) "gamepaddisconnected"
|
|
|
|
(procedure->external
|
|
|
|
(lambda (e)
|
|
|
|
(disconnect-gamepad! (gamepad-event-gamepad e)))))
|
2024-05-17 13:08:43 -04:00
|
|
|
(add-event-listener! (current-document) "keydown"
|
|
|
|
(procedure->external on-key-down))
|
2024-05-26 14:55:04 -04:00
|
|
|
(define (register-touch-control elem-id input-id)
|
|
|
|
(add-event-listener! (get-element-by-id elem-id) "click"
|
|
|
|
(procedure->external
|
|
|
|
(lambda (e) (on-input-down input-id)))))
|
|
|
|
(register-touch-control "dpad-left" 'left)
|
|
|
|
(register-touch-control "dpad-right" 'right)
|
|
|
|
(register-touch-control "dpad-down" 'down)
|
|
|
|
(register-touch-control "dpad-up" 'up)
|
|
|
|
(register-touch-control "button-a" 'undo)
|
2024-05-18 14:04:35 -04:00
|
|
|
(resize-canvas)
|
2024-05-17 13:08:43 -04:00
|
|
|
(request-animation-frame draw-callback)
|
|
|
|
(timeout update-callback dt)
|
2024-05-20 22:12:35 -04:00
|
|
|
(load-game!)
|