implemented duplicate

This commit is contained in:
Alexander Griffith 2022-03-25 19:40:58 -04:00
parent 24294b9095
commit 00c0f6605b
10 changed files with 604 additions and 12 deletions

View file

@ -0,0 +1,3 @@
target=$1
location=$(dirname "$0")
cp -rf $location/../../* $target

87
.duplicate/new-game.sh Executable file
View file

@ -0,0 +1,87 @@
#!/bin/bash
help="new-game.sh: \n
A tool for duplicating min-love2d-fennel. \n\
\n\
usage: \n\
./new-game.sh [project-name] [opts]\n\
\n
opts:\n
-o= (--output-directory=) Target parent directory. Defaults to PWD\n
-l= (--layut=) Set the layout. Defaults to minimal layout.\n
-f (--force) overwrite the target directory if it exists\n
-h (--help) Display this text\n
\n
eg: \n\
./new-game.sh sample-game -o=../ -f
"
name=$1
output_dir=$(pwd)
gethelp=false
location=$(dirname "$0")
force=false
layout="clone"
for i in "$@"
do
case $i in
-o=*|--output-directory=*)
output_dir="${i#*=}"
;;
-l=*|--layout=*)
layout="${i#*=}"
;;
-f|--force)
force=true
;;
-h|--help)
gethelp=true
;;
*)
# unknown option
;;
esac
done
if [ ! -d $output_dir ]; then
echo "output directory not found!"
echo $output_dir
exit 1
fi
if [ ! -d $location/$layout ]; then
echo "\"$layout\" layout not found!"
echo "Valid built in layouts are:"
echo "clone"
echo "seperate-source"
exit 1
fi
target_dir=$output_dir/$name
if [ -d $target_dir ]; then
if [ $force = true ]; then
echo "Overwriting $target_dir."
rm -rf $target_dir
else
echo "target directory already exists! Use -f or --force to overwrite this directory."
echo $target_dir
exit 1
fi
fi
copy-dir (){
mkdir $target_dir
./$location/$layout/update-layout.sh $target_dir
rm -rf $location/.duplicate
rm -rf $location/.git
}
if [ $gethelp = true ]; then
echo -e $help
else
copy-dir
fi

View file

@ -0,0 +1,113 @@
--[[
Copyright (c) 2010-2013 Matthias Richter
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]--
local function __NULL__() end
-- default gamestate produces error on every callback
local state_init = setmetatable({leave = __NULL__},
{__index = function() error("Gamestate not initialized. Use Gamestate.switch()") end})
local stack = {state_init}
local initialized_states = setmetatable({}, {__mode = "k"})
local state_is_dirty = true
local GS = {}
function GS.new(t) return t or {} end -- constructor - deprecated!
local function change_state(stack_offset, to, ...)
local pre = stack[#stack]
-- initialize only on first call
;(initialized_states[to] or to.init or __NULL__)(to)
initialized_states[to] = __NULL__
stack[#stack+stack_offset] = to
state_is_dirty = true
return (to.enter or __NULL__)(to, pre, ...)
end
function GS.switch(to, ...)
assert(to, "Missing argument: Gamestate to switch to")
assert(to ~= GS, "Can't call switch with colon operator")
;(stack[#stack].leave or __NULL__)(stack[#stack])
return change_state(0, to, ...)
end
function GS.push(to, ...)
assert(to, "Missing argument: Gamestate to switch to")
assert(to ~= GS, "Can't call push with colon operator")
return change_state(1, to, ...)
end
function GS.pop(...)
assert(#stack > 1, "No more states to pop!")
local pre, to = stack[#stack], stack[#stack-1]
stack[#stack] = nil
;(pre.leave or __NULL__)(pre)
state_is_dirty = true
return (to.resume or __NULL__)(to, pre, ...)
end
function GS.current()
return stack[#stack]
end
-- XXX: don't overwrite love.errorhandler by default:
-- this callback is different than the other callbacks
-- (see http://love2d.org/wiki/love.errorhandler)
-- overwriting thi callback can result in random crashes (issue #95)
local all_callbacks = { 'draw', 'update' }
-- fetch event callbacks from love.handlers
for k in pairs(love.handlers) do
all_callbacks[#all_callbacks+1] = k
end
function GS.registerEvents(callbacks)
local registry = {}
callbacks = callbacks or all_callbacks
for _, f in ipairs(callbacks) do
registry[f] = love[f] or __NULL__
love[f] = function(...)
registry[f](...)
return GS[f](...)
end
end
end
-- forward any undefined functions
setmetatable(GS, {__index = function(_, func)
-- call function only if at least one 'update' was called beforehand
-- (see issue #46)
if not state_is_dirty or func == 'update' then
state_is_dirty = false
return function(...)
return (stack[#stack][func] or __NULL__)(stack[#stack], ...)
end
end
return __NULL__
end})
return GS

View file

@ -0,0 +1,151 @@
__requestQueue = {}
_requestCount = 0
_Request =
{
command = "",
currentTime = 0,
timeOut = 2,
id = '0'
}
local os = love.system.getOS()
local __defaultErrorFunction = nil
local isDebugActive = false
JS = {}
function JS.callJS(funcToCall)
if(os == "Web") then
print("callJavascriptFunction " .. funcToCall)
end
end
--You can pass a set of commands here and, it is a syntactic sugar for executing many commands inside callJS, as it only calls a function
--If you pass arguments to the func beyond the string, it will perform automatically string.format
--Return statement is possible inside this structure
--This will return a string containing a function to be called by JS.callJS
local _unpack
if(_VERSION == "Lua 5.1" or _VERSION == "LuaJIT") then
_unpack = unpack
else
_unpack = table.unpack
end
function JS.stringFunc(str, ...)
str = "(function(){"..str.."})()"
if(#arg > 0) then
str = str:format(_unpack(arg))
end
str = str:gsub("[\n\t]", "")
return str
end
--The call will store in the webDB the return value from the function passed
--it timeouts
local function retrieveJS(funcToCall, id)
--Used for retrieveData function
JS.callJS("FS.writeFile('"..love.filesystem.getSaveDirectory().."/__temp"..id.."', "..funcToCall..");")
end
--Call JS.newRequest instead
function _Request:new(isPromise, command, onDataLoaded, onError, timeout, id)
local obj = {}
setmetatable(obj, self)
obj.command = command
obj.onError = onError or __defaultErrorFunction
if not isPromise then
retrieveJS(command, id)
else
JS.callJS(command)
end
obj.onDataLoaded = onDataLoaded
obj.timeOut = (timeout == nil) and obj.timeOut or timeout
obj.id = id
function obj:getData()
--Try to read from webdb
return love.filesystem.read("__temp"..self.id)
end
function obj:purgeData()
--Data must be purged for not allowing old data to be retrieved
love.filesystem.remove("__temp"..self.id)
end
function obj:update(dt)
self.timeOut = self.timeOut - dt
local retData = self:getData()
if((retData ~= nil and retData ~= "nil") or self.timeOut <= 0) then
if(retData ~= nil and retData:match("ERROR") == nil) then
if isDebugActive then
print("Data has been retrieved "..retData)
end
self.onDataLoaded(retData)
else
self.onError(self.id, retData)
end
self:purgeData()
return false
else
return true
end
end
return obj
end
--Place this function on love.update and set it to return if it returns false (This API is synchronous)
function JS.retrieveData(dt)
local isRetrieving = #__requestQueue ~= 0
local deadRequests = {}
for i = 1, #__requestQueue do
local isUpdating =__requestQueue[i]:update(dt)
if not isUpdating then
table.insert(deadRequests, i)
end
end
for i = 1, #deadRequests do
if(isDebugActive) then
print("Request died: "..deadRequests[i])
end
table.remove(__requestQueue, deadRequests[i])
end
return isRetrieving
end
--May only be used for functions that don't return a promise
function JS.newRequest(funcToCall, onDataLoaded, onError, timeout, optionalId)
if(os ~= "Web") then
return
end
table.insert(__requestQueue, _Request:new(false, funcToCall, onDataLoaded, onError, timeout or 5, optionalId or _requestCount))
end
--This function can be handled manually (in JS code)
--How to: add the function call when your events resolve: FS.writeFile("Put love.filesystem.getSaveDirectory here", "Pass a string here (NUMBER DONT WORK"))
--Or it can be handled by Lua, it auto sets your data if you write the following command:
-- _$_(yourStringOrFunctionHere)
function JS.newPromiseRequest(funcToCall, onDataLoaded, onError, timeout, optionalId)
if(os ~= "Web") then
return
end
optionalId = optionalId or _requestCount
funcToCall = funcToCall:gsub("_$_%(", "FS.writeFile('"..love.filesystem.getSaveDirectory().."/__temp"..optionalId.."', ")
table.insert(__requestQueue, _Request:new(true, funcToCall, onDataLoaded, onError, timeout or 5, optionalId))
end
--It receives the ID from ther request
--Don't try printing the request.command, as it will execute the javascript command
function JS.setDefaultErrorFunction(func)
__defaultErrorFunction = func
end
JS.setDefaultErrorFunction(function(id, error)
if( isDebugActive ) then
local msg = "Data could not be loaded for id:'"..id.."'"
if(error)then
msg = msg.."\nError: "..error
end
print(msg)
end
end)

View file

@ -0,0 +1,58 @@
-- bootstrap the compiler
fennel = require("lib.fennel")
table.insert(package.loaders, fennel.make_searcher({correlate=true}))
fennel.path = love.filesystem.getSource() .. "/?.fnl;" ..
love.filesystem.getSource() .. "/src/?.fnl;" ..
love.filesystem.getSource() .. "/src/?/init.fnl;" ..
fennel.path
debug_mode = true
pp = function(x) print(fennel.view(x)) end
db = function(x)
if (debug_mode == true) then
local debug_info = debug.getinfo(1)
-- print debug.getinfo
local currentline = debug_info.currentline
-- local file = debug_info.source:match("^.+/(.+)$")
local file = debug_info["short_src"] or ""
local name = debug_info["namewhat"] or ""
pp({"db", x})
end
end
js = (require "lib.js")
lume = require("lib.lume")
local make_love_searcher = function(env)
return function(module_name)
local path = module_name:gsub("%.", "/") .. ".fnl"
if love.filesystem.getInfo(path) then
return function(...)
local code = love.filesystem.read(path)
return fennel.eval(code, {env=env}, ...)
end, path
end
end
end
table.insert(package.loaders, make_love_searcher(_G))
table.insert(fennel["macro-searchers"], make_love_searcher("_COMPILER"))
require("lib.js")
-- taken from bump
function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2)
return x1 < x2+w2 and x2 < x1+w1 and
y1 < y2+h2 and y2 < y1+h1
end
function pointWithin(px,py,x,y,w,h)
return px < x + w and px > x and
py < y + h and py > y
end
math.randomseed( os.time() )
require("wrap")

View file

@ -0,0 +1,95 @@
VERSION=0.1.0
LOVE_VERSION=11.4
NAME=change-me
ITCH_ACCOUNT=username
URL=https://username.itch.io/change-me
AUTHOR="Change Me"
DESCRIPTION="Change Me"
HEIGHT=640
WIDTH=880
CANVAS_COLOUR="26,16,16"
TEXT_COLOUR="250,248,240"
LIBS_LUA := $(wildcard lib/*)
LIBS_FNL := $(wildcard lib/*.fnl)
LUA := $(wildcard *.lua)
SRC := $(wildcard src/*.fnl)
OUT := $(patsubst src/%.fnl,%.lua,$(SRC))
OUT_LIBS := $(patsubst %.fnl,%.lua,$(LIBS_FNL))
run: $(OUT) $(OUT_ENTS); love .
count: ; cloc src/*.fnl --force-lang=clojure
clean: ; rm -rf releases/* $(OUT) $(OUT_LIBS)
cleansrc: ; rm -rf $(OUT) $(OUT_LIBS)
%.lua: src/%.fnl; lua lib/fennel --compile --correlate $< > $@
lib/%.lua: lib/%.fnl; lua lib/fennel --compile --correlate $< > $@
LOVEFILE=releases/$(NAME)-$(VERSION).love
$(LOVEFILE): $(LUA) $(OUT) $(OUT_LIBS) $(LIBS_LUA) assets #text
mkdir -p releases/
find $^ -type f | LC_ALL=C sort | env TZ=UTC zip -r -q -9 -X $@ -@
love: $(LOVEFILE)
# platform-specific distributables
REL=$(PWD)/buildtools/love-release.sh # https://p.hagelb.org/love-release.sh
FLAGS=-a "$(AUTHOR)" --description $(DESCRIPTION) \
--love $(LOVE_VERSION) --url $(URL) --version $(VERSION) --lovefile $(LOVEFILE)
releases/$(NAME)-$(VERSION)-x86_64.AppImage: $(LOVEFILE)
cd buildtools/appimage && ./build.sh $(LOVE_VERSION) $(PWD)/$(LOVEFILE)
mv buildtools/appimage/game-x86_64.AppImage $@
releases/$(NAME)-$(VERSION)-macos.zip: $(LOVEFILE)
$(REL) $(FLAGS) -M
mv releases/$(NAME)-macos.zip $@
releases/$(NAME)-$(VERSION)-win.zip: $(LOVEFILE)
$(REL) $(FLAGS) -W32
mv releases/$(NAME)-win32.zip $@
releases/$(NAME)-$(VERSION)-web.zip: $(LOVEFILE)
buildtools/love-js/love-js.sh releases/$(NAME)-$(VERSION).love $(NAME) -v=$(VERSION) -a=$(AUTHOR) -o=releases -x=$(WIDTH) -y=$(HEIGHT) -c=$(CANVAS_COLOUR) -t=$(TEXT_COLOUR)
releases/$(NAME)-$(VERSION)-source.zip:
find assets buildtools lib ./conf.lua ./makefile ./license.txt ./main.lua ./readme.org src buildtools -type f | LC_ALL=C sort | env TZ=UTC zip -r -q -9 -X $@ -@
runweb: $(LOVEFILE)
buildtools/love-js/love-js.sh releases/$(NAME)-$(VERSION).love $(NAME) -v=$(VERSION) -a=$(AUTHOR) -o=releases -x=$(WIDTH) -y=$(HEIGHT) -c=$(CANVAS_COLOUR) -t=$(TEXT_COLOUR) -r
linux: releases/$(NAME)-$(VERSION)-x86_64.AppImage
mac: releases/$(NAME)-$(VERSION)-macos.zip
windows: releases/$(NAME)-$(VERSION)-win.zip
web: releases/$(NAME)-$(VERSION)-web.zip
source: releases/$(NAME)-$(VERSION)-source.zip
# If you release on itch.io, you should install butler:
# https://itch.io/docs/butler/installing.html
uploadlinux: releases/$(NAME)-$(VERSION)-x86_64.AppImage
butler push $^ $(ITCH_ACCOUNT)/$(NAME):linux --userversion $(VERSION)
uploadmac: releases/$(NAME)-$(VERSION)-macos.zip
butler push $^ $(ITCH_ACCOUNT)/$(NAME):mac --userversion $(VERSION)
uploadwindows: releases/$(NAME)-$(VERSION)-win.zip
butler push $^ $(ITCH_ACCOUNT)/$(NAME):windows --userversion $(VERSION)
uploadlove: releases/$(NAME)-$(VERSION).love
butler push $^ $(ITCH_ACCOUNT)/$(NAME):love --userversion $(VERSION)
uploadweb: releases/$(NAME)-$(VERSION)-web.zip
butler push $^ $(ITCH_ACCOUNT)/$(NAME):web --userversion $(VERSION)
uploadsource: releases/$(NAME)-$(VERSION)-source.zip
butler push $^ $(ITCH_ACCOUNT)/$(NAME):source --userversion $(VERSION)
upload: uploadlinux uploadmac uploadwindows uploadweb uploadlove cleansrc
release: linux web mac windows upload cleansrc

View file

@ -0,0 +1,27 @@
(import-macros {: incf} :sample-macros)
(var counter 0)
(var time 0)
(love.graphics.setNewFont 30)
(local (major minor revision) (love.getVersion))
{:draw (fn draw [self message]
(local (w h _flags) (love.window.getMode))
(love.graphics.printf
(: "Love Version: %s.%s.%s"
:format major minor revision) 0 10 w :center)
(love.graphics.printf
(: "This window should close in %0.1f seconds"
:format (math.max 0 (- 3 time)))
0 (- (/ h 2) 15) w :center))
:update (fn update [self dt set-mode]
(if (< counter 65535)
(set counter (+ counter 1))
(set counter 0))
(incf time dt)
(when (> time 3)
(love.event.quit)))
:keypressed (fn keypressed [self key set-mode]
(love.event.quit))}

View file

@ -0,0 +1,9 @@
(local gamestate (require :lib.gamestate))
(local stdio (require :lib.stdio))
(fn love.load [args]
(love.graphics.setDefaultFilter "nearest" "nearest" 0)
(if (= :web (. args 1)) (global web true) (global web false))
(gamestate.registerEvents)
(gamestate.switch (require :mode-intro))
(when (not web) (stdio.start)))

View file

@ -0,0 +1,7 @@
target=$1
location=$(dirname "$0")
cp -rf $location/../../* $target
cp -rf $location/overwrite/* $target
mv $target/sample-macros.fnl $target/src/
rm -rf $target/*fnl
mv -f $target/src/sample-macros.fnl $target

View file

@ -1,30 +1,52 @@
* Minimal Fennel Love2D Setup
In a lead up to the annual [[https://itch.io/jam/autumn-lisp-game-jam-2018][Autumn Lisp Game Jam]] I thought I'd look into Phil Hegelberg's approach to last Aprils Jam, using [[https://love2d.org/][love2d]] in concert with [[https://fennel-lang.org/][fennel]]. Phil outlines his approach on his [[https://technomancy.us/187][blog]].
The first step is to see how fennel works as a standalone execution environment, then slowly integrate some of the love2d API.
In the lead up to the semi-annual [[https://itch.io/jam/autumn-lisp-game-jam-2018][Autumn Lisp Game Jam]] I thought I'd look into Phil Hegelberg's approach to last Aprils Jam, using [[https://love2d.org/][love2d]] in concert with [[https://fennel-lang.org/][fennel]]. Phil outlines his approach on his [[https://technomancy.us/187][blog]].
This repo contains the minimal viable setup to get started with Phil Hegelberg's game design process, plus some additional libraries.
* Getting Started
The following commands will clone this project and duplicate its structure into a new folder =$PROJECT_NAME=
#+BEGIN_SRC bash
PROJECT=project-name
git clone https://gitlab.com/alexjgriffith/min-love2d-fennel.git $PROJECT
cd $PROJECT
rm -rf .git
git clone https://gitlab.com/alexjgriffith/min-love2d-fennel.git
./min-love2d-fennel/.duplicate/new-game.sh $PROJECT_NAME
#+END_SRC
* Phil's Modal Callbacks (PMC)
Check out the makefile and conf.lua files in =$PROJECT_NAME=, updating them with information relevant to your game.
Phil Hegelberg's [[https://gitlab.com/technomancy/exo-encounter-667/][exo-encounter-667]] is structured using a modal callback system. Each game state has a mode and each mode has a series of specific callbacks.
You can enter =love .= in the =$PROJECT_NAME= directory to run your game, or =make run=.
If you design your game as a series of states in a very simple state machine, for example *start-screen*, *play* and *end*, with unidirectional progression, you can easily separate the logic for each state into state/mode specific callbacks. As an example, in order to have state dependant rendering that differs between start-screen,play and end you could provide a *draw* callback for each of those states. Similarly if we need state dependent logic and keyboard input we could provide *update* and *keyboard* callbacks. As you iterate you can add and remove callbacks and states/modes as needed with very little friction.
The following lines with =Update= should be changed in the =makefile= and =love.conf= to reflect your game.
#+BEGIN_SRC makefile
VERSION=0.1.0
LOVE_VERSION=11.4
NAME=change-me # Update
ITCH_ACCOUNT=change-me-too # Update
URL=https://gitlab.com/alexjgriffith/min-love2d-fennel # Update
AUTHOR="Your Name" # Update
DESCRIPTION="Minimal setup for trying out Phil Hagelberg's fennel/love game design process." # Update
#+END_SRC
#+BEGIN_SRC lua
love.conf = function(t)
t.gammacorrect = true
t.title, t.identity = "change-me", "Minimal" -- Update
t.modules.joystick = false
t.modules.physics = false
t.window.width = 720
t.window.height = 450
t.window.vsync = false
t.version = "11.4"
end
#+END_SRC
* Emacs Setup
Once you install the latest version of [[https://gitlab.com/technomancy/fennel-mode][fennel-mode]], you can run
=C-u M-x fennel-repl= followed by =love .= to launch a repl.
* Project Structure
* Default Project Structure
The =make= process as-is will only compile the contents of the root folder and the =lib/= folder+subfolders, so make sure to put your game files in either of those.
@ -32,6 +54,26 @@ Specifically, every =.fnl= file needed at runtime needs to be situated in the ro
In order to use macros, they have to be put in =.fnl= files inside =lib/=.
* Seperate your Code into a /src directory
If you want a more opinionated layout, you can use pass in a =--layout= parameter when creating your project.
#+BEGIN_SRC bash
./min-love2d-fennel/.duplicate/new-game.sh $PROJECT_NAME --layout=seperate-source
#+END_SRC
This build uses =gamestate= rather than Phil's approach to scene seperation and puts all your =.fnl= files into a =/src= directory. It also provides a seperate makefile that handles this layout.
Note, any macros will have to be placed in the root of the project or in the =lib= folder (this can be modified in =main.lua=)
Presently the only layouts are =clone= and =seperate-source=. If you want to make your own check out the =.duplicate= directory to see how they work.
* Release Process
Use =make linux=, =make windows=, or =make mac= to create executables for each platform, or =make release= to do all three. Make sure to read the makefile for more commands (and remember to edit your game data in it!)
Use =make linux=, =make windows=, =make mac=, or =make web= to create targets for each platform, or =make release= to make all four. Check out the makefile for more commands, and remember to edit your game data in it!
* Phil's Modal Callbacks (PMC)
Phil Hegelberg's [[https://gitlab.com/technomancy/exo-encounter-667/][exo-encounter-667]] is structured using a modal callback system. Each game state has a mode and each mode has a series of specific callbacks.
If you design your game as a series of states in a very simple state machine, for example *start-screen*, *play* and *end*, with unidirectional progression, you can easily separate the logic for each state into state/mode specific callbacks. As an example, in order to have state dependant rendering that differs between start-screen,play and end you could provide a *draw* callback for each of those states. Similarly if we need state dependent logic and keyboard input we could provide *update* and *keyboard* callbacks. As you iterate you can add and remove callbacks and states/modes as needed with very little friction.