;;; Copyright (C) 2025 Gene Pasquet ;;; ;;; This program 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. ;;; ;;; This program 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 this program. If not, see . (require hy) (require hyrule *) (import pygame pymunk pymunk.pygame_util pytmx.util_pygame [load_pygame] entities [Player LevelTile Goal Enemy PlayerKilled Physics] tiles [TileSet draw-tile] utils [neg add-tuples null-tuple normalise-tuple] text [render-text] systems [apply-gravity apply-collisions run-enemies GoalHit] math [floor]) (pygame.init) (setv TILE_SCALING 1) (setv TILE_SIZE (* TILE_SCALING 16)) (setv MACRO_STEP_WAIT 300) (setv MACRO_COOLDOWN 2000) (setv FPS 60.0) (setv screen (pygame.display.set_mode #((* TILE_SCALING 640) (* TILE_SCALING 480)))) (setv screen-options (pymunk.pygame_util.DrawOptions screen)) (setv clock (pygame.time.Clock)) (setv tileset (TileSet "assets/monochrome-transparent.png" TILE_SCALING TILE_SIZE TILE_SIZE 1)) (setv levels [(load_pygame "assets/level-0.tmx") (load_pygame "assets/level-1.tmx") (load_pygame "assets/level-2.tmx") (load_pygame "assets/level-3.tmx")]) (setv level-id 0) (defn abs-to-tile-index [abs-id] (int (floor (/ abs-id TILE_SIZE)))) (do ;; Help screen (.fill screen "#000000") (render-text screen tileset "MACROKNIGHT" 15 10) (render-text screen tileset "GENE AND OWEN PASQUET" 10 2) (render-text screen tileset "CONTROLS" 16 22) (render-text screen tileset "WASD TO MOVE" 14 25) (render-text screen tileset "SPACE TO ATTACK" 13 26) (render-text screen tileset "ENTER FOR MACRO" 13 27) (pygame.display.flip) ;; (pygame.time.wait 3000) ;;; TODO: reactivate ) (setv game-running True) (while game-running ;; Load the level (setv running True) (setv level (get levels level-id)) (setv entities []) (setv space (pymunk.Space)) (setv space.gravity #(0.0 200.0)) (setv player-pos #(5 5)) (for [item (get level.layers 1)] (let [tile-x (abs-to-tile-index item.x) tile-y (abs-to-tile-index item.y)] (case item.type "Player" (setv player-pos #(tile-x tile-y)) "Goal" (.append entities (let [entity (Goal (len entities) (get tileset.tiles 0) (let [body (pymunk.Body :body_type pymunk.Body.STATIC)] (setv body.position #(item.x item.y)) (Physics body (pymunk.Poly.create_box body #(TILE_SIZE TILE_SIZE)))) TILE_SIZE tile-x tile-y)] (.add space entity._physics.body entity._physics.shape) entity)) "Enemy1" (.append entities (let [entity (Enemy (len entities) [(get tileset.tiles 128) (get tileset.tiles 129)] (let [body (pymunk.Body 10 0.2)] (setv body.position #(item.x item.y)) (Physics body (pymunk.Poly.create_box body #(TILE_SIZE TILE_SIZE)))) TILE_SIZE tile-x tile-y)] (.add space entity._physics.body entity._physics.shape) entity))))) (setv player-pos (let [player-objects (lfor ent (get level.layers 1) :if (= ent.type "Player") ent)] (if (any player-objects) (let [player-object (get player-objects 0)] #((abs-to-tile-index player-object.x) (abs-to-tile-index player-object.y))) #(5 5)))) (setv player (Player (len entities) [(get tileset.tiles 28) (get tileset.tiles 29)] (let [body (pymunk.Body 10 (float "inf"))] (setv body.position #((* TILE_SIZE (get player-pos 0)) (* TILE_SIZE (get player-pos 1)))) (setv body.elasticity 0.2) (setv body.friction 0.1) (Physics body (pymunk.Poly.create_box body #(TILE_SIZE TILE_SIZE)))) TILE_SIZE #* player-pos)) (.add space player._physics.body player._physics.shape) (.append entities player) (setv macro-input-mode False) (setv macro-wait-time 0) (setv macro-commands [None None None]) (let [id-offset (len entities)] (for [#(id tiledef) (enumerate (.tiles (get level.layers 0)))] (let [x (get tiledef 0) y (get tiledef 1) tile (get tiledef 2) tile-ent (LevelTile (+ id id-offset) tile (let [body (pymunk.Body :body_type pymunk.Body.STATIC)] (setv body.position #((* x TILE_SIZE) (* y TILE_SIZE))) (setv body.elasticity 0.0) (setv body.friction 0.1) (Physics body (pymunk.Poly.create_box body #(TILE_SIZE TILE_SIZE)))) TILE_SIZE x y TILE_SCALING)] (.add space tile-ent._physics.body tile-ent._physics.shape) (.append entities tile-ent)))) (setv ongoing-inputs []) (while running (for [event (pygame.event.get)] (case event.type pygame.QUIT (do (setv running False) (setv game-running False)) pygame.KEYDOWN (if (= event.key pygame.K_ESCAPE) (do (setv running False) (setv game-running False)) (if macro-input-mode (when (in event.key [pygame.K_a pygame.K_w pygame.K_a pygame.K_s pygame.K_d pygame.K_SPACE]) (setv (get macro-commands (.index macro-commands None)) event.key)) (if (and (= event.key pygame.K_RETURN) (= macro-wait-time 0)) (setv macro-input-mode True) (.append ongoing-inputs event.key)))) pygame.KEYUP (when (in event.key ongoing-inputs) (.remove ongoing-inputs event.key)))) (.fill screen "#000000") ;; Render special objects (for [item (get level.layers 1)] (case item.type "Text" (render-text screen tileset (.upper item.text) (abs-to-tile-index item.x) (abs-to-tile-index item.y)))) (if macro-input-mode ;; If the commands list is full (if (get macro-commands -1) ;; Process commands (do (let [#(command-id command) (get (lfor command (enumerate macro-commands) :if (get command 1) command) 0)] (case command pygame.K_a (.move player #((neg (* 2 TILE_SIZE)) 0)) pygame.K_s (.move player #(0 TILE_SIZE)) pygame.K_w (.move player #(0 (neg (/ player.MAX_JUMPING 2)))) pygame.K_d (.move player #((* 2 TILE_SIZE) 0)) pygame.K_SPACE (.attack player)) (if (= command-id (- (len macro-commands) 1)) (do (setv macro-commands [None None None]) (setv macro-input-mode False) (setv macro-wait-time (+ (pygame.time.get_ticks) MACRO_COOLDOWN))) (setv (get macro-commands command-id) None))) (pygame.time.wait MACRO_STEP_WAIT)) ;; If there's still space in the commands list (for [#(num command) (enumerate macro-commands)] (let [x-pos (+ 4 num)] (case command pygame.K_w (draw-tile screen tileset 1057 x-pos 5) pygame.K_d (draw-tile screen tileset 1058 x-pos 5) pygame.K_s (draw-tile screen tileset 1059 x-pos 5) pygame.K_a (draw-tile screen tileset 1060 x-pos 5) pygame.K_SPACE (draw-tile screen tileset 329 x-pos 5) None (draw-tile screen tileset 725 x-pos 5))))) ;; Not in macro mode (do (when (> macro-wait-time 0) (let [progress (round (* 3 (/ (- macro-wait-time (pygame.time.get_ticks)) MACRO_COOLDOWN)))] (for [indicator (range progress)] (draw-tile screen tileset 725 (+ 4 indicator) 5)))) (let [player-velocity #(0 0)] (for [inp ongoing-inputs] (case inp pygame.K_a (setv player-velocity (add-tuples player-velocity #((neg player.SPEED) 0))) pygame.K_s (setv player-velocity (add-tuples player-velocity #(0 1))) pygame.K_w (setv player-velocity (add-tuples player-velocity #(0 -100))) ;; (.jump player) pygame.K_d (setv player-velocity (add-tuples player-velocity #(player.SPEED 0))) pygame.K_SPACE (.attack player))) (when (not (null-tuple player-velocity)) (setv player._physics.body.velocity (normalise-tuple (add-tuples player._physics.body.velocity player-velocity) #(-100 -200) #(100 100))))) (try (when (any ongoing-inputs) (for [entity entities] (apply-collisions entity entities))) ;; Apply systems (let [ticks (pygame.time.get_ticks)] (for [entity entities] (run-enemies entity entities) (when (hasattr entity "animate") (.animate entity ticks)) (apply-gravity entity entities) (apply-collisions entity entities))) (except [GoalHit] (setv level-id (+ level-id 1)) (setv running False) (when (>= level-id (len levels)) (setv level-id 0) (.fill screen "#000000") (render-text screen tileset "YOU WIN" 15 14) (pygame.display.flip) (pygame.time.wait 1000))) (except [PlayerKilled] (setv running False))) (.flush player))) (for [entity entities] (.blit screen entity.surf entity.rect)) ;; (space.debug_draw screen-options) (pygame.display.flip) (when (and (!= 0 macro-wait-time) (> (pygame.time.get_ticks) macro-wait-time)) (setv macro-wait-time 0)) (.step space (/ 1.0 FPS)) (.tick clock FPS))) (pygame.quit)