Compare commits
101 Commits
proper-bal
...
main
Author | SHA1 | Date |
---|---|---|
|
4b9a94c2c2 | |
|
decd1f7080 | |
|
04d25127fc | |
|
8b66e2e826 | |
|
876f828117 | |
|
ce9a2d335e | |
|
80015dbe62 | |
|
176a7e6d5e | |
|
55a3a7b0ee | |
|
ddfdc8947a | |
|
e035c0ca72 | |
|
668fa9ffd4 | |
|
b4ac028cd9 | |
|
30aa5bd6c6 | |
|
09e48b65b4 | |
|
9bbd68c302 | |
|
7c7b5ff762 | |
|
b928ee3658 | |
|
3a465cb02d | |
|
b44756ff57 | |
|
48a9854653 | |
|
51c80fa427 | |
|
19ddae6273 | |
|
687bf74979 | |
|
aa72d2a19f | |
|
f42ef06ff6 | |
|
aceefeb25c | |
|
d82ab06534 | |
|
3715361718 | |
|
7525daccb6 | |
|
9dc8b10f15 | |
|
cea10a7706 | |
|
7deadbe316 | |
|
7b49603760 | |
|
786f80b0df | |
|
384a14fe5f | |
|
56c0c27d75 | |
|
35c7754207 | |
|
92985da58f | |
|
17a30e9822 | |
|
2d6f83a23f | |
|
e45231dadd | |
|
08a3189780 | |
|
d77675b0cb | |
|
56a5e197cd | |
|
699dab8c7d | |
|
b003c148a4 | |
|
52434fe891 | |
|
ad82035ccc | |
|
aebbc35bac | |
|
2d812f2046 | |
|
1bdcc62347 | |
|
c3a9122580 | |
|
e20ad0d3ad | |
|
4c9fbcdee7 | |
|
1ccf8765ee | |
|
5c45b7bba0 | |
|
6007ac971f | |
|
db1409d94d | |
|
bbaaca4a2d | |
|
51855e13cf | |
|
bb95ef5a63 | |
|
8943eef73f | |
|
e710a79d9c | |
|
c8f128f277 | |
|
027bb31bff | |
|
a801b64f55 | |
|
1926960c86 | |
|
b9d25e18d8 | |
|
0646663e5e | |
|
90f792ff4e | |
|
fbbfc3c2e7 | |
|
f67d6262ac | |
|
534a16ad67 | |
|
fc4e12eddd | |
|
90fa692303 | |
|
4d69e77d9f | |
|
575c9e0a18 | |
|
0f83298086 | |
|
89c37eaf3d | |
|
476e0d54cb | |
|
fed1151179 | |
|
d74332f685 | |
|
9c0d263a29 | |
|
aadaa6e0d6 | |
|
c56cae6527 | |
|
1a68521bd4 | |
|
50ddd67730 | |
|
fedf680626 | |
|
82d1dac5de | |
|
b119310859 | |
|
30f2eada72 | |
|
80c15161e3 | |
|
d85db79e52 | |
|
66bd97499a | |
|
324673ea98 | |
|
8dc999fd72 | |
|
4a4049996f | |
|
f07530623f | |
|
881ff0e734 | |
|
969de111fe |
|
@ -0,0 +1,4 @@
|
||||||
|
std = "lua54+playdate"
|
||||||
|
stds.project = {
|
||||||
|
read_globals = {"playdate"}
|
||||||
|
}
|
|
@ -2,7 +2,6 @@
|
||||||
"Lua.runtime.version": "Lua 5.4",
|
"Lua.runtime.version": "Lua 5.4",
|
||||||
"Lua.diagnostics.disable": ["undefined-global", "lowercase-global"],
|
"Lua.diagnostics.disable": ["undefined-global", "lowercase-global"],
|
||||||
"Lua.diagnostics.globals": ["playdate", "import"],
|
"Lua.diagnostics.globals": ["playdate", "import"],
|
||||||
"Lua.runtime.nonstandardSymbol": ["+=", "-=", "*=", "/="],
|
|
||||||
"Lua.workspace.library": ["/home/sage/Downloads/PlaydateSDK-2.6.2/CoreLibs"],
|
"Lua.workspace.library": ["/home/sage/Downloads/PlaydateSDK-2.6.2/CoreLibs"],
|
||||||
"Lua.workspace.preloadFileSize": 1000
|
"Lua.workspace.preloadFileSize": 1000
|
||||||
}
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
src/assets.lua
|
13
Makefile
|
@ -1,14 +1,15 @@
|
||||||
SOURCE_FILES := src/utils.lua src/dbg.lua src/announcer.lua src/graphics.lua src/scoreboard.lua src/main.lua
|
|
||||||
|
|
||||||
all:
|
all:
|
||||||
pdc src BatterUp.pdx
|
pdc --skip-unknown src BatterUp.pdx
|
||||||
|
|
||||||
check:
|
assets:
|
||||||
|
lua lib/preprocess-cl.lua src/assets.lua2p
|
||||||
|
|
||||||
|
check: assets
|
||||||
stylua -c --indent-type Spaces src/
|
stylua -c --indent-type Spaces src/
|
||||||
cat __stub.ext.lua <(sed 's/^function/-- selene: allow(unused_variable)\nfunction/' ${PLAYDATE_SDK_PATH}/CoreLibs/__types.lua) ${SOURCE_FILES} | grep -v '^import' | sed 's/<const>//g' | selene -
|
luacheck -d --codes src/ --exclude-files src/test/
|
||||||
|
|
||||||
test: check
|
test: check
|
||||||
(cd src; find ./test -name '*lua' | xargs -L1 lua)
|
(cd src; find ./test -name '*lua' | xargs -L1 -I %% lua %% -v)
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
stylua --indent-type Spaces src/
|
stylua --indent-type Spaces src/
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
-- These warning-allieviators could also be injected directly into __types.lua
|
-- These warning-allieviators could also be injected directly into __types.lua
|
||||||
-- Base __types.lua can be found at https://github.com/balpha/playdate-types
|
-- Base __types.lua can be found at https://github.com/balpha/playdate-types
|
||||||
|
|
||||||
-- selene: allow(unused_variable)
|
---@type pd_playdate_lib
|
||||||
-- selene: allow(unscoped_variables)
|
|
||||||
playdate = playdate
|
playdate = playdate
|
||||||
|
|
||||||
-- selene: allow(unscoped_variables)
|
-- selene: allow(unscoped_variables)
|
||||||
|
|
|
@ -0,0 +1,651 @@
|
||||||
|
#!/bin/sh
|
||||||
|
_=[[
|
||||||
|
exec lua "$0" "$@"
|
||||||
|
]]and nil
|
||||||
|
--==============================================================
|
||||||
|
--=
|
||||||
|
--= LuaPreprocess command line program
|
||||||
|
--= by Marcus 'ReFreezed' Thunström
|
||||||
|
--=
|
||||||
|
--= Requires preprocess.lua to be in the same folder!
|
||||||
|
--=
|
||||||
|
--= License: MIT (see the bottom of this file)
|
||||||
|
--= Website: http://refreezed.com/luapreprocess/
|
||||||
|
--= Documentation: http://refreezed.com/luapreprocess/docs/command-line/
|
||||||
|
--=
|
||||||
|
--= Tested with Lua 5.1, 5.2, 5.3, 5.4 and LuaJIT.
|
||||||
|
--=
|
||||||
|
--==============================================================
|
||||||
|
local help = [[
|
||||||
|
|
||||||
|
Script usage:
|
||||||
|
lua preprocess-cl.lua [options] [--] filepath1 [filepath2 ...]
|
||||||
|
OR
|
||||||
|
lua preprocess-cl.lua --outputpaths [options] [--] inputpath1 outputpath1 [inputpath2 outputpath2 ...]
|
||||||
|
|
||||||
|
File paths can be "-" for usage of stdin/stdout.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
lua preprocess-cl.lua --saveinfo=logs/info.lua --silent src/main.lua2p src/network.lua2p
|
||||||
|
lua preprocess-cl.lua --debug src/main.lua2p src/network.lua2p
|
||||||
|
lua preprocess-cl.lua --outputpaths --linenumbers src/main.lua2p output/main.lua src/network.lua2p output/network.lua
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--backtickstrings
|
||||||
|
Enable the backtick (`) to be used as string literal delimiters.
|
||||||
|
Backtick strings don't interpret any escape sequences and can't
|
||||||
|
contain other backticks.
|
||||||
|
|
||||||
|
--data|-d="Any data."
|
||||||
|
A string with any data. If this option is present then the value
|
||||||
|
will be available through the global 'dataFromCommandLine' in the
|
||||||
|
processed files (and any message handler). Otherwise,
|
||||||
|
'dataFromCommandLine' is nil.
|
||||||
|
|
||||||
|
--faststrings
|
||||||
|
Force fast serialization of string values. (Non-ASCII characters
|
||||||
|
will look ugly.)
|
||||||
|
|
||||||
|
--handler|-h=pathToMessageHandler
|
||||||
|
Path to a Lua file that's expected to return a function or a
|
||||||
|
table of functions. If it returns a function then it will be
|
||||||
|
called with various messages as it's first argument. If it's
|
||||||
|
a table, the keys should be the message names and the values
|
||||||
|
should be functions to handle the respective message.
|
||||||
|
(See 'Handler messages' and tests/quickTestHandler*.lua)
|
||||||
|
The file shares the same environment as the processed files.
|
||||||
|
|
||||||
|
--help
|
||||||
|
Show this help.
|
||||||
|
|
||||||
|
--jitsyntax
|
||||||
|
Allow LuaJIT-specific syntax, specifically literals for 64-bit
|
||||||
|
integers, complex numbers and binary numbers.
|
||||||
|
(https://luajit.org/ext_ffi_api.html#literals)
|
||||||
|
|
||||||
|
--linenumbers
|
||||||
|
Add comments with line numbers to the output.
|
||||||
|
|
||||||
|
--loglevel=levelName
|
||||||
|
Set maximum log level for the @@LOG() macro. Can be "off",
|
||||||
|
"error", "warning", "info", "debug" or "trace". The default is
|
||||||
|
"trace", which enables all logging.
|
||||||
|
|
||||||
|
--macroprefix=prefix
|
||||||
|
String to prepend to macro names.
|
||||||
|
|
||||||
|
--macrosuffix=suffix
|
||||||
|
String to append to macro names.
|
||||||
|
|
||||||
|
--meta OR --meta=pathToSaveMetaprogramTo
|
||||||
|
Output the metaprogram to a temporary file (*.meta.lua). Useful if
|
||||||
|
an error happens when the metaprogram runs. This file is removed
|
||||||
|
if there's no error and --debug isn't enabled.
|
||||||
|
|
||||||
|
--nogc
|
||||||
|
Stop the garbage collector. This may speed up the preprocessing.
|
||||||
|
|
||||||
|
--nonil
|
||||||
|
Disallow !(expression) and outputValue() from outputting nil.
|
||||||
|
|
||||||
|
--nostrictmacroarguments
|
||||||
|
Disable checks that macro arguments are valid Lua expressions.
|
||||||
|
|
||||||
|
--novalidate
|
||||||
|
Disable validation of outputted Lua.
|
||||||
|
|
||||||
|
--outputextension=fileExtension
|
||||||
|
Specify what file extension generated files should have. The
|
||||||
|
default is "lua". If any input files end in .lua then you must
|
||||||
|
specify another file extension with this option. (It's suggested
|
||||||
|
that you use .lua2p (as in "Lua To Process") as extension for
|
||||||
|
unprocessed files.)
|
||||||
|
|
||||||
|
--outputpaths|-o
|
||||||
|
This flag makes every other specified path be the output path
|
||||||
|
for the previous path.
|
||||||
|
|
||||||
|
--release
|
||||||
|
Enable release mode. Currently only disables the @@ASSERT() macro.
|
||||||
|
|
||||||
|
--saveinfo|-i=pathToSaveProcessingInfoTo
|
||||||
|
Processing information includes what files had any preprocessor
|
||||||
|
code in them, and things like that. The format of the file is a
|
||||||
|
lua module that returns a table. Search this file for 'SavedInfo'
|
||||||
|
to see what information is saved.
|
||||||
|
|
||||||
|
--silent
|
||||||
|
Only print errors to the console. (This flag is automatically
|
||||||
|
enabled if an output path is stdout.)
|
||||||
|
|
||||||
|
--version
|
||||||
|
Print the version of LuaPreprocess to stdout and exit.
|
||||||
|
|
||||||
|
--debug
|
||||||
|
Enable some preprocessing debug features. Useful if you want
|
||||||
|
to inspect the generated metaprogram (*.meta.lua). (This also
|
||||||
|
enables the --meta option.)
|
||||||
|
|
||||||
|
--
|
||||||
|
Stop options from being parsed further. Needed if you have paths
|
||||||
|
starting with "-" (except for usage of stdin/stdout).
|
||||||
|
|
||||||
|
Handler messages:
|
||||||
|
"init"
|
||||||
|
Sent before any other message.
|
||||||
|
Arguments:
|
||||||
|
inputPaths: Array of file paths to process. Paths can be added or removed freely.
|
||||||
|
outputPaths: If the --outputpaths option is present this is an array of output paths for the respective path in inputPaths, otherwise it's nil.
|
||||||
|
|
||||||
|
"insert"
|
||||||
|
Sent for each @insert"name" statement. The handler is expected to return a Lua code string.
|
||||||
|
Arguments:
|
||||||
|
path: The file being processed.
|
||||||
|
name: The name of the resource to be inserted (could be a file path or anything).
|
||||||
|
|
||||||
|
"beforemeta"
|
||||||
|
Sent before a file's metaprogram runs, if a metaprogram is generated.
|
||||||
|
Arguments:
|
||||||
|
path: The file being processed.
|
||||||
|
luaString: The generated metaprogram.
|
||||||
|
|
||||||
|
"aftermeta"
|
||||||
|
Sent after a file's metaprogram has produced output (before the output is written to a file).
|
||||||
|
Arguments:
|
||||||
|
path: The file being processed.
|
||||||
|
luaString: The produced Lua code. You can modify this and return the modified string.
|
||||||
|
|
||||||
|
"filedone"
|
||||||
|
Sent after a file has finished processing and the output written to file.
|
||||||
|
Arguments:
|
||||||
|
path: The file being processed.
|
||||||
|
outputPath: Where the output of the metaprogram was written.
|
||||||
|
info: Info about the processed file. (See 'ProcessInfo' in preprocess.lua)
|
||||||
|
|
||||||
|
"fileerror"
|
||||||
|
Sent if an error happens while processing a file (right before the program exits).
|
||||||
|
Arguments:
|
||||||
|
path: The file being processed.
|
||||||
|
error: The error message.
|
||||||
|
|
||||||
|
"alldone"
|
||||||
|
Sent after all other messages (right before the program exits).
|
||||||
|
Arguments:
|
||||||
|
(none)
|
||||||
|
]]
|
||||||
|
--==============================================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
local startTime = os.time()
|
||||||
|
local startClock = os.clock()
|
||||||
|
|
||||||
|
local args = arg
|
||||||
|
|
||||||
|
if not args[0] then error("Expected to run from the Lua interpreter.") end
|
||||||
|
local pp = dofile((args[0]:gsub("[^/\\]+$", "preprocess.lua")))
|
||||||
|
|
||||||
|
-- From args:
|
||||||
|
local addLineNumbers = false
|
||||||
|
local allowBacktickStrings = false
|
||||||
|
local allowJitSyntax = false
|
||||||
|
local canOutputNil = true
|
||||||
|
local customData = nil
|
||||||
|
local fastStrings = false
|
||||||
|
local hasOutputExtension = false
|
||||||
|
local hasOutputPaths = false
|
||||||
|
local isDebug = false
|
||||||
|
local outputExtension = "lua"
|
||||||
|
local outputMeta = false -- flag|path
|
||||||
|
local processingInfoPath = ""
|
||||||
|
local silent = false
|
||||||
|
local validate = true
|
||||||
|
local macroPrefix = ""
|
||||||
|
local macroSuffix = ""
|
||||||
|
local releaseMode = false
|
||||||
|
local maxLogLevel = "trace"
|
||||||
|
local strictMacroArguments = true
|
||||||
|
|
||||||
|
--==============================================================
|
||||||
|
--= Local functions ============================================
|
||||||
|
--==============================================================
|
||||||
|
local F = string.format
|
||||||
|
|
||||||
|
local function formatBytes(n)
|
||||||
|
if n >= 1024*1024*1024 then
|
||||||
|
return F("%.2f GiB", n/(1024*1024*1024))
|
||||||
|
elseif n >= 1024*1024 then
|
||||||
|
return F("%.2f MiB", n/(1024*1024))
|
||||||
|
elseif n >= 1024 then
|
||||||
|
return F("%.2f KiB", n/(1024))
|
||||||
|
elseif n == 1 then
|
||||||
|
return F("1 byte", n)
|
||||||
|
else
|
||||||
|
return F("%d bytes", n)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function printfNoise(s, ...)
|
||||||
|
print(s:format(...))
|
||||||
|
end
|
||||||
|
local function printError(s)
|
||||||
|
io.stderr:write(s, "\n")
|
||||||
|
end
|
||||||
|
local function printfError(s, ...)
|
||||||
|
io.stderr:write(s:format(...), "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function errorLine(err)
|
||||||
|
printError(pp.tryToFormatError(err))
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local loadLuaFile = (
|
||||||
|
(_VERSION >= "Lua 5.2" or jit) and function(path, env)
|
||||||
|
return loadfile(path, "bt", env)
|
||||||
|
end
|
||||||
|
or function(path, env)
|
||||||
|
local chunk, err = loadfile(path)
|
||||||
|
if not chunk then return nil, err end
|
||||||
|
|
||||||
|
if env then setfenv(chunk, env) end
|
||||||
|
|
||||||
|
return chunk
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
--==============================================================
|
||||||
|
--= Preprocessor script ========================================
|
||||||
|
--==============================================================
|
||||||
|
|
||||||
|
io.stdout:setvbuf("no")
|
||||||
|
io.stderr:setvbuf("no")
|
||||||
|
|
||||||
|
math.randomseed(os.time()) -- In case math.random() is used anywhere.
|
||||||
|
math.random() -- Must kickstart...
|
||||||
|
|
||||||
|
local processOptions = true
|
||||||
|
local messageHandlerPath = ""
|
||||||
|
local pathsIn = {}
|
||||||
|
local pathsOut = {}
|
||||||
|
|
||||||
|
for _, arg in ipairs(args) do
|
||||||
|
if processOptions and (arg:find"^%-%-?help$" or arg == "/?" or arg:find"^/[Hh][Ee][Ll][Pp]$") then
|
||||||
|
print("LuaPreprocess v"..pp.VERSION)
|
||||||
|
print((help:gsub("\t", " ")))
|
||||||
|
os.exit()
|
||||||
|
|
||||||
|
elseif not (processOptions and arg:find"^%-.") then
|
||||||
|
local paths = (hasOutputPaths and #pathsOut < #pathsIn) and pathsOut or pathsIn
|
||||||
|
table.insert(paths, arg)
|
||||||
|
|
||||||
|
if arg == "-" and (not hasOutputPaths or paths == pathsOut) then
|
||||||
|
silent = true
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif arg == "--" then
|
||||||
|
processOptions = false
|
||||||
|
|
||||||
|
elseif arg:find"^%-%-data=" or arg:find"^%-d=" then
|
||||||
|
customData = arg:gsub("^.-=", "")
|
||||||
|
|
||||||
|
elseif arg == "--backtickstrings" then
|
||||||
|
allowBacktickStrings = true
|
||||||
|
|
||||||
|
elseif arg == "--debug" then
|
||||||
|
isDebug = true
|
||||||
|
outputMeta = outputMeta or true
|
||||||
|
|
||||||
|
elseif arg:find"^%-%-handler=" or arg:find"^%-h=" then
|
||||||
|
messageHandlerPath = arg:gsub("^.-=", "")
|
||||||
|
|
||||||
|
elseif arg == "--jitsyntax" then
|
||||||
|
allowJitSyntax = true
|
||||||
|
|
||||||
|
elseif arg == "--linenumbers" then
|
||||||
|
addLineNumbers = true
|
||||||
|
|
||||||
|
elseif arg == "--meta" then
|
||||||
|
outputMeta = true
|
||||||
|
elseif arg:find"^%-%-meta=" then
|
||||||
|
outputMeta = arg:gsub("^.-=", "")
|
||||||
|
|
||||||
|
elseif arg == "--nonil" then
|
||||||
|
canOutputNil = false
|
||||||
|
|
||||||
|
elseif arg == "--novalidate" then
|
||||||
|
validate = false
|
||||||
|
|
||||||
|
elseif arg:find"^%-%-outputextension=" then
|
||||||
|
if hasOutputPaths then
|
||||||
|
errorLine("Cannot specify both --outputextension and --outputpaths")
|
||||||
|
end
|
||||||
|
hasOutputExtension = true
|
||||||
|
outputExtension = arg:gsub("^.-=", "")
|
||||||
|
|
||||||
|
elseif arg == "--outputpaths" or arg == "-o" then
|
||||||
|
if hasOutputExtension then
|
||||||
|
errorLine("Cannot specify both --outputpaths and --outputextension")
|
||||||
|
elseif pathsIn[1] then
|
||||||
|
errorLine(arg.." must appear before any input path.")
|
||||||
|
end
|
||||||
|
hasOutputPaths = true
|
||||||
|
|
||||||
|
elseif arg:find"^%-%-saveinfo=" or arg:find"^%-i=" then
|
||||||
|
processingInfoPath = arg:gsub("^.-=", "")
|
||||||
|
|
||||||
|
elseif arg == "--silent" then
|
||||||
|
silent = true
|
||||||
|
|
||||||
|
elseif arg == "--faststrings" then
|
||||||
|
fastStrings = true
|
||||||
|
|
||||||
|
elseif arg == "--nogc" then
|
||||||
|
collectgarbage("stop")
|
||||||
|
|
||||||
|
elseif arg:find"^%-%-macroprefix=" then
|
||||||
|
macroPrefix = arg:gsub("^.-=", "")
|
||||||
|
|
||||||
|
elseif arg:find"^%-%-macrosuffix=" then
|
||||||
|
macroSuffix = arg:gsub("^.-=", "")
|
||||||
|
|
||||||
|
elseif arg == "--release" then
|
||||||
|
releaseMode = true
|
||||||
|
|
||||||
|
elseif arg:find"^%-%-loglevel=" then
|
||||||
|
maxLogLevel = arg:gsub("^.-=", "")
|
||||||
|
|
||||||
|
elseif arg == "--version" then
|
||||||
|
io.stdout:write(pp.VERSION)
|
||||||
|
os.exit()
|
||||||
|
|
||||||
|
elseif arg == "--nostrictmacroarguments" then
|
||||||
|
strictMacroArguments = false
|
||||||
|
|
||||||
|
else
|
||||||
|
errorLine("Unknown option '"..arg:gsub("=.*", "").."'.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if silent then
|
||||||
|
printfNoise = function()end
|
||||||
|
end
|
||||||
|
|
||||||
|
local header = "= LuaPreprocess v"..pp.VERSION..os.date(", %Y-%m-%d %H:%M:%S =", startTime)
|
||||||
|
printfNoise(("="):rep(#header))
|
||||||
|
printfNoise("%s", header)
|
||||||
|
printfNoise(("="):rep(#header))
|
||||||
|
|
||||||
|
if hasOutputPaths and #pathsOut < #pathsIn then
|
||||||
|
errorLine("Missing output path for "..pathsIn[#pathsIn])
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Prepare metaEnvironment.
|
||||||
|
pp.metaEnvironment.dataFromCommandLine = customData -- May be nil.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Load message handler.
|
||||||
|
local messageHandler = nil
|
||||||
|
|
||||||
|
local function hasMessageHandler(message)
|
||||||
|
if not messageHandler then
|
||||||
|
return false
|
||||||
|
|
||||||
|
elseif type(messageHandler) == "function" then
|
||||||
|
return true
|
||||||
|
|
||||||
|
elseif type(messageHandler) == "table" then
|
||||||
|
return messageHandler[message] ~= nil
|
||||||
|
|
||||||
|
else
|
||||||
|
assert(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sendMessage(message, ...)
|
||||||
|
if not messageHandler then
|
||||||
|
return
|
||||||
|
|
||||||
|
elseif type(messageHandler) == "function" then
|
||||||
|
local returnValues = pp.pack(messageHandler(message, ...))
|
||||||
|
return pp.unpack(returnValues, 1, returnValues.n)
|
||||||
|
|
||||||
|
elseif type(messageHandler) == "table" then
|
||||||
|
local _messageHandler = messageHandler[message]
|
||||||
|
if not _messageHandler then return end
|
||||||
|
|
||||||
|
local returnValues = pp.pack(_messageHandler(...))
|
||||||
|
return pp.unpack(returnValues, 1, returnValues.n)
|
||||||
|
|
||||||
|
else
|
||||||
|
assert(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if messageHandlerPath ~= "" then
|
||||||
|
-- Make the message handler and the metaprogram share the same environment.
|
||||||
|
-- This way the message handler can easily define globals that the metaprogram uses.
|
||||||
|
local mainChunk, err = loadLuaFile(messageHandlerPath, pp.metaEnvironment)
|
||||||
|
if not mainChunk then
|
||||||
|
errorLine("Could not load message handler...\n"..pp.tryToFormatError(err))
|
||||||
|
end
|
||||||
|
|
||||||
|
messageHandler = mainChunk()
|
||||||
|
|
||||||
|
if type(messageHandler) == "function" then
|
||||||
|
-- void
|
||||||
|
elseif type(messageHandler) == "table" then
|
||||||
|
for message, _messageHandler in pairs(messageHandler) do
|
||||||
|
if type(message) ~= "string" then
|
||||||
|
errorLine(messageHandlerPath..": Table of handlers must only contain messages as keys.")
|
||||||
|
elseif type(_messageHandler) ~= "function" then
|
||||||
|
errorLine(messageHandlerPath..": Table of handlers must only contain functions as values.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
errorLine(messageHandlerPath..": File did not return a table or a function.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Init stuff.
|
||||||
|
sendMessage("init", pathsIn, (hasOutputPaths and pathsOut or nil)) -- @Incomplete: Use pcall and format error message better?
|
||||||
|
|
||||||
|
if not hasOutputPaths then
|
||||||
|
for i, pathIn in ipairs(pathsIn) do
|
||||||
|
pathsOut[i] = (pathIn == "-") and "-" or pathIn:gsub("%.%w+$", "").."."..outputExtension
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not pathsIn[1] then
|
||||||
|
errorLine("No path(s) specified.")
|
||||||
|
elseif #pathsIn ~= #pathsOut then
|
||||||
|
errorLine(F("Number of input and output paths differ. (%d in, %d out)", #pathsIn, #pathsOut))
|
||||||
|
end
|
||||||
|
|
||||||
|
local pathsSetIn = {}
|
||||||
|
local pathsSetOut = {}
|
||||||
|
|
||||||
|
for i = 1, #pathsIn do
|
||||||
|
if pathsSetIn [pathsIn [i]] then errorLine("Duplicate input path: " ..pathsIn [i]) end
|
||||||
|
if pathsSetOut[pathsOut[i]] then errorLine("Duplicate output path: "..pathsOut[i]) end
|
||||||
|
|
||||||
|
pathsSetIn [pathsIn [i]] = true
|
||||||
|
pathsSetOut[pathsOut[i]] = true
|
||||||
|
|
||||||
|
if pathsIn [i] ~= "-" and pathsSetOut[pathsIn [i]] then errorLine("Path is both input and output: "..pathsIn [i]) end
|
||||||
|
if pathsOut[i] ~= "-" and pathsSetIn [pathsOut[i]] then errorLine("Path is both input and output: "..pathsOut[i]) end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Process files.
|
||||||
|
|
||||||
|
-- :SavedInfo
|
||||||
|
local processingInfo = {
|
||||||
|
date = os.date("%Y-%m-%d %H:%M:%S", startTime),
|
||||||
|
files = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
local byteCount = 0
|
||||||
|
local lineCount = 0
|
||||||
|
local lineCountCode = 0
|
||||||
|
local tokenCount = 0
|
||||||
|
|
||||||
|
for i, pathIn in ipairs(pathsIn) do
|
||||||
|
local startClockForPath = os.clock()
|
||||||
|
printfNoise("Processing '%s'...", pathIn)
|
||||||
|
|
||||||
|
local pathOut = pathsOut[i]
|
||||||
|
local pathMeta = (type(outputMeta) == "string") and outputMeta or pathOut:gsub("%.%w+$", "")..".meta.lua"
|
||||||
|
|
||||||
|
if not outputMeta or pathOut == "-" then
|
||||||
|
pathMeta = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local info, err = pp.processFile{
|
||||||
|
pathIn = pathIn,
|
||||||
|
pathMeta = pathMeta,
|
||||||
|
pathOut = pathOut,
|
||||||
|
|
||||||
|
debug = isDebug,
|
||||||
|
addLineNumbers = addLineNumbers,
|
||||||
|
|
||||||
|
backtickStrings = allowBacktickStrings,
|
||||||
|
jitSyntax = allowJitSyntax,
|
||||||
|
canOutputNil = canOutputNil,
|
||||||
|
fastStrings = fastStrings,
|
||||||
|
validate = validate,
|
||||||
|
strictMacroArguments = strictMacroArguments,
|
||||||
|
|
||||||
|
macroPrefix = macroPrefix,
|
||||||
|
macroSuffix = macroSuffix,
|
||||||
|
|
||||||
|
release = releaseMode,
|
||||||
|
logLevel = maxLogLevel,
|
||||||
|
|
||||||
|
onInsert = (hasMessageHandler("insert") or nil) and function(name)
|
||||||
|
local lua = sendMessage("insert", pathIn, name)
|
||||||
|
|
||||||
|
-- onInsert() is expected to return a Lua code string and so is the message
|
||||||
|
-- handler. However, if the handler is a single catch-all function we allow
|
||||||
|
-- the message to not be handled and we fall back to the default behavior of
|
||||||
|
-- treating 'name' as a path to a file to be inserted. If we didn't allow this
|
||||||
|
-- then it would be required for the "insert" message to be handled. I think
|
||||||
|
-- it's better if the user can choose whether to handle a message or not!
|
||||||
|
--
|
||||||
|
if lua == nil and type(messageHandler) == "function" then
|
||||||
|
return assert(pp.readFile(name))
|
||||||
|
end
|
||||||
|
|
||||||
|
return lua
|
||||||
|
end,
|
||||||
|
|
||||||
|
onBeforeMeta = messageHandler and function(lua)
|
||||||
|
sendMessage("beforemeta", pathIn, lua)
|
||||||
|
end,
|
||||||
|
|
||||||
|
onAfterMeta = messageHandler and function(lua)
|
||||||
|
local luaModified = sendMessage("aftermeta", pathIn, lua)
|
||||||
|
|
||||||
|
if type(luaModified) == "string" then
|
||||||
|
lua = luaModified
|
||||||
|
|
||||||
|
elseif luaModified ~= nil then
|
||||||
|
error(F(
|
||||||
|
"%s: Message handler did not return a string for 'aftermeta'. (Got %s)",
|
||||||
|
messageHandlerPath, type(luaModified)
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
return lua
|
||||||
|
end,
|
||||||
|
|
||||||
|
onDone = messageHandler and function(info)
|
||||||
|
sendMessage("filedone", pathIn, pathOut, info)
|
||||||
|
end,
|
||||||
|
|
||||||
|
onError = function(err)
|
||||||
|
xpcall(function()
|
||||||
|
sendMessage("fileerror", pathIn, err)
|
||||||
|
end, function(err)
|
||||||
|
printfError("Additional error in 'fileerror' message handler...\n%s", pp.tryToFormatError(err))
|
||||||
|
end)
|
||||||
|
os.exit(1)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
assert(info, err) -- The onError() handler above should have been called and we should have exited already.
|
||||||
|
|
||||||
|
byteCount = byteCount + info.processedByteCount
|
||||||
|
lineCount = lineCount + info.lineCount
|
||||||
|
lineCountCode = lineCountCode + info.linesOfCode
|
||||||
|
tokenCount = tokenCount + info.tokenCount
|
||||||
|
|
||||||
|
if processingInfoPath ~= "" then
|
||||||
|
|
||||||
|
-- :SavedInfo
|
||||||
|
table.insert(processingInfo.files, info) -- See 'ProcessInfo' in preprocess.lua for what more 'info' contains.
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
printfNoise("Processing '%s' successful! (%.3fs)", pathIn, os.clock()-startClockForPath)
|
||||||
|
printfNoise(("-"):rep(#header))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Finalize stuff.
|
||||||
|
if processingInfoPath ~= "" then
|
||||||
|
printfNoise("Saving processing info to '%s'.", processingInfoPath)
|
||||||
|
|
||||||
|
local luaParts = {"return"}
|
||||||
|
assert(pp.serialize(luaParts, processingInfo))
|
||||||
|
local lua = table.concat(luaParts)
|
||||||
|
|
||||||
|
local file = assert(io.open(processingInfoPath, "wb"))
|
||||||
|
file:write(lua)
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
printfNoise(
|
||||||
|
"All done! (%.3fs, %.0f file%s, %.0f LOC, %.0f line%s, %.0f token%s, %s)",
|
||||||
|
os.clock()-startClock,
|
||||||
|
#pathsIn, (#pathsIn == 1) and "" or "s",
|
||||||
|
lineCountCode,
|
||||||
|
lineCount, (lineCount == 1) and "" or "s",
|
||||||
|
tokenCount, (tokenCount == 1) and "" or "s",
|
||||||
|
formatBytes(byteCount)
|
||||||
|
)
|
||||||
|
|
||||||
|
sendMessage("alldone") -- @Incomplete: Use pcall and format error message better?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--[[!===========================================================
|
||||||
|
|
||||||
|
Copyright © 2018-2022 Marcus 'ReFreezed' Thunström
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
==============================================================]]
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
---@class ActionQueue
|
||||||
|
---@field queue table<any, { coroutine: thread, expireTimeMs: number }>
|
||||||
|
actionQueue = {
|
||||||
|
queue = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
---@alias Action fun(deltaSeconds: number)
|
||||||
|
|
||||||
|
local close = coroutine.close
|
||||||
|
|
||||||
|
--- Added actions will be called on every runWaiting() update.
|
||||||
|
--- They will continue to be executed until they return Succeeded or Failed instead of NeedsMoreTime.
|
||||||
|
---
|
||||||
|
--- Replaces any existing action with the given id.
|
||||||
|
--- If the initial call of action() doesn't return NeedsMoreTime, this function will not bother adding it to the queue.
|
||||||
|
---@param id any
|
||||||
|
---@param maxTimeMs number
|
||||||
|
---@param action Action
|
||||||
|
function actionQueue:upsert(id, maxTimeMs, action)
|
||||||
|
if self.queue[id] then
|
||||||
|
close(self.queue[id].coroutine)
|
||||||
|
end
|
||||||
|
self.queue[id] = {
|
||||||
|
coroutine = coroutine.create(action),
|
||||||
|
expireTimeMs = maxTimeMs + playdate.getCurrentTimeMilliseconds(),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- The new action will not be added if an entry with the current id already exists in the queue.
|
||||||
|
---@param id any
|
||||||
|
---@param maxTimeMs number
|
||||||
|
---@param action Action
|
||||||
|
function actionQueue:newOnly(id, maxTimeMs, action)
|
||||||
|
if self.queue[id] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.queue[id] = {
|
||||||
|
coroutine = coroutine.create(action),
|
||||||
|
expireTimeMs = maxTimeMs + playdate.getCurrentTimeMilliseconds(),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Must be called on every playdate.update() to check for (and run) any waiting tasks.
|
||||||
|
--- Actions that return NeedsMoreTime will not be removed from the queue unless they have expired.
|
||||||
|
---@param deltaSeconds number
|
||||||
|
function actionQueue:runWaiting(deltaSeconds)
|
||||||
|
local currentTimeMs = playdate.getCurrentTimeMilliseconds()
|
||||||
|
|
||||||
|
for id, actionObject in pairs(self.queue) do
|
||||||
|
coroutine.resume(actionObject.coroutine, deltaSeconds)
|
||||||
|
|
||||||
|
if currentTimeMs > actionObject.expireTimeMs then
|
||||||
|
close(actionObject.coroutine)
|
||||||
|
end
|
||||||
|
|
||||||
|
if coroutine.status(actionObject.coroutine) == "dead" then
|
||||||
|
self.queue[id] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
if not playdate or playdate.TEST_MODE then
|
||||||
|
return actionQueue
|
||||||
|
end
|
|
@ -1,4 +1,6 @@
|
||||||
local AnnouncementFont <const> = playdate.graphics.font.new("fonts/Roobert-20-Medium.pft")
|
local gfx = playdate.graphics
|
||||||
|
|
||||||
|
local AnnouncementFont <const> = Roobert20Medium
|
||||||
local AnnouncementTransitionMs <const> = 300
|
local AnnouncementTransitionMs <const> = 300
|
||||||
local AnnouncerMarginX <const> = 26
|
local AnnouncerMarginX <const> = 26
|
||||||
|
|
||||||
|
@ -7,15 +9,21 @@ local AnnouncerAnimatorInY <const> =
|
||||||
local AnnouncerAnimatorOutY <const> =
|
local AnnouncerAnimatorOutY <const> =
|
||||||
playdate.graphics.animator.new(AnnouncementTransitionMs, 0, -70, playdate.easingFunctions.outQuint)
|
playdate.graphics.animator.new(AnnouncementTransitionMs, 0, -70, playdate.easingFunctions.outQuint)
|
||||||
|
|
||||||
-- selene: allow(unscoped_variables)
|
---@class Announcer
|
||||||
announcer = {
|
---@field textQueue string[]
|
||||||
textQueue = {},
|
---@field animatorY pd_animator
|
||||||
animatorY = AnnouncerAnimatorInY,
|
Announcer = {}
|
||||||
}
|
|
||||||
|
|
||||||
local DurationMs <const> = 3000
|
function Announcer.new()
|
||||||
|
return setmetatable({
|
||||||
|
textQueue = {},
|
||||||
|
animatorY = AnnouncerAnimatorInY,
|
||||||
|
}, { __index = Announcer })
|
||||||
|
end
|
||||||
|
|
||||||
function announcer.popIn(self)
|
local DurationMs <const> = 2000
|
||||||
|
|
||||||
|
function Announcer:popIn()
|
||||||
self.animatorY = AnnouncerAnimatorInY
|
self.animatorY = AnnouncerAnimatorInY
|
||||||
self.animatorY:reset()
|
self.animatorY:reset()
|
||||||
|
|
||||||
|
@ -36,20 +44,22 @@ function announcer.popIn(self)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function announcer.say(self, text)
|
---@param text string
|
||||||
|
function Announcer:say(text)
|
||||||
self.textQueue[#self.textQueue + 1] = text
|
self.textQueue[#self.textQueue + 1] = text
|
||||||
if #self.textQueue == 1 then
|
if #self.textQueue == 1 then
|
||||||
self:popIn()
|
self:popIn()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function announcer.draw(self, x, y)
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
function Announcer:draw(x, y)
|
||||||
if #self.textQueue == 0 then
|
if #self.textQueue == 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
x = x - 5 -- Infield center is slightly offset from screen center
|
x = x - 5 -- Infield center is slightly offset from screen center
|
||||||
|
|
||||||
local gfx = playdate.graphics
|
|
||||||
local originalDrawMode = gfx.getImageDrawMode()
|
local originalDrawMode = gfx.getImageDrawMode()
|
||||||
local width = math.max(150, (AnnouncerMarginX * 2) + AnnouncementFont:getTextWidth(self.textQueue[1]))
|
local width = math.max(150, (AnnouncerMarginX * 2) + AnnouncementFont:getTextWidth(self.textQueue[1]))
|
||||||
local animY = self.animatorY:currentValue()
|
local animY = self.animatorY:currentValue()
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
-- GENERATED FILE - DO NOT EDIT
|
||||||
|
-- Instead, edit the source file directly: assets.lua2p.
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
BallBackground = playdate.graphics.image.new("assets/images/game/BallBackground.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
BigBat = playdate.graphics.image.new("assets/images/game/BigBat.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
DarkPlayerAwayBack = playdate.graphics.image.new("assets/images/game/DarkPlayerAwayBack.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
DarkPlayerAwayBase = playdate.graphics.image.new("assets/images/game/DarkPlayerAwayBase.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
DarkPlayerFrown = playdate.graphics.image.new("assets/images/game/DarkPlayerFrown.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
DarkPlayerHomeBack = playdate.graphics.image.new("assets/images/game/DarkPlayerHomeBack.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
DarkPlayerHomeBase = playdate.graphics.image.new("assets/images/game/DarkPlayerHomeBase.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
DarkPlayerSmile = playdate.graphics.image.new("assets/images/game/DarkPlayerSmile.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
DarkSkinFan = playdate.graphics.image.new("assets/images/game/DarkSkinFan.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
GameLogo = playdate.graphics.image.new("assets/images/game/GameLogo.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
GloveHoldingBall = playdate.graphics.image.new("assets/images/game/GloveHoldingBall.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
Glove = playdate.graphics.image.new("assets/images/game/Glove.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
GrassBackground = playdate.graphics.image.new("assets/images/game/GrassBackground.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
GrassBackgroundSmall = playdate.graphics.image.new("assets/images/game/GrassBackgroundSmall.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
Hat = playdate.graphics.image.new("assets/images/game/Hat.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
LightPlayerAwayBack = playdate.graphics.image.new("assets/images/game/LightPlayerAwayBack.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
LightPlayerAwayBase = playdate.graphics.image.new("assets/images/game/LightPlayerAwayBase.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
LightPlayerFrown = playdate.graphics.image.new("assets/images/game/LightPlayerFrown.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
LightPlayerHomeBack = playdate.graphics.image.new("assets/images/game/LightPlayerHomeBack.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
LightPlayerHomeBase = playdate.graphics.image.new("assets/images/game/LightPlayerHomeBase.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
LightPlayerSmile = playdate.graphics.image.new("assets/images/game/LightPlayerSmile.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
LightSkinFan = playdate.graphics.image.new("assets/images/game/LightSkinFan.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
MenuImage = playdate.graphics.image.new("assets/images/game/MenuImage.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
Minimap = playdate.graphics.image.new("assets/images/game/Minimap.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
PerfectPowerBg = playdate.graphics.image.new("assets/images/game/PerfectPowerBg.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
PerfectPowerFlickerLeft = playdate.graphics.image.new("assets/images/game/PerfectPowerFlickerLeft.png")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
PerfectPowerFlickerRight = playdate.graphics.image.new("assets/images/game/PerfectPowerFlickerRight.png")
|
||||||
|
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_sampleplayer
|
||||||
|
BatCrackReverb = playdate.sound.sampleplayer.new("assets/sounds/BatCrackReverb.wav")
|
||||||
|
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_sampleplayer
|
||||||
|
BootTuneOrgany = playdate.sound.sampleplayer.new("assets/music/BootTuneOrgany.wav")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_sampleplayer
|
||||||
|
BootTune = playdate.sound.sampleplayer.new("assets/music/BootTune.wav")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_sampleplayer
|
||||||
|
MenuMusic = playdate.sound.sampleplayer.new("assets/music/MenuMusic.wav")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_sampleplayer
|
||||||
|
TinnyBackground = playdate.sound.sampleplayer.new("assets/music/TinnyBackground.wav")
|
||||||
|
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_font
|
||||||
|
AshevilleSans14Bold = playdate.graphics.font.new("assets/fonts/Asheville-Sans-14-Bold.pft")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_font
|
||||||
|
FontFullCircle = playdate.graphics.font.new("assets/fonts/Font-Full-Circle.pft")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_font
|
||||||
|
NanoSans = playdate.graphics.font.new("assets/fonts/Nano Sans.pft")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_font
|
||||||
|
Roobert11Medium = playdate.graphics.font.new("assets/fonts/Roobert-11-Medium.pft")
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_font
|
||||||
|
Roobert20Medium = playdate.graphics.font.new("assets/fonts/Roobert-20-Medium.pft")
|
||||||
|
|
||||||
|
|
||||||
|
Logos = {
|
||||||
|
{ name = "Base", image = playdate.graphics.image.new("assets/images/game/logos/Base.png") },
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
{ name = "Arrows", image = playdate.graphics.image.new("assets/images/game/logos/Arrows.png") },
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
{ name = "Cats", image = playdate.graphics.image.new("assets/images/game/logos/Cats.png") },
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
{ name = "Checkmarks", image = playdate.graphics.image.new("assets/images/game/logos/Checkmarks.png") },
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
{ name = "FingerGuns", image = playdate.graphics.image.new("assets/images/game/logos/FingerGuns.png") },
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
{ name = "Frown", image = playdate.graphics.image.new("assets/images/game/logos/Frown.png") },
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
{ name = "Hearts", image = playdate.graphics.image.new("assets/images/game/logos/Hearts.png") },
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
{ name = "Smiles", image = playdate.graphics.image.new("assets/images/game/logos/Smiles.png") },
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
---@type pd_image
|
||||||
|
{ name = "Turds", image = playdate.graphics.image.new("assets/images/game/logos/Turds.png") },
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
!(function dirLookup(dir, extension, newFunc, type, sep, indent, handle)
|
||||||
|
indent = indent or ""
|
||||||
|
sep = sep or "\n\n"
|
||||||
|
handle = handle ~= nil and handle or function(varName, value)
|
||||||
|
return varName .. ' = ' .. value
|
||||||
|
end
|
||||||
|
|
||||||
|
local p = io.popen('find src/' .. dir .. ' -maxdepth 1 -type f | sort -h')
|
||||||
|
|
||||||
|
local assetCode = ""
|
||||||
|
--Loop through all files
|
||||||
|
for file in p:lines() do
|
||||||
|
if file:find(extension) then
|
||||||
|
local varName = file:gsub(".*/(.*)." .. extension, "%1")
|
||||||
|
file = file:gsub("src/", "")
|
||||||
|
assetCode = assetCode .. indent .. '-- luacheck: ignore\n'
|
||||||
|
assetCode = assetCode .. indent .. '---@type ' .. type ..'\n'
|
||||||
|
assetCode = assetCode .. indent .. handle(varName, newFunc .. '("' .. file .. '")') .. sep
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return assetCode
|
||||||
|
end
|
||||||
|
function generatedFileWarning()
|
||||||
|
-- Only in a function to make clear that THIS .lua2p is not the generated file!
|
||||||
|
return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: assets.lua2p."
|
||||||
|
end)!!(generatedFileWarning())
|
||||||
|
|
||||||
|
!!(dirLookup('assets/images/game', 'png', 'playdate.graphics.image.new', 'pd_image'))
|
||||||
|
!!(dirLookup('assets/sounds', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer'))
|
||||||
|
!!(dirLookup('assets/music', 'wav', 'playdate.sound.sampleplayer.new', 'pd_sampleplayer'))
|
||||||
|
!!(dirLookup('assets/fonts', 'fnt', 'playdate.graphics.font.new', 'pd_font', nil, nil, function(varName, value)
|
||||||
|
return varName:gsub("[- ]", "") .. " = " .. value:gsub("fnt", "pft")
|
||||||
|
end))
|
||||||
|
Logos = {
|
||||||
|
{ name = "Base", image = playdate.graphics.image.new("assets/images/game/logos/Base.png") },
|
||||||
|
|
||||||
|
!!(dirLookup('assets/images/game/logos -not -name "Base.png"', 'png', 'playdate.graphics.image.new', 'pd_image', ",\n\n", " ", function(varName, value)
|
||||||
|
return '{ name = "' .. varName .. '", image = ' .. value .. ' }'
|
||||||
|
end))
|
||||||
|
}
|
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,295 @@
|
||||||
|
tracking=1
|
||||||
|
space 3
|
||||||
|
! 2
|
||||||
|
" 5
|
||||||
|
# 9
|
||||||
|
$ 8
|
||||||
|
% 12
|
||||||
|
& 11
|
||||||
|
' 3
|
||||||
|
( 5
|
||||||
|
) 5
|
||||||
|
* 8
|
||||||
|
+ 8
|
||||||
|
, 3
|
||||||
|
- 6
|
||||||
|
. 2
|
||||||
|
/ 6
|
||||||
|
0 9
|
||||||
|
1 4
|
||||||
|
2 9
|
||||||
|
3 9
|
||||||
|
4 9
|
||||||
|
5 9
|
||||||
|
6 9
|
||||||
|
7 9
|
||||||
|
8 10
|
||||||
|
9 9
|
||||||
|
: 2
|
||||||
|
; 2
|
||||||
|
< 7
|
||||||
|
= 7
|
||||||
|
> 7
|
||||||
|
? 9
|
||||||
|
@ 11
|
||||||
|
A 10
|
||||||
|
B 9
|
||||||
|
C 9
|
||||||
|
D 9
|
||||||
|
E 8
|
||||||
|
F 8
|
||||||
|
G 9
|
||||||
|
H 9
|
||||||
|
I 2
|
||||||
|
J 8
|
||||||
|
K 10
|
||||||
|
L 9
|
||||||
|
M 12
|
||||||
|
N 9
|
||||||
|
O 9
|
||||||
|
P 9
|
||||||
|
Q 9
|
||||||
|
R 9
|
||||||
|
S 9
|
||||||
|
T 10
|
||||||
|
U 9
|
||||||
|
V 10
|
||||||
|
W 14
|
||||||
|
X 8
|
||||||
|
Y 8
|
||||||
|
Z 8
|
||||||
|
[ 3
|
||||||
|
\ 6
|
||||||
|
] 3
|
||||||
|
^ 6
|
||||||
|
_ 8
|
||||||
|
` 3
|
||||||
|
a 8
|
||||||
|
b 8
|
||||||
|
c 8
|
||||||
|
d 8
|
||||||
|
e 8
|
||||||
|
f 6
|
||||||
|
g 8
|
||||||
|
h 8
|
||||||
|
i 2
|
||||||
|
j 4
|
||||||
|
k 8
|
||||||
|
l 2
|
||||||
|
m 12
|
||||||
|
n 8
|
||||||
|
o 8
|
||||||
|
p 8
|
||||||
|
q 8
|
||||||
|
r 6
|
||||||
|
s 8
|
||||||
|
t 6
|
||||||
|
u 8
|
||||||
|
v 8
|
||||||
|
w 12
|
||||||
|
x 9
|
||||||
|
y 8
|
||||||
|
z 8
|
||||||
|
{ 6
|
||||||
|
| 2
|
||||||
|
} 6
|
||||||
|
~ 10
|
||||||
|
… 8
|
||||||
|
¥ 8
|
||||||
|
‼ 5
|
||||||
|
™ 8
|
||||||
|
© 11
|
||||||
|
® 11
|
||||||
|
。 16
|
||||||
|
、 16
|
||||||
|
ぁ 16
|
||||||
|
あ 16
|
||||||
|
ぃ 16
|
||||||
|
い 16
|
||||||
|
ぅ 16
|
||||||
|
う 16
|
||||||
|
ぇ 16
|
||||||
|
え 16
|
||||||
|
ぉ 16
|
||||||
|
お 16
|
||||||
|
か 16
|
||||||
|
が 16
|
||||||
|
き 16
|
||||||
|
ぎ 16
|
||||||
|
く 16
|
||||||
|
ぐ 16
|
||||||
|
け 16
|
||||||
|
げ 16
|
||||||
|
こ 16
|
||||||
|
ご 16
|
||||||
|
さ 16
|
||||||
|
ざ 16
|
||||||
|
し 16
|
||||||
|
じ 16
|
||||||
|
す 16
|
||||||
|
ず 16
|
||||||
|
せ 16
|
||||||
|
ぜ 16
|
||||||
|
そ 16
|
||||||
|
ぞ 16
|
||||||
|
た 16
|
||||||
|
だ 16
|
||||||
|
ち 16
|
||||||
|
ぢ 16
|
||||||
|
っ 16
|
||||||
|
つ 16
|
||||||
|
づ 16
|
||||||
|
て 16
|
||||||
|
で 16
|
||||||
|
と 16
|
||||||
|
ど 16
|
||||||
|
な 16
|
||||||
|
に 16
|
||||||
|
ぬ 16
|
||||||
|
ね 16
|
||||||
|
の 16
|
||||||
|
は 16
|
||||||
|
ば 16
|
||||||
|
ぱ 16
|
||||||
|
ひ 16
|
||||||
|
び 16
|
||||||
|
ぴ 16
|
||||||
|
ふ 16
|
||||||
|
ぶ 16
|
||||||
|
ぷ 16
|
||||||
|
へ 16
|
||||||
|
べ 16
|
||||||
|
ぺ 16
|
||||||
|
ほ 16
|
||||||
|
ぼ 16
|
||||||
|
ぽ 16
|
||||||
|
ま 16
|
||||||
|
み 16
|
||||||
|
む 16
|
||||||
|
め 16
|
||||||
|
も 16
|
||||||
|
ゃ 16
|
||||||
|
や 16
|
||||||
|
ゅ 16
|
||||||
|
ゆ 16
|
||||||
|
ょ 16
|
||||||
|
よ 16
|
||||||
|
ら 16
|
||||||
|
り 16
|
||||||
|
る 16
|
||||||
|
れ 16
|
||||||
|
ろ 16
|
||||||
|
ゎ 16
|
||||||
|
わ 16
|
||||||
|
ゐ 16
|
||||||
|
ゑ 16
|
||||||
|
を 16
|
||||||
|
ん 16
|
||||||
|
ゔ 16
|
||||||
|
ゕ 16
|
||||||
|
ゖ 16
|
||||||
|
゛ 1
|
||||||
|
゜ 0
|
||||||
|
ゝ 16
|
||||||
|
ゞ 16
|
||||||
|
ゟ 16
|
||||||
|
゠ 16
|
||||||
|
ァ 16
|
||||||
|
ア 16
|
||||||
|
ィ 16
|
||||||
|
イ 16
|
||||||
|
ゥ 16
|
||||||
|
ウ 16
|
||||||
|
ェ 16
|
||||||
|
エ 16
|
||||||
|
ォ 16
|
||||||
|
オ 16
|
||||||
|
カ 16
|
||||||
|
ガ 16
|
||||||
|
キ 16
|
||||||
|
ギ 16
|
||||||
|
ク 16
|
||||||
|
グ 16
|
||||||
|
ケ 16
|
||||||
|
ゲ 16
|
||||||
|
コ 16
|
||||||
|
ゴ 16
|
||||||
|
サ 16
|
||||||
|
ザ 16
|
||||||
|
シ 16
|
||||||
|
ジ 16
|
||||||
|
ス 16
|
||||||
|
ズ 16
|
||||||
|
セ 16
|
||||||
|
ゼ 16
|
||||||
|
ソ 16
|
||||||
|
ゾ 16
|
||||||
|
タ 16
|
||||||
|
ダ 16
|
||||||
|
チ 16
|
||||||
|
ヂ 16
|
||||||
|
ッ 16
|
||||||
|
ツ 16
|
||||||
|
ヅ 16
|
||||||
|
テ 16
|
||||||
|
デ 16
|
||||||
|
ト 16
|
||||||
|
ド 16
|
||||||
|
ナ 16
|
||||||
|
ニ 16
|
||||||
|
ヌ 16
|
||||||
|
ネ 16
|
||||||
|
ノ 16
|
||||||
|
ハ 16
|
||||||
|
バ 16
|
||||||
|
パ 16
|
||||||
|
ヒ 16
|
||||||
|
ビ 16
|
||||||
|
ピ 16
|
||||||
|
フ 16
|
||||||
|
ブ 16
|
||||||
|
プ 16
|
||||||
|
ヘ 16
|
||||||
|
ベ 16
|
||||||
|
ペ 16
|
||||||
|
ホ 16
|
||||||
|
ボ 16
|
||||||
|
ポ 16
|
||||||
|
マ 16
|
||||||
|
ミ 16
|
||||||
|
ム 16
|
||||||
|
メ 16
|
||||||
|
モ 16
|
||||||
|
ャ 16
|
||||||
|
ヤ 16
|
||||||
|
ュ 16
|
||||||
|
ユ 16
|
||||||
|
ョ 16
|
||||||
|
ヨ 16
|
||||||
|
ラ 16
|
||||||
|
リ 16
|
||||||
|
ル 16
|
||||||
|
レ 16
|
||||||
|
ロ 16
|
||||||
|
ヮ 16
|
||||||
|
ワ 16
|
||||||
|
ヰ 16
|
||||||
|
ヱ 16
|
||||||
|
ヲ 16
|
||||||
|
ン 16
|
||||||
|
ヴ 16
|
||||||
|
ヵ 16
|
||||||
|
ヶ 16
|
||||||
|
ヷ 16
|
||||||
|
ヸ 16
|
||||||
|
ヹ 16
|
||||||
|
ヺ 16
|
||||||
|
・ 16
|
||||||
|
ー 16
|
||||||
|
ヽ 16
|
||||||
|
ヾ 16
|
||||||
|
ヿ 16
|
||||||
|
「 16
|
||||||
|
」 16
|
||||||
|
円 16
|
||||||
|
<EFBFBD> 13
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,104 @@
|
||||||
|
tracking=1
|
||||||
|
|
||||||
|
space 2
|
||||||
|
A 3
|
||||||
|
B 3
|
||||||
|
T 3
|
||||||
|
a 3
|
||||||
|
b 3
|
||||||
|
c 3
|
||||||
|
d 3
|
||||||
|
e 3
|
||||||
|
f 3
|
||||||
|
g 3
|
||||||
|
h 3
|
||||||
|
i 1
|
||||||
|
l 2
|
||||||
|
q 3
|
||||||
|
r 3
|
||||||
|
s 3
|
||||||
|
w 5
|
||||||
|
z 3
|
||||||
|
j 1
|
||||||
|
n 3
|
||||||
|
o 3
|
||||||
|
p 3
|
||||||
|
m 5
|
||||||
|
k 3
|
||||||
|
t 3
|
||||||
|
u 3
|
||||||
|
v 3
|
||||||
|
y 3
|
||||||
|
x 3
|
||||||
|
. 1
|
||||||
|
C 3
|
||||||
|
D 3
|
||||||
|
E 3
|
||||||
|
F 3
|
||||||
|
G 3
|
||||||
|
H 3
|
||||||
|
I 3
|
||||||
|
0 3
|
||||||
|
1 3
|
||||||
|
8 3
|
||||||
|
9 3
|
||||||
|
7 3
|
||||||
|
6 3
|
||||||
|
5 3
|
||||||
|
4 3
|
||||||
|
3 3
|
||||||
|
2 3
|
||||||
|
: 1
|
||||||
|
; 1
|
||||||
|
! 1
|
||||||
|
" 3
|
||||||
|
{ 3
|
||||||
|
} 3
|
||||||
|
| 1
|
||||||
|
J 3
|
||||||
|
K 3
|
||||||
|
L 3
|
||||||
|
M 5
|
||||||
|
N 4
|
||||||
|
O 3
|
||||||
|
W 5
|
||||||
|
U 3
|
||||||
|
V 3
|
||||||
|
X 3
|
||||||
|
Y 3
|
||||||
|
Z 3
|
||||||
|
Q 3
|
||||||
|
S 3
|
||||||
|
R 3
|
||||||
|
P 3
|
||||||
|
[ 2
|
||||||
|
] 2
|
||||||
|
^ 3
|
||||||
|
< 3
|
||||||
|
= 3
|
||||||
|
> 3
|
||||||
|
? 3
|
||||||
|
@ 4
|
||||||
|
\ 3
|
||||||
|
_ 3
|
||||||
|
` 2
|
||||||
|
~ 5
|
||||||
|
¥ 3
|
||||||
|
… 5
|
||||||
|
™ 5
|
||||||
|
‼ 3
|
||||||
|
© 5
|
||||||
|
® 5
|
||||||
|
<EFBFBD> 5
|
||||||
|
# 5
|
||||||
|
/ 3
|
||||||
|
- 3
|
||||||
|
+ 3
|
||||||
|
, 1
|
||||||
|
* 3
|
||||||
|
) 2
|
||||||
|
( 2
|
||||||
|
' 1
|
||||||
|
$ 3
|
||||||
|
% 3
|
||||||
|
& 4
|
After Width: | Height: | Size: 7.5 KiB |
|
@ -0,0 +1,242 @@
|
||||||
|
--metrics={"baseline":0,"xHeight":0,"capHeight":0,"pairs":{"ac":[0,0],"ad":[0,0],"ae":[0,0],"af":[-1,0,0,0],"ag":[0,0],"ap":[0,0],"ar":[1,0,0,0],"at":[-1,0,0,0],"au":[0,0],"av":[-1,0,0,0],"aw":[-1,0,0,0],"ay":[-1,0,0,0],"b,":[-1,0,0,0],"b.":[-1,0,0,0],"bl":[0,0],"br":[0,0],"bu":[0,0],"by":[-1,0,0,0],"ca":[0,0],"ch":[0,0],"ck":[0,0],"d,":[-1,0,0,0],"d.":[0,0],"da":[0,0],"dc":[0,0],"de":[0,0],"dg":[0,0],"do":[0,0],"dt":[0,0],"du":[0,0],"dv":[0,0],"dw":[0,0],"dy":[0,0],"e,":[-1,0,0,0],"e.":[-1,0,0,0],"ea":[0,0],"ei":[0,0],"el":[0,0],"em":[0,0],"en":[0,0],"ep":[0,0],"er":[0,0],"et":[-1,0,0,0],"eu":[0,0],"ev":[-1,0,0,0],"ew":[-1,0,0,0],"ey":[-1,0,0,0],"f,":[-2,0,0,0],"f.":[-2,0,0,0],"fa":[-1,0,0,0],"fe":[-1,0,0,0],"ff":[-2,0,0,0],"fi":[0,0],"fl":[-1,0,0,0],"fo":[-2,0,0,0],"g,":[0,0],"g.":[0,0],"ga":[0,0],"ge":[0,0],"gg":[0,0],"gh":[0,0],"gl":[0,0],"go":[0,0],"hc":[0,0],"hd":[0,0],"he":[0,0],"hg":[0,0],"ho":[0,0],"hp":[0,0],"ht":[-1,0,0,0],"hu":[0,0],"hv":[-1,0,0,0],"hw":[-1,0,0,0],"hy":[-1,0,0,0],"ic":[-1,0,0,0],"id":[-1,0,0,0],"ie":[-1,0,0,0],"ig":[-1,0,0,0],"io":[-1,0,0,0],"ip":[-1,0,0,0],"it":[-2,0,0,0],"iu":[-1,0,0,0],"iv":[-1,0,0,0],"j,":[0,0],"j.":[0,0],"ja":[0,0],"je":[0,0],"jo":[0,0],"ju":[0,0],"ka":[-2,0,0,0],"kc":[-2,0,0,0],"kd":[-2,0,0,0],"ke":[-2,0,0,0],"kg":[-2,0,0,0],"ko":[-2,0,0,0],"la":[0,0],"lc":[0,0],"ld":[0,0],"le":[0,0],"lf":[0,0],"lg":[0,0],"lo":[0,0],"Lo":[-1,0,0,0],"lp":[0,0],"lq":[0,0],"lu":[0,0],"lv":[0,0],"lw":[0,0],"ly":[0,0],"ma":[0,0],"mc":[0,0],"md":[0,0],"me":[0,0],"mg":[0,0],"mn":[0,0],"mo":[0,0],"mp":[0,0],"mt":[-1,0,0,0],"mu":[0,0],"mv":[-1,0,0,0],"my":[-1,0,0,0],"nc":[0,0],"nd":[0,0],"ne":[0,0],"ng":[0,0],"no":[0,0],"np":[0,0],"nt":[-1,0,0,0],"nu":[0,0],"nv":[-1,0,0,0],"nw":[-1,0,0,0],"ny":[-1,0,0,0],"o,":[-2,0,0,0],"o.":[-1,0,0,0],"ob":[0,0],"of":[-2,0,0,0],"oh":[0,0],"oj":[-2,0,0,0],"ok":[0,0],"ol":[0,0],"om":[0,0],"on":[0,0],"op":[0,0],"or":[0,0],"ou":[0,0],"ov":[-1,0,0,0],"ow":[-1,0,0,0],"ox":[-1,0,0,0],"oy":[-1,0,0,0],"p,":[-1,0,0,0],"p.":[-1,0,0,0],"pa":[0,0],"ph":[0,0],"pi":[0,0],"pl":[0,0],"pp":[0,0],"pu":[0,0],"qu":[0,0],"r,":[-3,0,0,0],"r.":[-2,0,0,0],"ra":[-1,0,0,0],"rd":[-1,0,0,0],"re":[-1,0,0,0],"rg":[-1,0,0,0],"rk":[0,0],"rl":[0,0],"rm":[0,0],"rn":[0,0],"ro":[-2,0,0,0],"rq":[-1,0,0,0],"rr":[0,0],"rt":[-1,0,0,0],"rv":[0,0],"ry":[0,0],"s,":[-1,0,0,0],"s.":[-1,0,0,0],"sh":[0,0],"st":[-1,0,0,0],"su":[0,0],"t,":[0,0],"t.":[1,0,0,0],"ta":[1,0,0,0],"td":[0,0],"te":[0,0],"th":[0,0],"ti":[1,0,0,0],"tl":[1,0,0,0],"to":[0,0],"ua":[0,0],"uc":[0,0],"ud":[0,0],"ue":[0,0],"ug":[0,0],"uo":[0,0],"up":[1,0,0,0],"uq":[0,0],"ur":[1,0,0,0],"ut":[0,0],"uv":[0,0],"uw":[0,0],"uy":[0,0],"v,":[-2,0,0,0],"v.":[-2,0,0,0],"va":[0,0],"vb":[0,0],"vc":[-1,0,0,0],"vd":[-1,0,0,0],"ve":[-1,0,0,0],"vg":[-1,0,0,0],"vo":[-1,0,0,0],"vv":[0,0],"vy":[-1,0,0,0],"w,":[-2,0,0,0],"w.":[-1,0,0,0],"wa":[-1,0,0,0],"wd":[-1,0,0,0],"we":[-1,0,0,0],"wg":[-1,0,0,0],"wh":[0,0],"wo":[-1,0,0,0],"wx":[-1,0,0,0],"xa":[-1,0,0,0],"xe":[-1,0,0,0],"xo":[-1,0,0,0],"y,":[-3,0,0,0],"y.":[-2,0,0,0],"ya":[-1,0,0,0],"yc":[-1,0,0,0],"yd":[-1,0,0,0],"ye":[-1,0,0,0],"Yo":[-2,0,0,0],"yo":[-1,0,0,0],"LO":[-2,0,0,0],"AT":[-3,0,0,0],"AY":[-3,0,0,0],"//":[-4,0,0,0],"/d":[-2,0,0,0],"/p":[-1,0,0,0],"tp":[1,0,0,0],"t:":[1,0,0,0],"/w":[-1,0,0,0],"ot":[-1,0,0,0],"Wo":[-2,0,0,0],"Fo":[-2,0,0,0],"Fu":[-2,0,0,0],"Vu":[-1,0,0,0],"Tu":[-2,0,0,0],"To":[-3,0,0,0],"Vo":[-2,0,0,0],"Yu":[-1,0,0,0],"Zo":[-1,0,0,0],"ty":[-1,0,0,0],"is":[-1,0,0,0]},"left":[],"right":[]}
|
||||||
|
tracking=1
|
||||||
|
|
||||||
|
0 12
|
||||||
|
1 5
|
||||||
|
2 11
|
||||||
|
3 12
|
||||||
|
4 12
|
||||||
|
5 11
|
||||||
|
6 12
|
||||||
|
7 11
|
||||||
|
8 11
|
||||||
|
9 12
|
||||||
|
space 3
|
||||||
|
! 2
|
||||||
|
" 6
|
||||||
|
# 14
|
||||||
|
$ 11
|
||||||
|
% 15
|
||||||
|
& 13
|
||||||
|
' 2
|
||||||
|
( 5
|
||||||
|
) 5
|
||||||
|
* 8
|
||||||
|
+ 10
|
||||||
|
, 3
|
||||||
|
- 8
|
||||||
|
. 2
|
||||||
|
/ 9
|
||||||
|
: 2
|
||||||
|
; 4
|
||||||
|
< 9
|
||||||
|
= 11
|
||||||
|
> 9
|
||||||
|
? 9
|
||||||
|
@ 18
|
||||||
|
A 13
|
||||||
|
B 11
|
||||||
|
C 14
|
||||||
|
D 12
|
||||||
|
E 10
|
||||||
|
F 10
|
||||||
|
G 14
|
||||||
|
H 12
|
||||||
|
I 2
|
||||||
|
J 5
|
||||||
|
K 12
|
||||||
|
L 9
|
||||||
|
M 15
|
||||||
|
N 11
|
||||||
|
O 15
|
||||||
|
P 10
|
||||||
|
Q 15
|
||||||
|
R 10
|
||||||
|
S 11
|
||||||
|
T 12
|
||||||
|
U 12
|
||||||
|
V 12
|
||||||
|
W 18
|
||||||
|
X 11
|
||||||
|
Y 10
|
||||||
|
Z 11
|
||||||
|
[ 5
|
||||||
|
\ 9
|
||||||
|
] 5
|
||||||
|
^ 7
|
||||||
|
_ 11
|
||||||
|
` 3
|
||||||
|
a 9
|
||||||
|
b 10
|
||||||
|
c 10
|
||||||
|
d 10
|
||||||
|
e 10
|
||||||
|
f 7
|
||||||
|
g 10
|
||||||
|
h 9
|
||||||
|
i 3
|
||||||
|
j 4
|
||||||
|
k 10
|
||||||
|
l 2
|
||||||
|
m 16
|
||||||
|
n 9
|
||||||
|
o 11
|
||||||
|
p 10
|
||||||
|
q 10
|
||||||
|
r 6
|
||||||
|
s 8
|
||||||
|
t 7
|
||||||
|
u 9
|
||||||
|
v 8
|
||||||
|
w 14
|
||||||
|
x 9
|
||||||
|
y 10
|
||||||
|
z 9
|
||||||
|
{ 6
|
||||||
|
| 2
|
||||||
|
} 6
|
||||||
|
~ 10
|
||||||
|
¥ 10
|
||||||
|
… 12
|
||||||
|
™ 16
|
||||||
|
‼ 6
|
||||||
|
© 15
|
||||||
|
® 15
|
||||||
|
<EFBFBD> 15
|
||||||
|
Ⓐ 18
|
||||||
|
Ⓑ 18
|
||||||
|
🌐 18
|
||||||
|
› 14
|
||||||
|
▸ 12
|
||||||
|
⊙ 18
|
||||||
|
‘ 3
|
||||||
|
’ 3
|
||||||
|
“ 6
|
||||||
|
” 6
|
||||||
|
|
||||||
|
af -1
|
||||||
|
ar 1
|
||||||
|
at -1
|
||||||
|
av -1
|
||||||
|
aw -1
|
||||||
|
ay -1
|
||||||
|
b, -1
|
||||||
|
b. -1
|
||||||
|
by -1
|
||||||
|
d, -1
|
||||||
|
e, -1
|
||||||
|
e. -1
|
||||||
|
et -1
|
||||||
|
ev -1
|
||||||
|
ew -1
|
||||||
|
ey -1
|
||||||
|
f, -2
|
||||||
|
f. -2
|
||||||
|
fa -1
|
||||||
|
fe -1
|
||||||
|
ff -2
|
||||||
|
fl -1
|
||||||
|
fo -2
|
||||||
|
ht -1
|
||||||
|
hv -1
|
||||||
|
hw -1
|
||||||
|
hy -1
|
||||||
|
ic -1
|
||||||
|
id -1
|
||||||
|
ie -1
|
||||||
|
ig -1
|
||||||
|
io -1
|
||||||
|
ip -1
|
||||||
|
it -2
|
||||||
|
iu -1
|
||||||
|
iv -1
|
||||||
|
ka -2
|
||||||
|
kc -2
|
||||||
|
kd -2
|
||||||
|
ke -2
|
||||||
|
kg -2
|
||||||
|
ko -2
|
||||||
|
Lo -1
|
||||||
|
mt -1
|
||||||
|
mv -1
|
||||||
|
my -1
|
||||||
|
nt -1
|
||||||
|
nv -1
|
||||||
|
nw -1
|
||||||
|
ny -1
|
||||||
|
o, -2
|
||||||
|
o. -1
|
||||||
|
of -2
|
||||||
|
oj -2
|
||||||
|
ov -1
|
||||||
|
ow -1
|
||||||
|
ox -1
|
||||||
|
oy -1
|
||||||
|
p, -1
|
||||||
|
p. -1
|
||||||
|
r, -3
|
||||||
|
r. -2
|
||||||
|
ra -1
|
||||||
|
rd -1
|
||||||
|
re -1
|
||||||
|
rg -1
|
||||||
|
ro -2
|
||||||
|
rq -1
|
||||||
|
rt -1
|
||||||
|
s, -1
|
||||||
|
s. -1
|
||||||
|
st -1
|
||||||
|
t. 1
|
||||||
|
ta 1
|
||||||
|
ti 1
|
||||||
|
tl 1
|
||||||
|
up 1
|
||||||
|
ur 1
|
||||||
|
v, -2
|
||||||
|
v. -2
|
||||||
|
vc -1
|
||||||
|
vd -1
|
||||||
|
ve -1
|
||||||
|
vg -1
|
||||||
|
vo -1
|
||||||
|
vy -1
|
||||||
|
w, -2
|
||||||
|
w. -1
|
||||||
|
wa -1
|
||||||
|
wd -1
|
||||||
|
we -1
|
||||||
|
wg -1
|
||||||
|
wo -1
|
||||||
|
wx -1
|
||||||
|
xa -1
|
||||||
|
xe -1
|
||||||
|
xo -1
|
||||||
|
y, -3
|
||||||
|
y. -2
|
||||||
|
ya -1
|
||||||
|
yc -1
|
||||||
|
yd -1
|
||||||
|
ye -1
|
||||||
|
Yo -2
|
||||||
|
yo -1
|
||||||
|
LO -2
|
||||||
|
AT -3
|
||||||
|
AY -3
|
||||||
|
// -4
|
||||||
|
/d -2
|
||||||
|
/p -1
|
||||||
|
tp 1
|
||||||
|
t: 1
|
||||||
|
/w -1
|
||||||
|
ot -1
|
||||||
|
Wo -2
|
||||||
|
Fo -2
|
||||||
|
Fu -2
|
||||||
|
Vu -1
|
||||||
|
Tu -2
|
||||||
|
To -3
|
||||||
|
Vo -2
|
||||||
|
Yu -1
|
||||||
|
Zo -1
|
||||||
|
ty -1
|
||||||
|
is -1
|
After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 626 B |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 7.0 KiB |
After Width: | Height: | Size: 592 B |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 738 B |
After Width: | Height: | Size: 601 B |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 654 B |
After Width: | Height: | Size: 611 B |
After Width: | Height: | Size: 614 B |
After Width: | Height: | Size: 592 B |
After Width: | Height: | Size: 589 B |
After Width: | Height: | Size: 600 B |
After Width: | Height: | Size: 579 B |
After Width: | Height: | Size: 589 B |
After Width: | Height: | Size: 593 B |
After Width: | Height: | Size: 596 B |
After Width: | Height: | Size: 587 B |
After Width: | Height: | Size: 598 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,103 @@
|
||||||
|
---@class Ball
|
||||||
|
---@field x number
|
||||||
|
---@field y number
|
||||||
|
---@field z number
|
||||||
|
---@field size number
|
||||||
|
---@field heldBy Fielder | nil
|
||||||
|
---@field catchable boolean
|
||||||
|
---@field isFlyBall boolean
|
||||||
|
---@field xAnimator SimpleAnimator
|
||||||
|
---@field yAnimator SimpleAnimator
|
||||||
|
---@field sizeAnimator SimpleAnimator
|
||||||
|
---@field floatAnimator SimpleAnimator
|
||||||
|
---@field private animatorLib pd_animator_lib
|
||||||
|
Ball = {}
|
||||||
|
|
||||||
|
local function defaultFloatAnimator(animatorLib)
|
||||||
|
return animatorLib.new(2000, -60, 0, utils.easingHill)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param animatorLib pd_animator_lib
|
||||||
|
---@return Ball
|
||||||
|
function Ball.new(animatorLib)
|
||||||
|
return setmetatable({
|
||||||
|
animatorLib = animatorLib,
|
||||||
|
x = C.Center.x --[[@as number]],
|
||||||
|
y = C.Center.y --[[@as number]],
|
||||||
|
z = 0,
|
||||||
|
catchable = true,
|
||||||
|
size = C.SmallestBallRadius,
|
||||||
|
heldBy = nil --[[@type Runner | nil]],
|
||||||
|
|
||||||
|
xAnimator = utils.staticAnimator(C.BallOffscreen),
|
||||||
|
yAnimator = utils.staticAnimator(C.BallOffscreen),
|
||||||
|
|
||||||
|
-- TODO? Replace these with a ballAnimatorZ?
|
||||||
|
-- ...that might lose some of the magic of both. Compromise available? idk
|
||||||
|
sizeAnimator = utils.staticAnimator(C.SmallestBallRadius),
|
||||||
|
floatAnimator = defaultFloatAnimator(animatorLib),
|
||||||
|
}, { __index = Ball })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param deltaSeconds number
|
||||||
|
function Ball:updatePosition(deltaSeconds)
|
||||||
|
if self.heldBy then
|
||||||
|
utils.moveAtSpeedZ(self, 100 * deltaSeconds, { x = self.heldBy.x, y = self.heldBy.y, z = C.GloveZ })
|
||||||
|
self.size = C.SmallestBallRadius
|
||||||
|
else
|
||||||
|
self.x = self.xAnimator:currentValue()
|
||||||
|
local z = self.floatAnimator:currentValue()
|
||||||
|
-- TODO: This `+ z` is more graphics logic than physics logic
|
||||||
|
self.y = self.yAnimator:currentValue() + z
|
||||||
|
self.z = z
|
||||||
|
if self.z < 2 and self.isFlyBall then
|
||||||
|
print("Ball hit the ground!")
|
||||||
|
self.isFlyBall = false
|
||||||
|
end
|
||||||
|
self.size = C.SmallestBallRadius + math.max(0, (self.floatAnimator:currentValue() - C.GloveZ) / 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Ball:markUncatchable()
|
||||||
|
self.catchable = false
|
||||||
|
playdate.timer.new(200, function()
|
||||||
|
self.catchable = true
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Launches the ball from its current position to the given destination.
|
||||||
|
---@param destX number
|
||||||
|
---@param destY number
|
||||||
|
---@param easingFunc EasingFunc
|
||||||
|
---@param flyTimeMs number | nil
|
||||||
|
---@param floaty boolean | nil
|
||||||
|
---@param customFloater pd_animator | nil
|
||||||
|
---@param isHit boolean
|
||||||
|
function Ball:launch(destX, destY, easingFunc, flyTimeMs, floaty, customFloater, isHit)
|
||||||
|
self.heldBy = nil
|
||||||
|
self.isFlyBall = isHit
|
||||||
|
|
||||||
|
-- Prevent silly insta-catches
|
||||||
|
self:markUncatchable()
|
||||||
|
|
||||||
|
if not flyTimeMs then
|
||||||
|
flyTimeMs = utils.distanceBetween(self.x, self.y, destX, destY) * C.DefaultLaunchPower
|
||||||
|
end
|
||||||
|
|
||||||
|
if customFloater then
|
||||||
|
self.floatAnimator = customFloater
|
||||||
|
else
|
||||||
|
self.sizeAnimator = self.animatorLib.new(flyTimeMs, C.SmallestBallRadius, 9, utils.easingHill)
|
||||||
|
self.floatAnimator = defaultFloatAnimator(self.animatorLib)
|
||||||
|
end
|
||||||
|
self.yAnimator = self.animatorLib.new(flyTimeMs, self.y, destY, easingFunc)
|
||||||
|
self.xAnimator = self.animatorLib.new(flyTimeMs, self.x, destX, easingFunc)
|
||||||
|
if floaty then
|
||||||
|
self.floatAnimator:reset(flyTimeMs)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
if not playdate or playdate.TEST_MODE then
|
||||||
|
return Ball
|
||||||
|
end
|
|
@ -0,0 +1,298 @@
|
||||||
|
--- @class Runner
|
||||||
|
--- @field x number
|
||||||
|
--- @field y number
|
||||||
|
--- @field nextBase Base
|
||||||
|
--- @field prevBase Base | nil
|
||||||
|
--- @field forcedTo Base | nil
|
||||||
|
--- @field spriteIndex number
|
||||||
|
|
||||||
|
---@class Baserunning
|
||||||
|
---@field runners Runner[]
|
||||||
|
---@field outRunners Runner[]
|
||||||
|
---@field scoredRunners Runner[]
|
||||||
|
---@field batter Runner | nil
|
||||||
|
---@field outs number
|
||||||
|
-- TODO: Replace with timer, repeatedly reset, instead of constantly setting to 0
|
||||||
|
---@field secondsSinceLastRunnerMove number
|
||||||
|
---@field announcer Announcer
|
||||||
|
---@field onThirdOut fun()
|
||||||
|
Baserunning = {}
|
||||||
|
|
||||||
|
-- TODO: Implement slides? Would require making fielders' gloves "real objects" whose state is tracked.
|
||||||
|
|
||||||
|
---@param announcer Announcer
|
||||||
|
---@param onThirdOutCallback fun()
|
||||||
|
---@return Baserunning
|
||||||
|
function Baserunning.new(announcer, onThirdOutCallback)
|
||||||
|
local o = setmetatable({
|
||||||
|
runners = {},
|
||||||
|
outRunners = {},
|
||||||
|
scoredRunners = {},
|
||||||
|
batter = nil,
|
||||||
|
--- Since this object is what ultimately *mutates* the out count,
|
||||||
|
--- it seems sensible to store the value here.
|
||||||
|
outs = 0,
|
||||||
|
announcer = announcer,
|
||||||
|
onThirdOut = onThirdOutCallback,
|
||||||
|
}, { __index = Baserunning })
|
||||||
|
|
||||||
|
o:pushNewBatter()
|
||||||
|
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param runner number | Runner
|
||||||
|
---@param message string | nil
|
||||||
|
---@return boolean wasThirdOut
|
||||||
|
function Baserunning:outRunner(runner, message)
|
||||||
|
self.outs = self.outs + 1
|
||||||
|
if type(runner) ~= "number" then
|
||||||
|
for i, maybe in ipairs(self.runners) do
|
||||||
|
if runner == maybe then
|
||||||
|
runner = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local runnerType = type(runner)
|
||||||
|
assert(runnerType == "number", "Expected runner to have type 'number', but was: " .. runnerType)
|
||||||
|
self.outRunners[#self.outRunners + 1] = self.runners[runner]
|
||||||
|
table.remove(self.runners, runner)
|
||||||
|
|
||||||
|
self:updateForcedRunners()
|
||||||
|
|
||||||
|
self.announcer:say(message or "YOU'RE OUT!")
|
||||||
|
if self.outs < 3 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
self.onThirdOut()
|
||||||
|
self.outs = 0
|
||||||
|
|
||||||
|
while #self.runners > 0 do
|
||||||
|
self.outRunners[#self.outRunners + 1] = table.remove(self.runners, #self.runners)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param fielder Fielder
|
||||||
|
---@return boolean outedSomeRunner
|
||||||
|
function Baserunning:outEligibleRunners(fielder)
|
||||||
|
local touchedBase = utils.isTouchingBase(fielder.x, fielder.y)
|
||||||
|
local didOutRunner = false
|
||||||
|
|
||||||
|
local runnerBaseBiMap = {}
|
||||||
|
for _, runner in pairs(self.runners) do
|
||||||
|
local theTouchedBase = utils.isTouchingBase(runner.x, runner.y)
|
||||||
|
if theTouchedBase ~= nil and runnerBaseBiMap[theTouchedBase] == nil then
|
||||||
|
runnerBaseBiMap[runner] = theTouchedBase
|
||||||
|
runnerBaseBiMap[theTouchedBase] = runner
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, runner in pairs(self.runners) do
|
||||||
|
local runnerOnBase = runnerBaseBiMap[runner]
|
||||||
|
if -- Force out
|
||||||
|
touchedBase
|
||||||
|
and runner.prevBase -- Make sure the runner is not standing at home
|
||||||
|
and runner.forcedTo == touchedBase
|
||||||
|
and touchedBase ~= runnerOnBase
|
||||||
|
-- Tag out
|
||||||
|
or not runnerOnBase and utils.distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < C.TagDistance
|
||||||
|
then
|
||||||
|
local wasThirdOut = self:outRunner(i)
|
||||||
|
if wasThirdOut then
|
||||||
|
return true -- Don't keep running up self.outs after it's been reset
|
||||||
|
end
|
||||||
|
didOutRunner = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return didOutRunner
|
||||||
|
end
|
||||||
|
|
||||||
|
function Baserunning:updateForcedRunners()
|
||||||
|
local stillForced = true
|
||||||
|
for _, base in ipairs(C.Bases) do
|
||||||
|
local runnerTargetingBase = utils.getRunnerWithNextBase(self.runners, base)
|
||||||
|
if runnerTargetingBase then
|
||||||
|
if stillForced then
|
||||||
|
runnerTargetingBase.forcedTo = base
|
||||||
|
else
|
||||||
|
runnerTargetingBase.forcedTo = nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
stillForced = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Baserunning:convertBatterToRunner()
|
||||||
|
self.batter.nextBase = C.Bases[C.First]
|
||||||
|
self.batter.prevBase = C.Bases[C.Home]
|
||||||
|
self:updateForcedRunners()
|
||||||
|
self.batter.forcedTo = C.Bases[C.First]
|
||||||
|
self.batter = nil -- Demote batter to a mere runner
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param deltaSeconds number
|
||||||
|
---@param runner Runner
|
||||||
|
---@return boolean isStillWalking
|
||||||
|
local function walkWayOutRunner(deltaSeconds, runner)
|
||||||
|
if runner.x < C.Screen.W + 50 and runner.y < C.Screen.H + 50 then
|
||||||
|
runner.x = runner.x + (deltaSeconds * 25)
|
||||||
|
runner.y = runner.y + (deltaSeconds * 25)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param deltaSeconds number
|
||||||
|
function Baserunning:walkAwayOutRunners(deltaSeconds)
|
||||||
|
for i, runner in ipairs(self.outRunners) do
|
||||||
|
if not walkWayOutRunner(deltaSeconds, runner) then
|
||||||
|
table.remove(self.outRunners, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for i, runner in ipairs(self.scoredRunners) do
|
||||||
|
if not walkWayOutRunner(deltaSeconds, runner) then
|
||||||
|
table.remove(self.scoredRunners, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return Runner theBatterPushed
|
||||||
|
function Baserunning:pushNewBatter()
|
||||||
|
local new = {
|
||||||
|
-- imageSet = math.random() < C.WokeMeter and FemmeSet or MascSet, -- TODO? lol.
|
||||||
|
x = C.RightHandedBattersBox.x - 60,
|
||||||
|
y = C.RightHandedBattersBox.y + 60,
|
||||||
|
nextBase = C.RightHandedBattersBox,
|
||||||
|
prevBase = nil,
|
||||||
|
forcedTo = C.Bases[C.First],
|
||||||
|
spriteIndex = math.random(#HomeTeamSpriteGroup),
|
||||||
|
}
|
||||||
|
self.runners[#self.runners + 1] = new
|
||||||
|
self.batter = new
|
||||||
|
return new
|
||||||
|
end
|
||||||
|
|
||||||
|
function Baserunning:getNewestRunner()
|
||||||
|
return self.runners[#self.runners]
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param runnerIndex number
|
||||||
|
function Baserunning:runnerScored(runnerIndex)
|
||||||
|
self.scoredRunners[#self.scoredRunners + 1] = self.runners[runnerIndex]
|
||||||
|
table.remove(self.runners, runnerIndex)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns true only if the given runner moved during this update.
|
||||||
|
---@param runner Runner | nil
|
||||||
|
---@param runnerIndex number | nil May only be nil if runner == batter
|
||||||
|
---@param appliedSpeed number
|
||||||
|
---@param isAutoRun boolean
|
||||||
|
---@param deltaSeconds number
|
||||||
|
---@return boolean runnerMoved, boolean runnerScored
|
||||||
|
function Baserunning:updateRunner(runner, runnerIndex, appliedSpeed, isAutoRun, deltaSeconds)
|
||||||
|
local autoRunSpeed = 20 * deltaSeconds
|
||||||
|
|
||||||
|
if not runner or not runner.nextBase then
|
||||||
|
return false, false
|
||||||
|
end
|
||||||
|
|
||||||
|
local nearestBase, nearestBaseDistance = utils.getNearestOf(C.Bases, runner.x, runner.y)
|
||||||
|
|
||||||
|
if
|
||||||
|
nearestBaseDistance < 5
|
||||||
|
and runnerIndex ~= nil
|
||||||
|
and runner ~= self.batter
|
||||||
|
and runner.nextBase == C.Bases[C.Home]
|
||||||
|
and nearestBase == C.Bases[C.Home]
|
||||||
|
then
|
||||||
|
self:runnerScored(runnerIndex)
|
||||||
|
return true, true
|
||||||
|
end
|
||||||
|
|
||||||
|
local nb = runner.nextBase
|
||||||
|
local x, y, distance = utils.normalizeVector(runner.x, runner.y, nb.x, nb.y)
|
||||||
|
|
||||||
|
-- TODO: Do a better job drifting runners toward their bases when appliedSpeed is low/zero
|
||||||
|
if distance < 2 then
|
||||||
|
if runner.prevBase ~= nearestBase then
|
||||||
|
runner.prevBase = runner.nextBase
|
||||||
|
runner.nextBase = C.NextBaseMap[runner.nextBase]
|
||||||
|
end
|
||||||
|
runner.forcedTo = nil
|
||||||
|
return false, false
|
||||||
|
end
|
||||||
|
|
||||||
|
local prevX, prevY = runner.x, runner.y
|
||||||
|
local mult = 1
|
||||||
|
if appliedSpeed < 0 then
|
||||||
|
if runner.prevBase then
|
||||||
|
mult = -1
|
||||||
|
else
|
||||||
|
-- Don't allow running backwards when approaching the plate
|
||||||
|
appliedSpeed = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: Make this less "sticky" for the user.
|
||||||
|
-- Currently it can be a little hard to run *past* a base.
|
||||||
|
|
||||||
|
local autoRun = 0
|
||||||
|
if not isAutoRun then
|
||||||
|
autoRun = (nearestBaseDistance > 40 or runner.forcedTo) and mult * autoRunSpeed
|
||||||
|
or nearestBaseDistance < 5 and 0
|
||||||
|
or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed)
|
||||||
|
end
|
||||||
|
|
||||||
|
mult = autoRun + (appliedSpeed / 20)
|
||||||
|
runner.x = runner.x - (x * mult)
|
||||||
|
runner.y = runner.y - (y * mult)
|
||||||
|
|
||||||
|
return prevX ~= runner.x or prevY ~= runner.y, false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Update non-batter runners.
|
||||||
|
--- Returns true only if at least one of the given runners moved during this update
|
||||||
|
---@param appliedSpeed number | fun(runner: Runner): number
|
||||||
|
---@param forcedOnly boolean If true, only move forced runners (e.g. for a walk)
|
||||||
|
---@param isAutoRun boolean If true, does not attempt to hug the bases
|
||||||
|
---@param deltaSeconds number
|
||||||
|
---@return boolean runnersStillMoving, number runnersScored, number secondsSinceLastMove
|
||||||
|
function Baserunning:updateNonBatterRunners(appliedSpeed, forcedOnly, isAutoRun, deltaSeconds)
|
||||||
|
local runnersStillMoving = false
|
||||||
|
local runnersScored = 0
|
||||||
|
|
||||||
|
local speedIsFunction = type(appliedSpeed) == "function"
|
||||||
|
-- TODO: Filter for the runner closest to the currently-held direction button
|
||||||
|
for runnerIndex, runner in ipairs(self.runners) do
|
||||||
|
if runner ~= self.batter and (not forcedOnly or runner.forcedTo) then
|
||||||
|
local speed = appliedSpeed
|
||||||
|
if speedIsFunction then
|
||||||
|
speed = appliedSpeed(runner)
|
||||||
|
end
|
||||||
|
local thisRunnerMoved, thisRunnerScored =
|
||||||
|
self:updateRunner(runner, runnerIndex, speed, isAutoRun, deltaSeconds)
|
||||||
|
runnersStillMoving = runnersStillMoving or thisRunnerMoved
|
||||||
|
if thisRunnerScored then
|
||||||
|
runnersScored = runnersScored + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if runnersStillMoving then
|
||||||
|
self.secondsSinceLastRunnerMove = 0
|
||||||
|
self:updateForcedRunners()
|
||||||
|
else
|
||||||
|
self.secondsSinceLastRunnerMove = (self.secondsSinceLastRunnerMove or 0) + deltaSeconds
|
||||||
|
end
|
||||||
|
|
||||||
|
return runnersStillMoving, runnersScored, self.secondsSinceLastRunnerMove
|
||||||
|
end
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
if not playdate or playdate.TEST_MODE then
|
||||||
|
return Baserunning
|
||||||
|
end
|
|
@ -0,0 +1,68 @@
|
||||||
|
---@class BatRenderState
|
||||||
|
---@field batBase XyPair
|
||||||
|
---@field batTip XyPair
|
||||||
|
---@field batAngleDeg number
|
||||||
|
---@field batSpeed number
|
||||||
|
|
||||||
|
---@class Batting
|
||||||
|
---@field private Baserunning
|
||||||
|
---@field state BatRenderState Is updated by checkForHit()
|
||||||
|
Batting = {}
|
||||||
|
|
||||||
|
local SwingBackDeg <const> = 30
|
||||||
|
local SwingForwardDeg <const> = 170
|
||||||
|
local OffscreenPos <const> = utils.xy(-999, -999)
|
||||||
|
|
||||||
|
---@param baserunning Baserunning
|
||||||
|
function Batting.new(baserunning)
|
||||||
|
return setmetatable({
|
||||||
|
baserunning = baserunning,
|
||||||
|
state = {
|
||||||
|
batAngleDeg = 0,
|
||||||
|
batSpeed = 0,
|
||||||
|
batTip = OffscreenPos,
|
||||||
|
batBase = OffscreenPos,
|
||||||
|
},
|
||||||
|
}, { __index = Batting })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO? Make the bat angle work more like the throw meter.
|
||||||
|
-- Would instead constantly drift toward a default value, giving us a little more control,
|
||||||
|
-- and letting the user find a crank position and direction that works for them
|
||||||
|
|
||||||
|
--- Assumes the bat is being held by self.baserunning.batter
|
||||||
|
--- Mutates self.state for later rendering.
|
||||||
|
---@param batDeg number
|
||||||
|
---@param batSpeed number
|
||||||
|
---@param ball Point3d
|
||||||
|
---@return XyPair | nil, boolean, number | nil Ball destination or nil if no hit, true only if batter swung, power mult
|
||||||
|
function Batting:checkForHit(batDeg, batSpeed, ball)
|
||||||
|
local batter = self.baserunning.batter
|
||||||
|
local isSwinging = batDeg > SwingBackDeg and batDeg < SwingForwardDeg
|
||||||
|
local batRadians = math.rad(batDeg)
|
||||||
|
|
||||||
|
local base = batter and utils.xy(batter.x + C.BatterHandPos.x, batter.y + C.BatterHandPos.y) or OffscreenPos
|
||||||
|
local tip = utils.xy(base.x + (C.BatLength * math.sin(batRadians)), base.y + (C.BatLength * math.cos(batRadians)))
|
||||||
|
|
||||||
|
self.state.batSpeed = batSpeed
|
||||||
|
self.state.batAngleDeg = batDeg
|
||||||
|
self.state.batTip = tip
|
||||||
|
self.state.batBase = base
|
||||||
|
|
||||||
|
local ballWasHit = batSpeed > 0 and ball.y < 232 and utils.pointOnOrUnderLine(ball, base, tip, C.Screen.H)
|
||||||
|
|
||||||
|
if not ballWasHit then
|
||||||
|
return nil, isSwinging
|
||||||
|
end
|
||||||
|
|
||||||
|
local ballAngle = batRadians + math.rad(90)
|
||||||
|
local mult = math.abs(batSpeed / 15)
|
||||||
|
local ballVelX = mult * C.BattingPower * 10 * math.sin(ballAngle)
|
||||||
|
local ballVelY = mult * C.BattingPower * 5 * math.cos(ballAngle)
|
||||||
|
if ballVelY > 0 then
|
||||||
|
ballVelX = ballVelX * -1
|
||||||
|
ballVelY = ballVelY * -1
|
||||||
|
end
|
||||||
|
|
||||||
|
return utils.xy(ball.x + ballVelX, ball.y + ballVelY), isSwinging, mult
|
||||||
|
end
|
|
@ -0,0 +1,147 @@
|
||||||
|
C = {}
|
||||||
|
|
||||||
|
C.Screen = {
|
||||||
|
W = playdate and playdate.display.getWidth() or 400,
|
||||||
|
H = playdate and playdate.display.getHeight() or 240,
|
||||||
|
}
|
||||||
|
|
||||||
|
C.Center = utils.xy(C.Screen.W / 2, C.Screen.H / 2)
|
||||||
|
|
||||||
|
C.StrikeZoneStartX = C.Center.x - 16
|
||||||
|
C.StrikeZoneEndX = C.StrikeZoneStartX + 24
|
||||||
|
C.StrikeZoneStartY = C.Screen.H - 35
|
||||||
|
|
||||||
|
--- @alias Base {
|
||||||
|
--- x: number,
|
||||||
|
--- y: number,
|
||||||
|
--- }
|
||||||
|
|
||||||
|
---@type Base[]
|
||||||
|
C.Bases = {
|
||||||
|
utils.xy(C.Screen.W * 0.93, C.Screen.H * 0.52),
|
||||||
|
utils.xy(C.Screen.W * 0.47, C.Screen.H * 0.19),
|
||||||
|
utils.xy(C.Screen.W * 0.03, C.Screen.H * 0.52),
|
||||||
|
utils.xy(C.Screen.W * 0.474, C.Screen.H * 0.79),
|
||||||
|
}
|
||||||
|
|
||||||
|
C.First, C.Second, C.Third, C.Home = 1, 2, 3, 4
|
||||||
|
C.FieldHeight = C.Bases[C.Home].y - C.Bases[C.Second].y
|
||||||
|
|
||||||
|
-- Pseudo-base for batter to target
|
||||||
|
C.RightHandedBattersBox = {
|
||||||
|
x = C.Bases[C.Home].x - 30,
|
||||||
|
y = C.Bases[C.Home].y + 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@type table<Base, Base | nil>
|
||||||
|
C.NextBaseMap = {
|
||||||
|
[C.RightHandedBattersBox] = nil, -- Runner should not escape the box before a hit!
|
||||||
|
[C.Bases[C.First]] = C.Bases[C.Second],
|
||||||
|
[C.Bases[C.Second]] = C.Bases[C.Third],
|
||||||
|
[C.Bases[C.Third]] = C.Bases[C.Home],
|
||||||
|
}
|
||||||
|
|
||||||
|
C.LeftFoulLine = {
|
||||||
|
x1 = C.Center.x,
|
||||||
|
y1 = 220,
|
||||||
|
x2 = -1800,
|
||||||
|
y2 = -465,
|
||||||
|
}
|
||||||
|
|
||||||
|
C.RightFoulLine = {
|
||||||
|
x1 = C.Center.x,
|
||||||
|
y1 = 218,
|
||||||
|
x2 = 2120,
|
||||||
|
y2 = -465,
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Angle to align the bat to
|
||||||
|
C.CrankOffsetDeg = 90
|
||||||
|
|
||||||
|
C.DanceBounceMs = 500
|
||||||
|
C.DanceBounceCount = 4
|
||||||
|
|
||||||
|
C.ScoreboardDelayMs = 2000
|
||||||
|
|
||||||
|
--- Used to draw the ball well out of bounds, and
|
||||||
|
--- generally as a check for whether or not it's in play.
|
||||||
|
C.BallOffscreen = 999
|
||||||
|
|
||||||
|
C.PitchAfterSeconds = 6
|
||||||
|
C.ReturnToPitcherAfterSeconds = 2.4
|
||||||
|
C.PitchFlyMs = 1050
|
||||||
|
C.PitchStart = utils.xy(195, 105)
|
||||||
|
C.PitchEndY = 240
|
||||||
|
|
||||||
|
C.DefaultLaunchPower = 4
|
||||||
|
|
||||||
|
--- The max distance at which a fielder can tag out a runner.
|
||||||
|
C.TagDistance = 15
|
||||||
|
|
||||||
|
C.BallCatchHitbox = 3
|
||||||
|
|
||||||
|
--- The max distance at which a runner can be considered on base.
|
||||||
|
C.BaseHitbox = 10
|
||||||
|
|
||||||
|
C.BattingPower = 25
|
||||||
|
C.BatterHandPos = utils.xy(25, 15)
|
||||||
|
C.GloveZ = 0 -- 10
|
||||||
|
|
||||||
|
C.SmallestBallRadius = 6
|
||||||
|
|
||||||
|
C.BatLength = 35
|
||||||
|
|
||||||
|
---@alias OffenseState "batting" | "running" | "walking" | "homeRun"
|
||||||
|
--- An enum for what state the offense is in
|
||||||
|
---@type table<string, OffenseState>
|
||||||
|
C.Offense = {
|
||||||
|
batting = "batting",
|
||||||
|
running = "running",
|
||||||
|
walking = "walking",
|
||||||
|
homeRun = "homeRun",
|
||||||
|
fliedOut = "running",
|
||||||
|
}
|
||||||
|
|
||||||
|
---@alias Side "offense" | "defense"
|
||||||
|
|
||||||
|
C.PitcherStartPos = {
|
||||||
|
x = C.Screen.W * 0.48,
|
||||||
|
y = C.Screen.H * 0.40,
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Controls how hard the ball can be hit, and
|
||||||
|
--- how fast the ball can be thrown.
|
||||||
|
C.CrankPower = 10
|
||||||
|
|
||||||
|
C.FielderRunMult = 1.3
|
||||||
|
|
||||||
|
C.PlayerHeightOffset = 20
|
||||||
|
|
||||||
|
C.UserThrowPower = 0.3
|
||||||
|
|
||||||
|
--- How fast baserunners move after a walk
|
||||||
|
C.WalkedRunnerSpeed = 10
|
||||||
|
|
||||||
|
C.ResetFieldersAfterSeconds = 2.5
|
||||||
|
|
||||||
|
C.OutfieldWall = {
|
||||||
|
{ x = -400, y = -103 },
|
||||||
|
{ x = -167, y = -208 },
|
||||||
|
{ x = 50, y = -211 },
|
||||||
|
{ x = 150, y = -181 },
|
||||||
|
{ x = 339, y = -176 },
|
||||||
|
{ x = 450, y = -221 },
|
||||||
|
{ x = 700, y = -209 },
|
||||||
|
{ x = 785, y = -59 },
|
||||||
|
{ x = 801, y = -16 },
|
||||||
|
}
|
||||||
|
|
||||||
|
C.BottomOfOutfieldWall = {}
|
||||||
|
|
||||||
|
for i, v in ipairs(C.OutfieldWall) do
|
||||||
|
C.BottomOfOutfieldWall[i] = utils.xy(v.x, v.y + 40)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not playdate then
|
||||||
|
return C
|
||||||
|
end
|
|
@ -0,0 +1,100 @@
|
||||||
|
local gfx = playdate.graphics
|
||||||
|
|
||||||
|
local HeaderFont <const> = Roobert11Medium
|
||||||
|
local DetailFont <const> = FontFullCircle
|
||||||
|
|
||||||
|
---@alias TextObject { text: string, font: pd_font }
|
||||||
|
|
||||||
|
---@param texts TextObject[]
|
||||||
|
local function drawTexts(texts)
|
||||||
|
local xOffset = 10
|
||||||
|
local initialOffset <const> = -(HeaderFont:getHeight()) / 2
|
||||||
|
local yOffset = initialOffset
|
||||||
|
|
||||||
|
--- The text height plus a margin scaled to that height
|
||||||
|
function getOffsetOffset(textObject)
|
||||||
|
return (-4 + math.floor(textObject.font:getHeight() * 1.6)) / 2
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Inverted buffer around text to separate it from the background
|
||||||
|
for _, textObject in ipairs(texts) do
|
||||||
|
local offsetOffset = getOffsetOffset(textObject)
|
||||||
|
yOffset = yOffset + offsetOffset
|
||||||
|
gfx.setImageDrawMode(gfx.kDrawModeInverted)
|
||||||
|
for x = xOffset - 6, xOffset + 6 do
|
||||||
|
for y = yOffset - 6, yOffset + 6 do
|
||||||
|
textObject.font:drawText(textObject.text, x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
yOffset = yOffset + offsetOffset
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Drawing the actual text afterward (instead of inline) keeps the inverted buffer from drawing over it.
|
||||||
|
yOffset = initialOffset
|
||||||
|
gfx.setImageDrawMode(gfx.kDrawModeCopy)
|
||||||
|
for _, textObject in ipairs(texts) do
|
||||||
|
local offsetOffset = getOffsetOffset(textObject)
|
||||||
|
yOffset = yOffset + offsetOffset
|
||||||
|
textObject.font:drawText(textObject.text, xOffset, yOffset)
|
||||||
|
yOffset = yOffset + offsetOffset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param text string
|
||||||
|
---@return TextObject
|
||||||
|
local function header(text)
|
||||||
|
return { text = text, font = HeaderFont }
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param text string
|
||||||
|
---@return TextObject
|
||||||
|
local function detail(text)
|
||||||
|
return { text = text, font = DetailFont }
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class ControlScreen
|
||||||
|
---@field sceneToReturnTo Scene
|
||||||
|
---@field private renderedImage pd_image Static image doesn't need to be constantly re-rendered.
|
||||||
|
ControlScreen = {}
|
||||||
|
|
||||||
|
---@return pd_image
|
||||||
|
local function draw()
|
||||||
|
local image = gfx.image.new(C.Screen.W, C.Screen.H)
|
||||||
|
gfx.pushContext(image)
|
||||||
|
BallBackground:draw(0, 0)
|
||||||
|
drawTexts({
|
||||||
|
header("Batting:"),
|
||||||
|
detail("Swing the crank to swing your bat"),
|
||||||
|
detail("But watch out! Some pitches are tricky!"),
|
||||||
|
|
||||||
|
header("Pitching:"),
|
||||||
|
detail("Swing the crank to pitch the ball"),
|
||||||
|
detail("But be careful! Throw too hard and it might go wild!"),
|
||||||
|
detail("(shh: try holding A or B while you pitch)"),
|
||||||
|
|
||||||
|
header("Fielding:"),
|
||||||
|
detail("To throw, hold a direction button and crank!"),
|
||||||
|
detail("Right throws to 1st, Up goes to 2nd, etc."),
|
||||||
|
})
|
||||||
|
gfx.popContext()
|
||||||
|
return image
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param sceneToReturnTo Scene
|
||||||
|
---@return ControlScreen
|
||||||
|
function ControlScreen.new(sceneToReturnTo)
|
||||||
|
return setmetatable({
|
||||||
|
sceneToReturnTo = sceneToReturnTo,
|
||||||
|
renderedImage = draw(),
|
||||||
|
}, { __index = ControlScreen })
|
||||||
|
end
|
||||||
|
|
||||||
|
function ControlScreen:update()
|
||||||
|
gfx.animation.blinker.updateAll()
|
||||||
|
gfx.clear()
|
||||||
|
self.renderedImage:draw(0, 0)
|
||||||
|
drawButton("B", 370, 210)
|
||||||
|
if playdate.buttonJustPressed(playdate.kButtonA) or playdate.buttonJustPressed(playdate.kButtonB) then
|
||||||
|
transitionBetween(self, self.sceneToReturnTo)
|
||||||
|
end
|
||||||
|
end
|
104
src/dbg.lua
|
@ -1,7 +1,5 @@
|
||||||
-- selene: allow(unscoped_variables)
|
|
||||||
dbg = {}
|
dbg = {}
|
||||||
|
|
||||||
-- selene: allow(unused_variable)
|
|
||||||
function dbg.label(value, name)
|
function dbg.label(value, name)
|
||||||
if type(value) == "table" then
|
if type(value) == "table" then
|
||||||
print(name .. ":")
|
print(name .. ":")
|
||||||
|
@ -18,23 +16,95 @@ function dbg.label(value, name)
|
||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Only works if called with the bases empty (i.e. the only runner should be the batter.
|
--- Only works if called with the bases empty (i.e. the only runner should be the batter.
|
||||||
-- selene: allow(unused_variable)
|
---@param br Baserunning
|
||||||
function dbg.loadTheBases(baseConstants, runners)
|
function dbg.loadTheBases(br)
|
||||||
newRunner()
|
br:pushNewBatter()
|
||||||
newRunner()
|
br:pushNewBatter()
|
||||||
newRunner()
|
br:pushNewBatter()
|
||||||
runners[2].x = baseConstants[1].x
|
|
||||||
runners[2].y = baseConstants[1].y
|
|
||||||
runners[2].nextBase = baseConstants[2]
|
|
||||||
|
|
||||||
runners[3].x = baseConstants[2].x
|
br.runners[2].x = C.Bases[C.First].x
|
||||||
runners[3].y = baseConstants[2].y
|
br.runners[2].y = C.Bases[C.First].y
|
||||||
runners[3].nextBase = baseConstants[3]
|
br.runners[2].nextBase = C.Bases[C.Second]
|
||||||
|
|
||||||
runners[4].x = baseConstants[3].x
|
br.runners[3].x = C.Bases[C.Second].x
|
||||||
runners[4].y = baseConstants[3].y
|
br.runners[3].y = C.Bases[C.Second].y
|
||||||
runners[4].nextBase = baseConstants[4]
|
br.runners[3].nextBase = C.Bases[C.Third]
|
||||||
|
|
||||||
|
br.runners[4].x = C.Bases[C.Third].x
|
||||||
|
br.runners[4].y = C.Bases[C.Third].y
|
||||||
|
br.runners[4].nextBase = C.Bases[C.Home]
|
||||||
|
end
|
||||||
|
|
||||||
|
local hitSamples = {
|
||||||
|
away = {
|
||||||
|
{
|
||||||
|
utils.xy(7.88733, -16.3434),
|
||||||
|
utils.xy(378.3376, 30.49521),
|
||||||
|
utils.xy(367.1036, 21.55336),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
utils.xy(379.8051, -40.82794),
|
||||||
|
utils.xy(-444.5791, -30.30901),
|
||||||
|
utils.xy(-30.43079, -30.50307),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
utils.xy(227.8881, -14.56854),
|
||||||
|
utils.xy(293.5208, 39.38919),
|
||||||
|
utils.xy(154.4738, -26.55899),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
home = {
|
||||||
|
{
|
||||||
|
utils.xy(146.2505, -89.12155),
|
||||||
|
utils.xy(429.5428, 59.62944),
|
||||||
|
utils.xy(272.4666, -78.578),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
utils.xy(485.0516, 112.8341),
|
||||||
|
utils.xy(290.9232, -4.946442),
|
||||||
|
utils.xy(263.4262, -6.482407),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
utils.xy(260.6927, -63.63049),
|
||||||
|
utils.xy(392.1548, -44.22421),
|
||||||
|
utils.xy(482.5545, 105.3476),
|
||||||
|
utils.xy(125.5928, 18.53091),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
---@param inningCount number Number of innings to mock
|
||||||
|
---@return Statistics
|
||||||
|
function dbg.mockStatistics(inningCount)
|
||||||
|
inningCount = inningCount or 9
|
||||||
|
local stats = Statistics.new()
|
||||||
|
for i = 1, inningCount - 1 do
|
||||||
|
stats.innings[i].home.score = math.floor(math.random() * 5)
|
||||||
|
stats.innings[i].away.score = math.floor(math.random() * 5)
|
||||||
|
if hitSamples.home[i] ~= nil then
|
||||||
|
stats.innings[i].home.hits = hitSamples.home[i]
|
||||||
|
end
|
||||||
|
if hitSamples.away[i] ~= nil then
|
||||||
|
stats.innings[i].away.hits = hitSamples.away[i]
|
||||||
|
end
|
||||||
|
stats:pushInning()
|
||||||
|
end
|
||||||
|
|
||||||
|
local homeScore, awayScore = utils.totalScores(stats)
|
||||||
|
if homeScore == awayScore then
|
||||||
|
stats.innings[#stats.innings].home.score = 1 + stats.innings[#stats.innings].home.score
|
||||||
|
end
|
||||||
|
return stats
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param points XyPair[]
|
||||||
|
function dbg.drawLine(points)
|
||||||
|
for i = 2, #points do
|
||||||
|
local prev = points[i - 1]
|
||||||
|
local next = points[i]
|
||||||
|
playdate.graphics.drawLine(prev.x, prev.y, next.x, next.y)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if not playdate then
|
if not playdate then
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
local gfx <const> = playdate.graphics
|
||||||
|
|
||||||
|
function Ball:draw()
|
||||||
|
gfx.setLineWidth(2)
|
||||||
|
|
||||||
|
gfx.setColor(gfx.kColorWhite)
|
||||||
|
gfx.fillCircleAtPoint(self.x, self.y, self.size)
|
||||||
|
|
||||||
|
gfx.setColor(gfx.kColorBlack)
|
||||||
|
gfx.drawCircleAtPoint(self.x, self.y, self.size)
|
||||||
|
end
|
|
@ -0,0 +1,203 @@
|
||||||
|
local MarginY <const> = 70
|
||||||
|
|
||||||
|
local SmallFont <const> = FontFullCircle
|
||||||
|
local ScoreFont <const> = AshevilleSans14Bold
|
||||||
|
local NumWidth <const> = ScoreFont:getTextWidth("0")
|
||||||
|
local NumHeight <const> = ScoreFont:getHeight()
|
||||||
|
local AwayWidth <const> = ScoreFont:getTextWidth("AWAY")
|
||||||
|
local InningMargin = 4
|
||||||
|
|
||||||
|
local InningDrawWidth <const> = (InningMargin * 2) + (NumWidth * 2)
|
||||||
|
local ScoreDrawHeight = NumHeight * 2
|
||||||
|
|
||||||
|
-- luacheck: ignore 143
|
||||||
|
---@type pd_graphics_lib
|
||||||
|
local gfx = playdate.graphics
|
||||||
|
|
||||||
|
local function formatScore(n)
|
||||||
|
if n <= 9 then
|
||||||
|
return " " .. n
|
||||||
|
elseif n <= 19 then
|
||||||
|
return " " .. n
|
||||||
|
else
|
||||||
|
return tostring(n)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local HomeY <const> = -4 + (NumHeight * 2) + MarginY
|
||||||
|
local AwayY <const> = -4 + (NumHeight * 3) + MarginY
|
||||||
|
|
||||||
|
local function drawInning(x, inningNumber, homeScore, awayScore)
|
||||||
|
gfx.setColor(gfx.kColorBlack)
|
||||||
|
gfx.setColor(gfx.kColorWhite)
|
||||||
|
gfx.setLineWidth(1)
|
||||||
|
gfx.drawRect(x, 34 + MarginY, InningDrawWidth, ScoreDrawHeight)
|
||||||
|
|
||||||
|
inningNumber = " " .. inningNumber
|
||||||
|
homeScore = formatScore(homeScore)
|
||||||
|
awayScore = formatScore(awayScore)
|
||||||
|
|
||||||
|
x = x - 8 + (InningDrawWidth / 2)
|
||||||
|
ScoreFont:drawTextAligned(inningNumber, x, -4 + NumHeight + MarginY, gfx.kAlignRight)
|
||||||
|
ScoreFont:drawTextAligned(awayScore, x, HomeY, gfx.kAlignRight)
|
||||||
|
ScoreFont:drawTextAligned(homeScore, x, AwayY, gfx.kAlignRight)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class BoxScore
|
||||||
|
---@field stats Statistics
|
||||||
|
---@field private targetY number
|
||||||
|
BoxScore = {}
|
||||||
|
|
||||||
|
---@param stats Statistics
|
||||||
|
---@return BoxScore
|
||||||
|
function BoxScore.new(stats)
|
||||||
|
return setmetatable({
|
||||||
|
stats = stats,
|
||||||
|
targetY = 0,
|
||||||
|
}, { __index = BoxScore })
|
||||||
|
end
|
||||||
|
|
||||||
|
function BoxScore:drawBoxScore()
|
||||||
|
local inningStart = 4 + (AwayWidth * 1.5)
|
||||||
|
local widthAndMarg = InningDrawWidth + 4
|
||||||
|
ScoreFont:drawTextAligned(" HOME", 10, HomeY, gfx.kAlignRight)
|
||||||
|
ScoreFont:drawTextAligned("AWAY", 10, AwayY, gfx.kAlignRight)
|
||||||
|
for i = 1, #self.stats.innings do
|
||||||
|
local inningStats = self.stats.innings[i]
|
||||||
|
drawInning(inningStart + ((i - 1) * widthAndMarg), i, inningStats.home.score, inningStats.away.score)
|
||||||
|
end
|
||||||
|
local homeScore, awayScore = utils.totalScores(self.stats)
|
||||||
|
drawInning(4 + inningStart + (widthAndMarg * #self.stats.innings), "F", homeScore, awayScore)
|
||||||
|
ScoreFont:drawTextAligned("v", C.Center.x, C.Screen.H - 40, gfx.kAlignCenter)
|
||||||
|
end
|
||||||
|
|
||||||
|
local GraphM = 10
|
||||||
|
local GraphW = C.Screen.W - (GraphM * 2)
|
||||||
|
local GraphH = C.Screen.H - (GraphM * 2)
|
||||||
|
|
||||||
|
function BoxScore:drawScoreGraph(y)
|
||||||
|
-- TODO: Actually draw score legend
|
||||||
|
|
||||||
|
-- Offset by 2 to support a) the zero-index b) the score legend
|
||||||
|
local segmentWidth = GraphW / (#self.stats.innings + 2)
|
||||||
|
|
||||||
|
local legendX = segmentWidth * (#self.stats.innings + 2) - GraphM
|
||||||
|
gfx.drawLine(GraphM / 2, y + GraphM + GraphH, legendX, y + GraphM + GraphH)
|
||||||
|
gfx.drawLine(legendX, y + GraphM, legendX, y + GraphH + GraphM)
|
||||||
|
|
||||||
|
gfx.setLineWidth(3)
|
||||||
|
local homeScore, awayScore = utils.totalScores(self.stats)
|
||||||
|
local highestScore = math.max(homeScore, awayScore)
|
||||||
|
|
||||||
|
local heightPerPoint = (GraphH - 6) / highestScore
|
||||||
|
|
||||||
|
function point(inning, score)
|
||||||
|
return utils.xy(GraphM + (inning * segmentWidth), y + GraphM + GraphH + (score * -heightPerPoint))
|
||||||
|
end
|
||||||
|
|
||||||
|
function drawLine(teamId)
|
||||||
|
local linePoints = { point(0, 0) }
|
||||||
|
local scoreTotal = 0
|
||||||
|
for i, inning in ipairs(self.stats.innings) do
|
||||||
|
scoreTotal = scoreTotal + inning[teamId].score
|
||||||
|
linePoints[#linePoints + 1] = point(i, scoreTotal)
|
||||||
|
end
|
||||||
|
dbg.drawLine(linePoints)
|
||||||
|
local finalPoint = linePoints[#linePoints]
|
||||||
|
SmallFont:drawTextAligned(string.upper(teamId), finalPoint.x + 3, finalPoint.y - 7, gfx.kAlignRight)
|
||||||
|
end
|
||||||
|
|
||||||
|
drawLine("home")
|
||||||
|
gfx.setDitherPattern(0.5)
|
||||||
|
drawLine("away")
|
||||||
|
gfx.setDitherPattern(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param realHit XyPair
|
||||||
|
---@return XyPair
|
||||||
|
function convertHitToMini(realHit)
|
||||||
|
-- Convert to all-positive y
|
||||||
|
local y = realHit.y + C.Screen.H
|
||||||
|
y = y / 2
|
||||||
|
|
||||||
|
local x = realHit.x + C.Screen.W
|
||||||
|
x = x / 3
|
||||||
|
return utils.xy(x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function BoxScore:drawHitChart(y)
|
||||||
|
local leftMargin = 8
|
||||||
|
GrassBackgroundSmall:drawCentered(C.Center.x, y + C.Center.y + 54)
|
||||||
|
gfx.setLineWidth(1)
|
||||||
|
ScoreFont:drawTextAligned("AWAY", leftMargin, y + C.Screen.H - NumHeight, gfx.kAlignRight)
|
||||||
|
gfx.setColor(gfx.kColorBlack)
|
||||||
|
gfx.setDitherPattern(0.5, gfx.image.kDitherTypeBayer2x2)
|
||||||
|
gfx.fillRect(leftMargin, y + C.Screen.H - NumHeight, ScoreFont:getTextWidth("AWAY"), NumHeight)
|
||||||
|
|
||||||
|
gfx.setColor(gfx.kColorWhite)
|
||||||
|
gfx.setDitherPattern(0.5)
|
||||||
|
for _, inning in ipairs(self.stats.innings) do
|
||||||
|
for _, hit in ipairs(inning.away.hits) do
|
||||||
|
local miniHitPos = convertHitToMini(hit)
|
||||||
|
gfx.fillCircleAtPoint(miniHitPos.x + 10, miniHitPos.y + y, 4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
gfx.setColor(gfx.kColorWhite)
|
||||||
|
gfx.setDitherPattern(0)
|
||||||
|
ScoreFont:drawTextAligned(" HOME", leftMargin, y + C.Screen.H - (NumHeight * 2), gfx.kAlignRight)
|
||||||
|
for _, inning in ipairs(self.stats.innings) do
|
||||||
|
for _, hit in ipairs(inning.home.hits) do
|
||||||
|
local miniHitPos = convertHitToMini(hit)
|
||||||
|
gfx.fillCircleAtPoint(miniHitPos.x + 10, miniHitPos.y + y, 4)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local screens = {
|
||||||
|
BoxScore.drawBoxScore,
|
||||||
|
BoxScore.drawScoreGraph,
|
||||||
|
BoxScore.drawHitChart,
|
||||||
|
}
|
||||||
|
|
||||||
|
function BoxScore:render()
|
||||||
|
local originalDrawMode = gfx.getImageDrawMode()
|
||||||
|
gfx.clear(gfx.kColorBlack)
|
||||||
|
gfx.setImageDrawMode(gfx.kDrawModeInverted)
|
||||||
|
gfx.setColor(gfx.kColorBlack)
|
||||||
|
|
||||||
|
for i, screen in ipairs(screens) do
|
||||||
|
screen(self, (i - 1) * C.Screen.H)
|
||||||
|
end
|
||||||
|
|
||||||
|
gfx.setImageDrawMode(originalDrawMode)
|
||||||
|
end
|
||||||
|
|
||||||
|
local renderedImage
|
||||||
|
|
||||||
|
function BoxScore:update()
|
||||||
|
if not renderedImage then
|
||||||
|
renderedImage = gfx.image.new(C.Screen.W, C.Screen.H * #screens)
|
||||||
|
gfx.pushContext(renderedImage)
|
||||||
|
self:render()
|
||||||
|
gfx.popContext()
|
||||||
|
end
|
||||||
|
|
||||||
|
local deltaSeconds = playdate.getElapsedTime()
|
||||||
|
playdate.resetElapsedTime()
|
||||||
|
|
||||||
|
gfx.setDrawOffset(0, self.targetY)
|
||||||
|
renderedImage:draw(0, 0)
|
||||||
|
|
||||||
|
local crankChange = playdate.getCrankChange()
|
||||||
|
if crankChange ~= 0 then
|
||||||
|
self.targetY = self.targetY - (crankChange * 0.8)
|
||||||
|
else
|
||||||
|
local closestScreen = math.floor(0.5 + (self.targetY / C.Screen.H)) * C.Screen.H
|
||||||
|
if math.abs(self.targetY - closestScreen) > 3 then
|
||||||
|
local needsIncrease = self.targetY < closestScreen
|
||||||
|
local change = needsIncrease and 200 * deltaSeconds or -200 * deltaSeconds
|
||||||
|
self.targetY = self.targetY + change
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.targetY = math.max(math.min(self.targetY, 0), -C.Screen.H * (#screens - 1))
|
||||||
|
end
|
|
@ -0,0 +1,77 @@
|
||||||
|
-- selene: allow(shadowing)
|
||||||
|
local gfx = playdate.graphics
|
||||||
|
|
||||||
|
---@alias PlayerImageBundle { smiling: pd_image, lowHat: pd_image, frowning: pd_image, back: pd_image }
|
||||||
|
|
||||||
|
---@alias SpriteCollection PlayerImageBundle[]
|
||||||
|
|
||||||
|
---@param image pd_image
|
||||||
|
---@param drawInverted boolean
|
||||||
|
function maybeDrawInverted(image, x, y, drawInverted)
|
||||||
|
local drawMode = gfx.getImageDrawMode()
|
||||||
|
if drawInverted then
|
||||||
|
gfx.setImageDrawMode(gfx.kDrawModeInverted)
|
||||||
|
end
|
||||||
|
image:draw(x, y)
|
||||||
|
gfx.setImageDrawMode(drawMode)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- TODO: Custom names on jerseys?
|
||||||
|
---@return PlayerImageBundle
|
||||||
|
---@param base pd_image
|
||||||
|
---@param isInverted boolean
|
||||||
|
function buildPlayerBundle(base, back, smile, frown, logo, isInverted)
|
||||||
|
local smiling = gfx.image.new(base:getSize())
|
||||||
|
gfx.lockFocus(smiling)
|
||||||
|
base:draw(0, 0)
|
||||||
|
Hat:draw(6, 0)
|
||||||
|
smile:draw(5, 9)
|
||||||
|
maybeDrawInverted(logo, 3, 25, isInverted)
|
||||||
|
|
||||||
|
local lowHat = gfx.image.new(base:getSize())
|
||||||
|
gfx.lockFocus(lowHat)
|
||||||
|
base:draw(0, 0)
|
||||||
|
Hat:draw(6, 2)
|
||||||
|
smile:draw(5, 9)
|
||||||
|
maybeDrawInverted(logo, 3, 25, isInverted)
|
||||||
|
|
||||||
|
local frowning = gfx.image.new(base:getSize())
|
||||||
|
|
||||||
|
gfx.lockFocus(frowning)
|
||||||
|
base:draw(0, 0)
|
||||||
|
maybeDrawInverted(logo, 3, 25, isInverted)
|
||||||
|
Hat:draw(6, 0)
|
||||||
|
frown:draw(5, 9)
|
||||||
|
|
||||||
|
gfx.unlockFocus()
|
||||||
|
|
||||||
|
return {
|
||||||
|
smiling = smiling,
|
||||||
|
lowHat = lowHat,
|
||||||
|
frowning = frowning,
|
||||||
|
back = back,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type SpriteCollection
|
||||||
|
AwayTeamSpriteGroup = nil
|
||||||
|
|
||||||
|
---@type SpriteCollection
|
||||||
|
HomeTeamSpriteGroup = nil
|
||||||
|
|
||||||
|
function replaceAwayLogo(logo)
|
||||||
|
AwayTeamSpriteGroup = {
|
||||||
|
buildPlayerBundle(DarkPlayerAwayBase, DarkPlayerAwayBack, DarkPlayerSmile, DarkPlayerFrown, logo, true),
|
||||||
|
buildPlayerBundle(LightPlayerAwayBase, LightPlayerAwayBack, LightPlayerSmile, LightPlayerFrown, logo, true),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function replaceHomeLogo(logo)
|
||||||
|
HomeTeamSpriteGroup = {
|
||||||
|
buildPlayerBundle(DarkPlayerHomeBase, DarkPlayerHomeBack, DarkPlayerSmile, DarkPlayerFrown, logo, true),
|
||||||
|
buildPlayerBundle(LightPlayerHomeBase, LightPlayerHomeBack, LightPlayerSmile, LightPlayerFrown, logo, true),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
replaceAwayLogo(Logos[1].image)
|
||||||
|
replaceHomeLogo(Logos[2].image)
|
|
@ -0,0 +1,151 @@
|
||||||
|
---@class Characters
|
||||||
|
---@field homeSprites SpriteCollection
|
||||||
|
---@field awaySprites SpriteCollection
|
||||||
|
---@field homeBlipper table
|
||||||
|
---@field awayBlipper table
|
||||||
|
Characters = {}
|
||||||
|
|
||||||
|
local gfx <const> = playdate.graphics
|
||||||
|
|
||||||
|
local GloveSizeX, GloveSizeY <const> = Glove:getSize()
|
||||||
|
local GloveOffX, GloveOffY <const> = GloveSizeX / 2, GloveSizeY / 2
|
||||||
|
|
||||||
|
---@param homeSprites SpriteCollection
|
||||||
|
---@param awaySprites SpriteCollection
|
||||||
|
function Characters.new(homeSprites, awaySprites)
|
||||||
|
return setmetatable({
|
||||||
|
homeSprites = homeSprites,
|
||||||
|
awaySprites = awaySprites,
|
||||||
|
homeBlipper = blipper.new(100, homeSprites),
|
||||||
|
awayBlipper = blipper.new(100, awaySprites),
|
||||||
|
}, { __index = Characters })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ball Point3d
|
||||||
|
---@param fielderX number
|
||||||
|
---@param fielderY number
|
||||||
|
---@return boolean isHoldingBall
|
||||||
|
local function drawFielderGlove(ball, fielderX, fielderY, flip)
|
||||||
|
local distanceFromBall = utils.distanceBetweenZ(fielderX, fielderY, 0, ball.x, ball.y, ball.z)
|
||||||
|
local shoulderX, shoulderY = fielderX + 10, fielderY - 5
|
||||||
|
if distanceFromBall > 20 then
|
||||||
|
Glove:draw(shoulderX, shoulderY, flip)
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
GloveHoldingBall:draw(ball.x - GloveOffX, ball.y - GloveOffY, flip)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param fieldingTeamSprites SpriteCollection
|
||||||
|
---@param fielder Fielder
|
||||||
|
---@param ball Point3d
|
||||||
|
---@param flip boolean | nil
|
||||||
|
---@return boolean isHoldingBall
|
||||||
|
function drawFielder(fieldingTeamSprites, fielder, ball, flip)
|
||||||
|
local danceOffset = FielderDanceAnimator:currentValue()
|
||||||
|
|
||||||
|
local x = fielder.x
|
||||||
|
local y = fielder.y - danceOffset
|
||||||
|
fieldingTeamSprites[fielder.spriteIndex].smiling:draw(fielder.x, y - 20, flip)
|
||||||
|
return drawFielderGlove(ball, x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param batState BatRenderState
|
||||||
|
local function drawBat(batState)
|
||||||
|
gfx.setLineWidth(7)
|
||||||
|
gfx.drawLine(batState.batBase.x, batState.batBase.y, batState.batTip.x, batState.batTip.y)
|
||||||
|
|
||||||
|
gfx.setColor(gfx.kColorWhite)
|
||||||
|
gfx.setLineCapStyle(gfx.kLineCapStyleRound)
|
||||||
|
gfx.setLineWidth(3)
|
||||||
|
gfx.drawLine(batState.batBase.x, batState.batBase.y, batState.batTip.x, batState.batTip.y)
|
||||||
|
|
||||||
|
gfx.setColor(gfx.kColorBlack)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param battingTeamSprites SpriteCollection
|
||||||
|
---@param batter Runner
|
||||||
|
---@param batState BatRenderState
|
||||||
|
local function drawBatter(battingTeamSprites, batter, batState)
|
||||||
|
local spriteCollection = battingTeamSprites[batter.spriteIndex]
|
||||||
|
if batState.batAngleDeg > 50 and batState.batAngleDeg < 200 then
|
||||||
|
drawBat(batState)
|
||||||
|
spriteCollection.back:draw(batter.x, batter.y - C.PlayerHeightOffset)
|
||||||
|
else
|
||||||
|
spriteCollection.smiling:draw(batter.x, batter.y - C.PlayerHeightOffset)
|
||||||
|
drawBat(batState)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param battingTeam TeamId
|
||||||
|
---@return SpriteCollection battingTeam, SpriteCollection fieldingTeam, table runnerBlipper
|
||||||
|
function Characters:getSpriteCollections(battingTeam)
|
||||||
|
if battingTeam == "home" then
|
||||||
|
return self.homeSprites, self.awaySprites, self.homeBlipper
|
||||||
|
end
|
||||||
|
return self.awaySprites, self.homeSprites, self.awayBlipper
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param fielding Fielding
|
||||||
|
---@param baserunning Baserunning
|
||||||
|
---@param batState BatRenderState
|
||||||
|
---@param battingTeam TeamId
|
||||||
|
---@param ball Point3d
|
||||||
|
---@return Fielder | nil ballHeldBy
|
||||||
|
function Characters:drawAll(fielding, baserunning, batState, battingTeam, ball)
|
||||||
|
---@type { y: number, drawAction: fun() }[]
|
||||||
|
local characterDraws = {}
|
||||||
|
function addDraw(y, drawAction)
|
||||||
|
characterDraws[#characterDraws + 1] = { y = y, drawAction = drawAction }
|
||||||
|
end
|
||||||
|
|
||||||
|
local battingTeamSprites, fieldingTeamSprites, runnerBlipper = self:getSpriteCollections(battingTeam)
|
||||||
|
---@type Fielder | nil
|
||||||
|
local ballHeldBy
|
||||||
|
for _, fielder in pairs(fielding.fielders) do
|
||||||
|
addDraw(fielder.y, function()
|
||||||
|
local ballHeldByThisFielder = drawFielder(fieldingTeamSprites, fielder, ball)
|
||||||
|
if ballHeldByThisFielder then
|
||||||
|
ballHeldBy = fielder
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, runner in pairs(baserunning.runners) do
|
||||||
|
addDraw(runner.y, function()
|
||||||
|
local currentBatter = baserunning.batter
|
||||||
|
if runner == currentBatter then
|
||||||
|
drawBatter(battingTeamSprites, currentBatter, batState)
|
||||||
|
else
|
||||||
|
-- TODO? Change blip speed depending on runner speed?
|
||||||
|
runnerBlipper:draw(false, runner.x, runner.y - C.PlayerHeightOffset, runner)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, runner in pairs(baserunning.outRunners) do
|
||||||
|
addDraw(runner.y, function()
|
||||||
|
battingTeamSprites[runner.spriteIndex].frowning:draw(runner.x, runner.y - C.PlayerHeightOffset)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
for _, runner in pairs(baserunning.scoredRunners) do
|
||||||
|
addDraw(runner.y, function()
|
||||||
|
runnerBlipper:draw(false, runner.x, runner.y - C.PlayerHeightOffset, runner)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(characterDraws, function(a, b)
|
||||||
|
return a.y < b.y
|
||||||
|
end)
|
||||||
|
for _, character in pairs(characterDraws) do
|
||||||
|
character.drawAction()
|
||||||
|
end
|
||||||
|
|
||||||
|
return ballHeldBy
|
||||||
|
end
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
if not playdate or playdate.TEST_MODE then
|
||||||
|
return Characters
|
||||||
|
end
|
|
@ -0,0 +1,36 @@
|
||||||
|
local gfx <const> = playdate.graphics
|
||||||
|
|
||||||
|
fans = {}
|
||||||
|
|
||||||
|
local FanImages <const> = { DarkSkinFan, LightSkinFan }
|
||||||
|
local FanWidth <const>, FanHeight <const> = FanImages[1]:getSize()
|
||||||
|
local BgWidth <const>, BgHeight <const> = GrassBackground:getSize()
|
||||||
|
|
||||||
|
local AudienceImage1 <const> = gfx.image.new(BgWidth, BgHeight)
|
||||||
|
local AudienceImage2 <const> = gfx.image.new(BgWidth, BgHeight)
|
||||||
|
|
||||||
|
local height = 0
|
||||||
|
while height < BgHeight do
|
||||||
|
local width = 0
|
||||||
|
while width < BgWidth do
|
||||||
|
gfx.pushContext(AudienceImage1)
|
||||||
|
local image = FanImages[math.random(#FanImages)]
|
||||||
|
local jiggle = math.random(5)
|
||||||
|
image:draw(width + jiggle, height)
|
||||||
|
gfx.popContext()
|
||||||
|
|
||||||
|
gfx.pushContext(AudienceImage2)
|
||||||
|
image:draw(width + jiggle + math.random(0, 2), height)
|
||||||
|
gfx.popContext()
|
||||||
|
|
||||||
|
width = width + FanWidth
|
||||||
|
end
|
||||||
|
height = height + FanHeight - 10
|
||||||
|
end
|
||||||
|
local AudienceMovement = gfx.animation.blinker.new(200, 200, true)
|
||||||
|
AudienceMovement:start()
|
||||||
|
|
||||||
|
function fans.draw()
|
||||||
|
local currentImage = AudienceMovement.on and AudienceImage1 or AudienceImage2
|
||||||
|
currentImage:draw(-400, -720)
|
||||||
|
end
|
|
@ -0,0 +1,169 @@
|
||||||
|
-- selene: allow(shadowing)
|
||||||
|
local gfx = playdate.graphics
|
||||||
|
|
||||||
|
local ScoreFont <const> = FontFullCircle
|
||||||
|
|
||||||
|
local MinimapSizeX, MinimapSizeY <const> = Minimap:getSize()
|
||||||
|
local MinimapPosX, MinimapPosY = C.Screen.W - MinimapSizeX, C.Screen.H - MinimapSizeY
|
||||||
|
local MinimapBoundX, MinimapBoundY = (MinimapSizeX + MinimapPosX), (MinimapSizeY + MinimapPosY)
|
||||||
|
|
||||||
|
local MinimapMultX <const> = 0.75 * MinimapSizeX / C.Screen.W
|
||||||
|
local MinimapOffsetX <const> = MinimapPosX + 5
|
||||||
|
local MinimapMultY <const> = 0.70 * MinimapSizeY / C.FieldHeight
|
||||||
|
local MinimapOffsetY <const> = MinimapPosY - 15
|
||||||
|
|
||||||
|
local RunnerSquareWidth = 8
|
||||||
|
local FielderCircleRadius = 4
|
||||||
|
local FielderCircleStrokeWidth = 2
|
||||||
|
|
||||||
|
function drawMinimap(runners, fielders)
|
||||||
|
Minimap:draw(MinimapPosX, MinimapPosY)
|
||||||
|
gfx.setColor(gfx.kColorBlack)
|
||||||
|
for _, runner in pairs(runners) do
|
||||||
|
local x = (MinimapMultX * runner.x) + MinimapOffsetX
|
||||||
|
local y = (MinimapMultY * runner.y) + MinimapOffsetY
|
||||||
|
gfx.fillRect(x, y, RunnerSquareWidth, RunnerSquareWidth)
|
||||||
|
end
|
||||||
|
gfx.setLineWidth(FielderCircleStrokeWidth)
|
||||||
|
for _, fielder in pairs(fielders) do
|
||||||
|
local x = (MinimapMultX * fielder.x) + MinimapOffsetX
|
||||||
|
local y = (MinimapMultY * fielder.y) + MinimapOffsetY
|
||||||
|
if x > MinimapPosX and x < MinimapBoundX and y > MinimapPosY and y < MinimapBoundY then
|
||||||
|
gfx.drawCircleAtPoint(x, y, FielderCircleRadius)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local BallStrikeMarginY <const> = 4
|
||||||
|
local BallStrikeWidth <const> = 60
|
||||||
|
local BallStrikeHeight <const> = (BallStrikeMarginY * 2) + ScoreFont:getHeight()
|
||||||
|
|
||||||
|
local BallStrikeAnimatorIn <const> =
|
||||||
|
playdate.graphics.animator.new(500, BallStrikeHeight, 0, playdate.easingFunctions.outBounce)
|
||||||
|
|
||||||
|
local BallStrikeAnimatorOut <const> =
|
||||||
|
playdate.graphics.animator.new(500, 0, BallStrikeHeight, playdate.easingFunctions.linear)
|
||||||
|
|
||||||
|
-- Start out of frame.
|
||||||
|
local currentBallStrikeAnimator = utils.staticAnimator(20)
|
||||||
|
|
||||||
|
function drawBallsAndStrikes(x, y, balls, strikes)
|
||||||
|
if balls == 0 and strikes == 0 then
|
||||||
|
if currentBallStrikeAnimator == BallStrikeAnimatorIn then
|
||||||
|
currentBallStrikeAnimator = BallStrikeAnimatorOut
|
||||||
|
currentBallStrikeAnimator:reset()
|
||||||
|
end
|
||||||
|
if currentBallStrikeAnimator:ended() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if balls + strikes == 1 and currentBallStrikeAnimator ~= BallStrikeAnimatorIn then
|
||||||
|
-- First pitch - should pop in now.
|
||||||
|
currentBallStrikeAnimator = BallStrikeAnimatorIn
|
||||||
|
currentBallStrikeAnimator:reset()
|
||||||
|
end
|
||||||
|
|
||||||
|
y = y + currentBallStrikeAnimator:currentValue()
|
||||||
|
gfx.setColor(gfx.kColorBlack)
|
||||||
|
gfx.fillRect(x, y, BallStrikeWidth, BallStrikeHeight)
|
||||||
|
local originalDrawMode = gfx.getImageDrawMode()
|
||||||
|
gfx.setImageDrawMode(gfx.kDrawModeInverted)
|
||||||
|
|
||||||
|
local text = tostring(balls) .. " - " .. tostring(strikes)
|
||||||
|
local textWidth = ScoreFont:getTextWidth(text)
|
||||||
|
local widthDiff = BallStrikeWidth - textWidth
|
||||||
|
ScoreFont:drawText(text, x + (widthDiff / 2), y + BallStrikeMarginY)
|
||||||
|
gfx.setImageDrawMode(originalDrawMode)
|
||||||
|
end
|
||||||
|
|
||||||
|
local OutBubbleRadius <const> = 5
|
||||||
|
local ScoreboardMarginX <const> = 6
|
||||||
|
local ScoreboardMarginRight <const> = 4
|
||||||
|
local ScoreboardHeight <const> = 55
|
||||||
|
local Indicator = "> "
|
||||||
|
local IndicatorWidth <const> = ScoreFont:getTextWidth(Indicator)
|
||||||
|
|
||||||
|
---@param battingTeam any
|
||||||
|
---@return string, number, string, number
|
||||||
|
function getIndicators(battingTeam)
|
||||||
|
if battingTeam == "home" then
|
||||||
|
return Indicator, 0, "", IndicatorWidth
|
||||||
|
end
|
||||||
|
return "", IndicatorWidth, Indicator, 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local stats = {
|
||||||
|
homeScore = 0,
|
||||||
|
awayScore = 0,
|
||||||
|
outs = 0,
|
||||||
|
inning = 1,
|
||||||
|
battingTeam = nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawScoreboardImpl(x, y)
|
||||||
|
local homeScore = stats.homeScore
|
||||||
|
local awayScore = stats.awayScore
|
||||||
|
|
||||||
|
local homeIndicator, homeOffset, awayIndicator, awayOffset = getIndicators(stats.battingTeam)
|
||||||
|
|
||||||
|
local homeScoreText = homeIndicator .. "HOME " .. (homeScore > 9 and homeScore or " " .. homeScore)
|
||||||
|
local awayScoreText = awayIndicator .. "AWAY " .. (awayScore > 9 and awayScore or " " .. awayScore)
|
||||||
|
|
||||||
|
local rectWidth = (ScoreboardMarginX * 2)
|
||||||
|
+ ScoreboardMarginRight
|
||||||
|
+ ScoreFont:getTextWidth(homeScoreText)
|
||||||
|
+ homeOffset
|
||||||
|
|
||||||
|
gfx.setLineWidth(1)
|
||||||
|
gfx.setColor(gfx.kColorBlack)
|
||||||
|
gfx.fillRect(x, y, rectWidth, ScoreboardHeight)
|
||||||
|
|
||||||
|
local originalDrawMode = gfx.getImageDrawMode()
|
||||||
|
gfx.setImageDrawMode(gfx.kDrawModeInverted)
|
||||||
|
|
||||||
|
ScoreFont:drawText(homeScoreText, x + ScoreboardMarginX + homeOffset, y + 6)
|
||||||
|
ScoreFont:drawText(awayScoreText, x + ScoreboardMarginX + awayOffset, y + 22)
|
||||||
|
local inningOffsetX = (x + ScoreboardMarginX + IndicatorWidth) + (4 * 2.5 * OutBubbleRadius)
|
||||||
|
ScoreFont:drawText(tostring(stats.inning), inningOffsetX, y + 39)
|
||||||
|
|
||||||
|
gfx.setImageDrawMode(originalDrawMode)
|
||||||
|
|
||||||
|
gfx.setColor(gfx.kColorWhite)
|
||||||
|
|
||||||
|
function circleParams(i)
|
||||||
|
local circleOffset = i * 2.5 * OutBubbleRadius
|
||||||
|
return (x + ScoreboardMarginX + OutBubbleRadius + IndicatorWidth) + circleOffset, y + 46, OutBubbleRadius
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = stats.outs, 2 do
|
||||||
|
gfx.drawCircleAtPoint(circleParams(i))
|
||||||
|
end
|
||||||
|
for i = 0, (stats.outs - 1) do
|
||||||
|
gfx.fillCircleAtPoint(circleParams(i))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local newStats = stats
|
||||||
|
|
||||||
|
function drawScoreboard(x, y, statistics, outs, battingTeam, inning)
|
||||||
|
local homeScore, awayScore = utils.totalScores(statistics)
|
||||||
|
if
|
||||||
|
newStats.homeScore ~= homeScore
|
||||||
|
or newStats.awayScore ~= awayScore
|
||||||
|
or newStats.outs ~= outs
|
||||||
|
or newStats.inning ~= inning
|
||||||
|
or newStats.battingTeam ~= battingTeam
|
||||||
|
then
|
||||||
|
newStats = {
|
||||||
|
homeScore = homeScore,
|
||||||
|
awayScore = awayScore,
|
||||||
|
outs = outs,
|
||||||
|
inning = inning,
|
||||||
|
battingTeam = battingTeam,
|
||||||
|
}
|
||||||
|
playdate.timer.new(C.ScoreboardDelayMs, function()
|
||||||
|
stats = newStats
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
drawScoreboardImpl(x, y)
|
||||||
|
end
|
|
@ -0,0 +1,52 @@
|
||||||
|
---@class Panner
|
||||||
|
Panner = {}
|
||||||
|
|
||||||
|
local function panCoroutine(ball)
|
||||||
|
local offset = utils.xy(getDrawOffset(ball.x, ball.y))
|
||||||
|
while true do
|
||||||
|
local target, deltaSeconds = coroutine.yield(offset.x, offset.y)
|
||||||
|
if target == nil then
|
||||||
|
offset = utils.xy(getDrawOffset(ball.x, ball.y))
|
||||||
|
else
|
||||||
|
while utils.moveAtSpeed(offset, 200 * deltaSeconds, target, 20) do
|
||||||
|
target, deltaSeconds = coroutine.yield(offset.x, offset.y)
|
||||||
|
end
|
||||||
|
-- -- Pan back to ball
|
||||||
|
-- while utils.moveAtSpeed(offset, 200 * deltaSeconds, ball, 20) do
|
||||||
|
-- target, deltaSeconds = coroutine.yield(offset.x, offset.y)
|
||||||
|
-- end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ball XyPair
|
||||||
|
function Panner.new(ball)
|
||||||
|
return setmetatable({
|
||||||
|
coroutine = coroutine.create(function()
|
||||||
|
panCoroutine(ball)
|
||||||
|
end),
|
||||||
|
panTarget = nil,
|
||||||
|
}, { __index = Panner })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param deltaSeconds number
|
||||||
|
---@return number offsetX, number offsetY
|
||||||
|
function Panner:get(deltaSeconds)
|
||||||
|
if self.holdUntil and self.holdUntil() then
|
||||||
|
self:reset()
|
||||||
|
end
|
||||||
|
local _, offsetX, offsetY = coroutine.resume(self.coroutine, self.panTarget, deltaSeconds)
|
||||||
|
return offsetX, offsetY
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param panTarget XyPair
|
||||||
|
---@param holdUntil fun(): boolean
|
||||||
|
function Panner:panTo(panTarget, holdUntil)
|
||||||
|
self.panTarget = panTarget
|
||||||
|
self.holdUntil = holdUntil
|
||||||
|
end
|
||||||
|
|
||||||
|
function Panner:reset()
|
||||||
|
self.holdUntil = nil
|
||||||
|
self.panTarget = nil
|
||||||
|
end
|
|
@ -0,0 +1,53 @@
|
||||||
|
---@type pd_graphics_lib
|
||||||
|
local gfx <const> = playdate.graphics
|
||||||
|
|
||||||
|
local ThrowMeterHeight <const> = 50
|
||||||
|
local ThrowMeterLingerSec <const> = 1.5
|
||||||
|
|
||||||
|
local flickerTimer = gfx.animation.blinker.new(50, 50, true)
|
||||||
|
flickerTimer:start()
|
||||||
|
|
||||||
|
---@param x number
|
||||||
|
---@param y number
|
||||||
|
function throwMeter:draw(x, y)
|
||||||
|
gfx.setLineWidth(1)
|
||||||
|
gfx.drawRect(x, y, 14, ThrowMeterHeight)
|
||||||
|
if self.lastReadThrow then
|
||||||
|
local ratio = 1
|
||||||
|
if not self.wasPerfect then
|
||||||
|
ratio = (self.lastReadThrow - throwMeter.MinCharge) / (self.idealPower - throwMeter.MinCharge)
|
||||||
|
end
|
||||||
|
local height = ThrowMeterHeight * ratio
|
||||||
|
gfx.fillRect(x + 2, y + ThrowMeterHeight - height, 10, height)
|
||||||
|
-- TODO: Dither or bend if the user throws too hard
|
||||||
|
-- Or maybe dither if it's too soft - bend if it's too hard
|
||||||
|
if self.wasPerfect then
|
||||||
|
PerfectPowerBg:draw(x - 11, y - 9)
|
||||||
|
if flickerTimer.on then
|
||||||
|
PerfectPowerFlickerLeft:draw(x - 11, y - 9)
|
||||||
|
else
|
||||||
|
PerfectPowerFlickerRight:draw(x - 11, y - 9)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function throwMeter:drawNearFielder(fielder)
|
||||||
|
if not fielder and not self.lastThrower then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if fielder then
|
||||||
|
if fielder ~= self.lastThrower then
|
||||||
|
self.lastReadThrow = nil
|
||||||
|
end
|
||||||
|
self.lastThrower = fielder
|
||||||
|
actionQueue:upsert("throwMeterLinger", 200 + ThrowMeterLingerSec * 1000, function()
|
||||||
|
local dt = 0
|
||||||
|
while dt < ThrowMeterLingerSec do
|
||||||
|
dt = dt + coroutine.yield()
|
||||||
|
end
|
||||||
|
self.lastThrower = nil
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
self:draw(self.lastThrower.x - 25, self.lastThrower.y - 10)
|
||||||
|
end
|
|
@ -0,0 +1,99 @@
|
||||||
|
Transitions = {
|
||||||
|
---@type Scene | nil
|
||||||
|
nextScene = nil,
|
||||||
|
---@type Scene | nil
|
||||||
|
previousScene = nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
local gfx = playdate.graphics
|
||||||
|
|
||||||
|
local previousSceneImage
|
||||||
|
local previousSceneMask
|
||||||
|
local nextSceneImage
|
||||||
|
|
||||||
|
local batImageTable = {}
|
||||||
|
|
||||||
|
local batOffset = 80
|
||||||
|
local degStep = 3
|
||||||
|
|
||||||
|
function loadBatImageTable()
|
||||||
|
for deg = 90 - (degStep * 3), 270 + (degStep * 3), degStep do
|
||||||
|
local img = gfx.image.new(C.Screen.W, C.Screen.H)
|
||||||
|
gfx.pushContext(img)
|
||||||
|
BigBat:drawRotated(C.Center.x, C.Screen.H + batOffset, 90 + deg)
|
||||||
|
gfx.popContext()
|
||||||
|
batImageTable[deg] = img
|
||||||
|
end
|
||||||
|
end
|
||||||
|
loadBatImageTable()
|
||||||
|
|
||||||
|
local function update()
|
||||||
|
local lastAngle
|
||||||
|
local seamAngle = math.rad(270)
|
||||||
|
while seamAngle > math.rad(90) do
|
||||||
|
local deltaSeconds = playdate.getElapsedTime()
|
||||||
|
playdate.resetElapsedTime()
|
||||||
|
-- Setting a max value keeps from leaving unmasked areas
|
||||||
|
seamAngle = seamAngle - math.min(0.1, deltaSeconds * 3)
|
||||||
|
local seamAngleDeg = math.floor(math.deg(seamAngle))
|
||||||
|
seamAngleDeg = seamAngleDeg - (seamAngleDeg % degStep)
|
||||||
|
|
||||||
|
-- Skip re-drawing if no change
|
||||||
|
if lastAngle ~= seamAngleDeg then
|
||||||
|
lastAngle = seamAngleDeg
|
||||||
|
nextSceneImage:draw(0, 0)
|
||||||
|
|
||||||
|
gfx.pushContext(previousSceneMask)
|
||||||
|
gfx.setImageDrawMode(gfx.kDrawModeFillBlack)
|
||||||
|
batImageTable[seamAngleDeg]:draw(0, 0)
|
||||||
|
gfx.popContext()
|
||||||
|
|
||||||
|
previousSceneImage:setMaskImage(previousSceneMask)
|
||||||
|
previousSceneImage:draw(0, 0)
|
||||||
|
batImageTable[seamAngleDeg]:draw(0, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
playdate.update = function()
|
||||||
|
Transitions.nextScene:update()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param nextScene fun() The next playdate.update function
|
||||||
|
function transitionTo(nextScene)
|
||||||
|
if not Transitions.nextScene then
|
||||||
|
error("Expected Transitions to already have nextScene defined! E.g. by calling transitionBetween")
|
||||||
|
end
|
||||||
|
local previousScene = Transitions.nextScene
|
||||||
|
transitionBetween(previousScene, nextScene)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param scene Scene
|
||||||
|
---@return pd_image
|
||||||
|
local function getSceneRender(scene)
|
||||||
|
local image = gfx.image.new(C.Screen.W, C.Screen.H)
|
||||||
|
gfx.pushContext(image)
|
||||||
|
scene:update()
|
||||||
|
gfx.popContext()
|
||||||
|
return image
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param previousScene Scene Has the current playdate.update function
|
||||||
|
---@param nextScene Scene Has the next playdate.update function
|
||||||
|
function transitionBetween(previousScene, nextScene)
|
||||||
|
playdate.wait(2) -- TODO: There's some sort of timing wack here.
|
||||||
|
playdate.update = update
|
||||||
|
|
||||||
|
previousSceneImage = getSceneRender(previousScene)
|
||||||
|
nextSceneImage = getSceneRender(nextScene)
|
||||||
|
|
||||||
|
previousSceneMask = gfx.image.new(C.Screen.W, C.Screen.H, gfx.kColorWhite)
|
||||||
|
previousSceneImage:setMaskImage(previousSceneMask)
|
||||||
|
|
||||||
|
Transitions.nextScene = nextScene
|
||||||
|
Transitions.previousScene = previousScene
|
||||||
|
|
||||||
|
-- Prevents bad transition calculations due to a long "delta"
|
||||||
|
playdate.resetElapsedTime()
|
||||||
|
end
|
220
src/ecs.lua
|
@ -1,220 +0,0 @@
|
||||||
ecs = {}
|
|
||||||
|
|
||||||
local allEntities <const> = {}
|
|
||||||
|
|
||||||
---@alias System { callback: fun(delta: number, entity: any, a: any, b: any, c: any, d: any, e: any, any), shapes: {}, keys: string[], entityCache: nil | table<any, boolean> }
|
|
||||||
|
|
||||||
---@type System[]
|
|
||||||
local systems <const> = {}
|
|
||||||
|
|
||||||
-- TODO: Add entity to any existing systems
|
|
||||||
function ecs.addEntity(entity)
|
|
||||||
allEntities[entity] = true
|
|
||||||
for _, system in pairs(systems) do
|
|
||||||
if entityMatchesShapes(entity, system.shapes) then
|
|
||||||
system.entityCache[entity] = true
|
|
||||||
else
|
|
||||||
system.entityCache[entity] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ecs.removeEntity(entity)
|
|
||||||
allEntities[entity] = nil
|
|
||||||
for _, system in pairs(systems) do
|
|
||||||
system.entityCache[entity] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local Placeholder = {}
|
|
||||||
---@generic T
|
|
||||||
---@return T
|
|
||||||
function ecs.field()
|
|
||||||
return Placeholder
|
|
||||||
end
|
|
||||||
|
|
||||||
function allKeysIncluded(entity, filter)
|
|
||||||
for k, _ in pairs(filter) do
|
|
||||||
if not entity[k] then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function entityMatchesShapes(entity, shapes)
|
|
||||||
for _, shape in pairs(shapes) do
|
|
||||||
if not allKeysIncluded(entity, shape) then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
---@generic T
|
|
||||||
---@generic U
|
|
||||||
---@generic V
|
|
||||||
---@generic W
|
|
||||||
---@param tShape T
|
|
||||||
---@param uShape U?
|
|
||||||
---@param vShape V?
|
|
||||||
---@param wShape W?
|
|
||||||
---@return fun(callback: fun(componentT: T, componentU: U, componentV: V, componentW: W))
|
|
||||||
function ecs.entitiesHavingShapes(tShape, uShape, vShape, wShape)
|
|
||||||
return function() end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Print contents of `tbl`, with indentation.
|
|
||||||
-- `indent` sets the initial level of indentation.
|
|
||||||
function tprint(tbl, indent)
|
|
||||||
if not indent then
|
|
||||||
indent = 0
|
|
||||||
end
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
formatting = string.rep(" ", indent) .. k .. ": "
|
|
||||||
if type(v) == "table" then
|
|
||||||
print(formatting)
|
|
||||||
tprint(v, indent + 1)
|
|
||||||
elseif type(v) == "boolean" then
|
|
||||||
print(formatting .. tostring(v))
|
|
||||||
else
|
|
||||||
print(formatting .. v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function addSystem(callback, keys, shapes)
|
|
||||||
systems[#systems + 1] = {
|
|
||||||
callback = callback,
|
|
||||||
keys = keys,
|
|
||||||
shapes = shapes,
|
|
||||||
entityCache = nil,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
---@return boolean
|
|
||||||
function is(entity, shape)
|
|
||||||
return allKeysIncluded(entity, shape)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param deltaSeconds number
|
|
||||||
function ecs.update(deltaSeconds)
|
|
||||||
for _, system in pairs(systems) do
|
|
||||||
if not system.entityCache then
|
|
||||||
system.entityCache = {}
|
|
||||||
for entity, _ in pairs(allEntities) do
|
|
||||||
if entityMatchesShapes(entity, system.shapes) then
|
|
||||||
system.entityCache[entity] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local keys = system.keys
|
|
||||||
for entity, _ in pairs(system.entityCache) do
|
|
||||||
system.callback(
|
|
||||||
deltaSeconds,
|
|
||||||
entity,
|
|
||||||
entity[keys[1]],
|
|
||||||
entity[keys[2]],
|
|
||||||
entity[keys[3]],
|
|
||||||
entity[keys[4]],
|
|
||||||
entity
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns a function that accepts a callback. This callback will receive one argument for each Shape provided.
|
|
||||||
---
|
|
||||||
---@generic T
|
|
||||||
---@generic TKey
|
|
||||||
---@generic U
|
|
||||||
---@generic UKey
|
|
||||||
---@generic V
|
|
||||||
---@generic VKey
|
|
||||||
---@generic W
|
|
||||||
---@generic WKey
|
|
||||||
---@param tShape { [TKey]: T }
|
|
||||||
---@param uShape { [UKey]: U } | fun(entity: any, componentT: T, any) | nil
|
|
||||||
---@param vShape { [VKey]: V } | fun(entity: any, componentT: T, componentU: U, any) | nil
|
|
||||||
---@param wShape { [WKey]: W } | fun(entity: any, componentT: T, componentU: U, componentV: V, any) | nil
|
|
||||||
---@param finalFunc fun(entity: any, componentT: T, componentU: U, componentV: V, componentW: W, any) | nil
|
|
||||||
function ecs.forEntitiesWith(tShape, uShape, vShape, wShape, finalFunc)
|
|
||||||
local maybeShapes = { tShape, uShape, vShape, wShape, finalFunc }
|
|
||||||
local shapes = {}
|
|
||||||
local callback
|
|
||||||
|
|
||||||
for _, maybeShape in pairs(maybeShapes) do
|
|
||||||
if type(maybeShape) == "table" then
|
|
||||||
shapes[#shapes + 1] = maybeShape
|
|
||||||
elseif type(maybeShape) == "function" then
|
|
||||||
callback = maybeShape
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local keys = {}
|
|
||||||
for _, shape in pairs(shapes) do
|
|
||||||
for key, _ in pairs(shape) do
|
|
||||||
keys[#keys + 1] = key
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
addSystem(callback, keys, shapes)
|
|
||||||
end
|
|
||||||
|
|
||||||
local f = ecs.field()
|
|
||||||
local XYPair = { x = f, y = f }
|
|
||||||
local Position = { position = XYPair }
|
|
||||||
local Target = { target = XYPair }
|
|
||||||
local Velocity = { velocity = XYPair }
|
|
||||||
|
|
||||||
function ecs.overlayOnto(entity, value)
|
|
||||||
for key, v in pairs(value) do
|
|
||||||
entity[key] = v
|
|
||||||
end
|
|
||||||
ecs.addEntity(entity)
|
|
||||||
end
|
|
||||||
|
|
||||||
local data = {
|
|
||||||
position = { x = 0, y = 0 },
|
|
||||||
velocity = { x = 1, y = 2 },
|
|
||||||
}
|
|
||||||
|
|
||||||
---@generic T
|
|
||||||
---@param shape T
|
|
||||||
---@param entity unknown
|
|
||||||
---@return T
|
|
||||||
function ecs.get(shape, entity)
|
|
||||||
return entity
|
|
||||||
end
|
|
||||||
|
|
||||||
---@generic T
|
|
||||||
---@param entity unknown
|
|
||||||
---@param shape `T`
|
|
||||||
---@param value `T`
|
|
||||||
function ecs.set(entity, shape, value)
|
|
||||||
for key, v in pairs(shape) do
|
|
||||||
entity[key] = value[v]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ecs.addEntity(data)
|
|
||||||
|
|
||||||
ecs.forEntitiesWith(Position, Velocity, function(delta, e, pos, vel)
|
|
||||||
pos.x = pos.x + (delta * vel.x)
|
|
||||||
pos.y = pos.y + (delta * vel.y)
|
|
||||||
print("position")
|
|
||||||
tprint(pos, 1)
|
|
||||||
ecs.set(Target, e, {
|
|
||||||
--target = { x = 10, y = 10}
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
|
|
||||||
ecs.forEntitiesWith(Target, function(delta, e, pos, vel)
|
|
||||||
pos.x = pos.x + (delta * vel.x)
|
|
||||||
pos.y = pos.y + (delta * vel.y)
|
|
||||||
print("position")
|
|
||||||
tprint(pos, 1)
|
|
||||||
ecs.set(e, Target, "hallo")
|
|
||||||
end)
|
|
||||||
|
|
||||||
ecs.update(1)
|
|
|
@ -0,0 +1,219 @@
|
||||||
|
--- @class Fielder {
|
||||||
|
--- @field name string
|
||||||
|
--- @field x number
|
||||||
|
--- @field y number
|
||||||
|
--- @field targets XyPair[]
|
||||||
|
--- @field speed number
|
||||||
|
--- @field spriteIndex number
|
||||||
|
|
||||||
|
---@class Fielders
|
||||||
|
---@field first Fielder
|
||||||
|
---@field second Fielder
|
||||||
|
---@field shortstop Fielder
|
||||||
|
---@field third Fielder
|
||||||
|
---@field pitcher Fielder
|
||||||
|
---@field catcher Fielder
|
||||||
|
---@field left Fielder
|
||||||
|
---@field center Fielder
|
||||||
|
---@field right Fielder
|
||||||
|
|
||||||
|
---@class Fielding
|
||||||
|
---@field fielders Fielders
|
||||||
|
---@field fielderHoldingBall Fielder | nil
|
||||||
|
Fielding = {}
|
||||||
|
|
||||||
|
FielderDanceAnimator = playdate.graphics.animator.new(1, 10, 0, utils.easingHill)
|
||||||
|
FielderDanceAnimator.repeatCount = C.DanceBounceCount - 1
|
||||||
|
|
||||||
|
---@param name string
|
||||||
|
---@param speed number
|
||||||
|
---@return Fielder
|
||||||
|
local function newFielder(name, speed)
|
||||||
|
return {
|
||||||
|
name = name,
|
||||||
|
speed = speed * C.FielderRunMult,
|
||||||
|
spriteIndex = math.random(#HomeTeamSpriteGroup),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function Fielding.new()
|
||||||
|
return setmetatable({
|
||||||
|
fielders = {
|
||||||
|
first = newFielder("First", 40),
|
||||||
|
second = newFielder("Second", 40),
|
||||||
|
shortstop = newFielder("Shortstop", 40),
|
||||||
|
third = newFielder("Third", 40),
|
||||||
|
pitcher = newFielder("Pitcher", 30),
|
||||||
|
catcher = newFielder("Catcher", 35),
|
||||||
|
left = newFielder("Left", 50),
|
||||||
|
center = newFielder("Center", 50),
|
||||||
|
right = newFielder("Right", 50),
|
||||||
|
},
|
||||||
|
---@type Fielder | nil
|
||||||
|
fielderHoldingBall = nil,
|
||||||
|
}, { __index = Fielding })
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Actually only benches the infield, because outfielders are far away!
|
||||||
|
---@param position XyPair
|
||||||
|
function Fielding:benchTo(position)
|
||||||
|
self.fielders.first.targets = { position }
|
||||||
|
self.fielders.second.targets = { position }
|
||||||
|
self.fielders.shortstop.targets = { position }
|
||||||
|
self.fielders.third.targets = { position }
|
||||||
|
self.fielders.pitcher.targets = { position }
|
||||||
|
self.fielders.catcher.targets = { position }
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Resets the target positions of all fielders to their defaults (at their field positions).
|
||||||
|
---@param fromOffTheField XyPair | nil If provided, also sets all runners' current position to one centralized location.
|
||||||
|
---@param immediate boolean | nil
|
||||||
|
function Fielding:resetFielderPositions(fromOffTheField, immediate)
|
||||||
|
if fromOffTheField then
|
||||||
|
for _, fielder in pairs(self.fielders) do
|
||||||
|
fielder.x = fromOffTheField.x
|
||||||
|
fielder.y = fromOffTheField.y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self.fielders.first.targets = { utils.xy(C.Screen.W - 65, C.Screen.H * 0.48) }
|
||||||
|
self.fielders.second.targets = { utils.xy(C.Screen.W * 0.70, C.Screen.H * 0.30) }
|
||||||
|
self.fielders.shortstop.targets = { utils.xy(C.Screen.W * 0.30, C.Screen.H * 0.30) }
|
||||||
|
self.fielders.third.targets = { utils.xy(C.Screen.W * 0.1, C.Screen.H * 0.48) }
|
||||||
|
self.fielders.pitcher.targets = { utils.xy(C.PitcherStartPos.x, C.PitcherStartPos.y) }
|
||||||
|
self.fielders.catcher.targets = { utils.xy(C.Screen.W * 0.475, C.Screen.H * 0.92) }
|
||||||
|
self.fielders.left.targets = { utils.xy(C.Screen.W * -0.6, C.Screen.H * -0.1) }
|
||||||
|
self.fielders.center.targets = { utils.xy(C.Center.x, C.Screen.H * -0.4) }
|
||||||
|
self.fielders.right.targets = { utils.xy(C.Screen.W * 1.6, self.fielders.left.targets[1].y) }
|
||||||
|
|
||||||
|
if immediate then
|
||||||
|
for _, fielder in pairs(self.fielders) do
|
||||||
|
fielder.x = fielder.targets[1].x
|
||||||
|
fielder.y = fielder.targets[1].y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param deltaSeconds number
|
||||||
|
---@param fielder Fielder
|
||||||
|
---@param ball Ball
|
||||||
|
---@return boolean canCatch
|
||||||
|
local function updateFielderPosition(deltaSeconds, fielder, ball)
|
||||||
|
if #fielder.targets > 0 then
|
||||||
|
local nextFielderPos = utils.xy(fielder.x, fielder.y)
|
||||||
|
local currentTarget = fielder.targets[#fielder.targets]
|
||||||
|
local willMove = utils.moveAtSpeed(nextFielderPos, fielder.speed * deltaSeconds, currentTarget)
|
||||||
|
|
||||||
|
if willMove and utils.pointIsAboveLine(nextFielderPos, C.BottomOfOutfieldWall, 40) then
|
||||||
|
local targetCount = #fielder.targets
|
||||||
|
-- Back up a little
|
||||||
|
fielder.targets[targetCount + 2] = utils.xy(fielder.x, fielder.y + 5)
|
||||||
|
-- Try to come at it from below
|
||||||
|
fielder.targets[targetCount + 1] = utils.xy(currentTarget.x, fielder.y + 10)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not utils.moveAtSpeed(fielder, fielder.speed * deltaSeconds, fielder.targets[#fielder.targets]) then
|
||||||
|
table.remove(fielder.targets, #fielder.targets)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- TODO: Clean this up, like, a lot.
|
||||||
|
-- I'd love to avoid any "real" pathfinding implementation, but these huge target queues are liable to be an issue.
|
||||||
|
-- The worst case came when a ball was hit far, but not in a way that the game classed as a home run.
|
||||||
|
-- Maybe this queueing would be fine if that issue was resolved
|
||||||
|
if #fielder.targets >= 10 then
|
||||||
|
fielder.targets = { utils.xy(fielder.x, fielder.y + 100) }
|
||||||
|
end
|
||||||
|
assert(#fielder.targets < 10, "Fielder " .. fielder.name .. " is accruing too many target positions!")
|
||||||
|
|
||||||
|
return ball.catchable and utils.distanceBetweenPoints(fielder, ball) < C.BallCatchHitbox
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: Prevent multiple fielders covering the same base.
|
||||||
|
-- At least in a how-about-everybody-stand-right-here way.
|
||||||
|
|
||||||
|
--- Selects the nearest fielder to move toward the given coordinates.
|
||||||
|
--- Other fielders should attempt to cover their bases
|
||||||
|
---@param ball Point3d
|
||||||
|
---@param ballDest XyPair
|
||||||
|
function Fielding:haveSomeoneChase(ball, ballDest)
|
||||||
|
local chasingFielder = utils.getNearestOf(self.fielders, ballDest.x, ballDest.y)
|
||||||
|
-- Start moving toward the ball directly after reaching ballDest
|
||||||
|
chasingFielder.targets = { ball, ballDest }
|
||||||
|
|
||||||
|
for _, base in ipairs(C.Bases) do
|
||||||
|
local nearest = utils.getNearestOf(self.fielders, base.x, base.y, function(fielder)
|
||||||
|
-- Skip the pitcher for 2B - they're considered closer than second or shortstop.
|
||||||
|
if fielder == self.fielders.pitcher and base == C.Bases[C.Second] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return fielder ~= chasingFielder
|
||||||
|
end)
|
||||||
|
nearest.targets = { base }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- **Also updates `ball.heldby`**
|
||||||
|
---@param ball Ball
|
||||||
|
---@param deltaSeconds number
|
||||||
|
---@return Fielder | nil, boolean fielderHoldingBall nil if no fielder is currently touching the ball, true if caught a fly ball
|
||||||
|
function Fielding:updateFielderPositions(ball, deltaSeconds)
|
||||||
|
local fielderHoldingBall
|
||||||
|
local caughtAFlyBall = false
|
||||||
|
for _, fielder in pairs(self.fielders) do
|
||||||
|
-- TODO: Base this catch on fielder skill?
|
||||||
|
local canCatch = updateFielderPosition(deltaSeconds, fielder, ball)
|
||||||
|
if canCatch then
|
||||||
|
fielderHoldingBall = fielder
|
||||||
|
ball.heldBy = fielder -- How much havoc will this wreak?
|
||||||
|
if ball.isFlyBall then
|
||||||
|
ball.isFlyBall = false
|
||||||
|
caughtAFlyBall = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.fielderHoldingBall = fielderHoldingBall
|
||||||
|
return fielderHoldingBall, caughtAFlyBall
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO? Start moving target fielders close sooner?
|
||||||
|
---@param field Fielding
|
||||||
|
---@param targetBase Base
|
||||||
|
---@param ball { launch: LaunchBall }
|
||||||
|
---@param throwFlyMs number
|
||||||
|
local function userThrowToCoroutine(field, targetBase, ball, throwFlyMs)
|
||||||
|
while true do
|
||||||
|
if field.fielderHoldingBall == nil then
|
||||||
|
coroutine.yield()
|
||||||
|
else
|
||||||
|
local closestFielder = utils.getNearestOf(field.fielders, targetBase.x, targetBase.y, function(fielder)
|
||||||
|
return fielder ~= field.fielderHoldingBall -- presumably, this is who will be doing the throwing
|
||||||
|
end)
|
||||||
|
|
||||||
|
closestFielder.targets = { targetBase }
|
||||||
|
ball:launch(targetBase.x, targetBase.y, playdate.easingFunctions.linear, throwFlyMs)
|
||||||
|
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Buffer in a fielder throw action.
|
||||||
|
---@param self table
|
||||||
|
---@param targetBase Base
|
||||||
|
---@param ball { launch: LaunchBall }
|
||||||
|
---@param throwFlyMs number
|
||||||
|
function Fielding:userThrowTo(targetBase, ball, throwFlyMs)
|
||||||
|
local maxTryTimeMs = 5000
|
||||||
|
actionQueue:upsert("userThrowTo", maxTryTimeMs, function()
|
||||||
|
userThrowToCoroutine(self, targetBase, ball, throwFlyMs)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Fielding.celebrate()
|
||||||
|
FielderDanceAnimator:reset(C.DanceBounceMs)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
if not playdate or playdate.TEST_MODE then
|
||||||
|
return { Fielding, newFielder }
|
||||||
|
end
|
Before Width: | Height: | Size: 11 KiB |
|
@ -1,40 +1,63 @@
|
||||||
--- Assumes that background image is of size
|
local gfx <const> = playdate.graphics
|
||||||
|
|
||||||
|
local ButtonFont <const> = FontFullCircle
|
||||||
|
|
||||||
|
--- Assumes that background image is of size:
|
||||||
|
---
|
||||||
--- XXX
|
--- XXX
|
||||||
--- XOX
|
--- XOX
|
||||||
|
---
|
||||||
--- Where each character is the size of the screen, and 'O' is the default view.
|
--- Where each character is the size of the screen, and 'O' is the default view.
|
||||||
function getDrawOffset(screenW, screenH, ballX, ballY)
|
function getDrawOffset(ballX, ballY)
|
||||||
local offsetX, offsetY
|
local offsetX, offsetY
|
||||||
if ballY > screenH then
|
if ballY > C.Screen.H or ballX >= C.BallOffscreen then
|
||||||
return 0, 0
|
return 0, 0
|
||||||
end
|
end
|
||||||
offsetY = math.max(0, -1 * ballY)
|
-- Keep the ball approximately in the center, once it's past C.Center.y - 30
|
||||||
|
offsetY = math.max(0, (-1 * ballY) + C.Center.y - 30)
|
||||||
|
|
||||||
if ballX > 0 and ballX < screenW then
|
if ballX >= 0 and ballX <= C.Screen.W then
|
||||||
offsetX = 0
|
offsetX = 0
|
||||||
elseif ballX < 0 then
|
elseif ballX < 0 then
|
||||||
offsetX = math.max(-1 * screenW, ballX * -1)
|
offsetX = math.max(-1 * C.Screen.W, ballX * -1)
|
||||||
elseif ballX > screenW then
|
elseif ballX > C.Screen.W then
|
||||||
offsetX = math.min(screenW * 2, (ballX * -1) + screenW)
|
offsetX = math.min(C.Screen.W * 2, (ballX * -1) + C.Screen.W)
|
||||||
end
|
end
|
||||||
|
|
||||||
return offsetX * 1.3, offsetY * 1.5
|
return offsetX * 1.3, offsetY
|
||||||
end
|
end
|
||||||
|
|
||||||
-- selene: allow(unscoped_variables)
|
local buttonBlinker = gfx.animation.blinker.new(750, 500, true)
|
||||||
|
buttonBlinker:start()
|
||||||
|
|
||||||
|
--- Requires calling `playdate.graphics.animation.blinker.updateAll()` during `update()` to blink correctly.
|
||||||
|
function drawButton(buttonLabel, x, y)
|
||||||
|
gfx.setColor(gfx.kColorWhite)
|
||||||
|
gfx.fillCircleAtPoint(x + 4, y + 7, 12)
|
||||||
|
gfx.setColor(gfx.kColorBlack)
|
||||||
|
if buttonBlinker.on then
|
||||||
|
gfx.setLineWidth(1)
|
||||||
|
gfx.drawCircleAtPoint(x + 4, y + 7, 10)
|
||||||
|
end
|
||||||
|
ButtonFont:drawText(buttonLabel, x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class Blipper
|
||||||
|
---@field draw fun(self: self, disableBlipping: boolean, x: number, y: number)
|
||||||
blipper = {}
|
blipper = {}
|
||||||
|
|
||||||
--- Build an object that simply "blips" between the given images at the given interval.
|
--- Build an object that simply "blips" between the given images at the given interval.
|
||||||
--- Expects `playdate.graphics.animation.blinker.updateAll()` to be called on every update.
|
--- Expects `playdate.graphics.animation.blinker.updateAll()` to be called on every update.
|
||||||
function blipper.new(msInterval, imagePath1, imagePath2)
|
function blipper.new(msInterval, spriteCollection)
|
||||||
local blinker = playdate.graphics.animation.blinker.new(msInterval, msInterval, true)
|
local blinker = gfx.animation.blinker.new(msInterval, msInterval, true)
|
||||||
blinker:start()
|
blinker:start()
|
||||||
return {
|
return {
|
||||||
blinker = blinker,
|
blinker = blinker,
|
||||||
image1 = playdate.graphics.image.new(imagePath1),
|
draw = function(self, disableBlipping, x, y, hasSpriteIndex)
|
||||||
image2 = playdate.graphics.image.new(imagePath2),
|
local spriteBundle = spriteCollection[hasSpriteIndex.spriteIndex]
|
||||||
draw = function(self, disableBlipping, x, y)
|
local currentImage = (disableBlipping or self.blinker.on) and spriteBundle.lowHat or spriteBundle.smiling
|
||||||
local currentImage = (disableBlipping or self.blinker.on) and self.image2 or self.image1
|
local offsetY = currentImage == spriteBundle.lowHat and -1 or 0
|
||||||
currentImage:draw(x, y)
|
currentImage:draw(x, y + offsetY)
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 681 B |
Before Width: | Height: | Size: 712 B |
Before Width: | Height: | Size: 711 B |
Before Width: | Height: | Size: 720 B |
|
@ -0,0 +1,154 @@
|
||||||
|
-- stylua: ignore start
|
||||||
|
import "control-screen.lua"
|
||||||
|
-- stylua: ignore end
|
||||||
|
|
||||||
|
---@class MainMenu
|
||||||
|
MainMenu = {
|
||||||
|
---@type { new: fun(settings: Settings): { update: fun(self) } }
|
||||||
|
next = nil,
|
||||||
|
}
|
||||||
|
local gfx = playdate.graphics
|
||||||
|
|
||||||
|
local ScoreFont <const> = FontFullCircle
|
||||||
|
local TinyFont <const> = NanoSans
|
||||||
|
|
||||||
|
--- Take control of playdate.update
|
||||||
|
--- Will replace playdate.update when the menu is done.
|
||||||
|
---@param next { new: fun(settings: Settings): { update: fun(self) } }
|
||||||
|
function MainMenu.start(next)
|
||||||
|
MenuMusic:play(0)
|
||||||
|
MainMenu.next = next
|
||||||
|
playdate.update = function()
|
||||||
|
MainMenu:update()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local inningCountSelection = 3
|
||||||
|
|
||||||
|
function MainMenu:showControls()
|
||||||
|
local next = ControlScreen.new(self)
|
||||||
|
transitionBetween(MainMenu, next)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function startGame()
|
||||||
|
local next = MainMenu.next.new({
|
||||||
|
finalInning = inningCountSelection,
|
||||||
|
homeTeamSpriteGroup = HomeTeamSpriteGroup,
|
||||||
|
awayTeamSpriteGroup = AwayTeamSpriteGroup,
|
||||||
|
})
|
||||||
|
playdate.resetElapsedTime()
|
||||||
|
transitionBetween(MainMenu, next)
|
||||||
|
MenuMusic:setPaused(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param baseEaser EasingFunc
|
||||||
|
---@return EasingFunc
|
||||||
|
local function pausingEaser(baseEaser)
|
||||||
|
--- t: elapsedTime
|
||||||
|
--- d: duration
|
||||||
|
return function(t, b, c, d)
|
||||||
|
local percDone = t / d
|
||||||
|
if percDone > 0.9 then
|
||||||
|
t = d
|
||||||
|
elseif percDone < 0.1 then
|
||||||
|
t = 0
|
||||||
|
else
|
||||||
|
t = (percDone - 0.1) * 1.25 * d
|
||||||
|
end
|
||||||
|
return baseEaser(t, b, c, d)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local animatorX = gfx.animator.new(2000, 30, 350, pausingEaser(playdate.easingFunctions.linear))
|
||||||
|
animatorX.repeatCount = -1
|
||||||
|
animatorX.reverses = true
|
||||||
|
|
||||||
|
local animatorY = gfx.animator.new(2000, 60, 200, pausingEaser(utils.easingHill))
|
||||||
|
animatorY.repeatCount = -1
|
||||||
|
animatorY.reverses = true
|
||||||
|
|
||||||
|
---@type number
|
||||||
|
local crankStartPos
|
||||||
|
|
||||||
|
---@generic T
|
||||||
|
---@param array T[]
|
||||||
|
---@param crankPosition number
|
||||||
|
---@return T
|
||||||
|
local function arrayElementFromCrank(array, crankPosition)
|
||||||
|
local i = math.ceil(#array * (crankPosition + 0.001) / 360)
|
||||||
|
return array[i]
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type pd_image
|
||||||
|
local currentLogo
|
||||||
|
|
||||||
|
--luacheck: ignore
|
||||||
|
function MainMenu:update()
|
||||||
|
if playdate.buttonJustPressed(playdate.kButtonA) then
|
||||||
|
startGame()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if playdate.buttonJustPressed(playdate.kButtonB) then
|
||||||
|
self:showControls()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
playdate.timer.updateTimers()
|
||||||
|
crankStartPos = crankStartPos or playdate.getCrankPosition()
|
||||||
|
|
||||||
|
gfx.clear()
|
||||||
|
|
||||||
|
if playdate.getCrankChange() ~= 0 then
|
||||||
|
local crankOffset = (crankStartPos - playdate.getCrankPosition()) % 360
|
||||||
|
currentLogo = arrayElementFromCrank(Logos, crankOffset).image
|
||||||
|
replaceAwayLogo(currentLogo)
|
||||||
|
end
|
||||||
|
|
||||||
|
if currentLogo then
|
||||||
|
currentLogo:drawScaled(20, C.Center.y + 40, 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
if playdate.buttonJustPressed(playdate.kButtonUp) or playdate.buttonJustPressed(playdate.kButtonRight) then
|
||||||
|
inningCountSelection = math.min(99, inningCountSelection + 1)
|
||||||
|
end
|
||||||
|
if playdate.buttonJustPressed(playdate.kButtonDown) or playdate.buttonJustPressed(playdate.kButtonLeft) then
|
||||||
|
inningCountSelection = math.max(1, inningCountSelection - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local logoCenter = 90
|
||||||
|
GameLogo:drawCentered(C.Center.x, logoCenter)
|
||||||
|
TinyFont:drawTextAligned("a game by Sage", C.Center.x, logoCenter + 35, kTextAlignment.center)
|
||||||
|
|
||||||
|
local promptOffsetX = 120
|
||||||
|
ScoreFont:drawTextAligned(
|
||||||
|
"Press A to start with <" .. inningCountSelection .. "> innings",
|
||||||
|
C.Center.x - promptOffsetX,
|
||||||
|
180,
|
||||||
|
kTextAlignment.left
|
||||||
|
)
|
||||||
|
ScoreFont:drawTextAligned("Press B for controls", C.Center.x - promptOffsetX, 198, kTextAlignment.left)
|
||||||
|
|
||||||
|
local ball = {
|
||||||
|
x = animatorX:currentValue(),
|
||||||
|
y = animatorY:currentValue(),
|
||||||
|
z = 6,
|
||||||
|
size = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
local fielder1 = { x = 30, y = 200, spriteIndex = 1 }
|
||||||
|
local ballIsHeld = drawFielder(AwayTeamSpriteGroup, fielder1, ball)
|
||||||
|
|
||||||
|
local fielder2 = { x = 350, y = 200, spriteIndex = 2 }
|
||||||
|
ballIsHeld = drawFielder(HomeTeamSpriteGroup, fielder2, ball, playdate.graphics.kImageFlippedX) or ballIsHeld
|
||||||
|
|
||||||
|
if not ballIsHeld then
|
||||||
|
gfx.setLineWidth(2)
|
||||||
|
|
||||||
|
gfx.setColor(gfx.kColorWhite)
|
||||||
|
gfx.fillCircleAtPoint(ball.x, ball.y, ball.size)
|
||||||
|
|
||||||
|
gfx.setColor(gfx.kColorBlack)
|
||||||
|
gfx.drawCircleAtPoint(ball.x, ball.y, ball.size)
|
||||||
|
end
|
||||||
|
end
|
1573
src/main.lua
|
@ -0,0 +1,177 @@
|
||||||
|
local npcBatDeg = 0
|
||||||
|
local BaseNpcBatSpeed <const> = 1000
|
||||||
|
local npcBatSpeed = BaseNpcBatSpeed
|
||||||
|
|
||||||
|
---@class Npc: InputHandler
|
||||||
|
---@field runners Runner[]
|
||||||
|
---@field fielders Fielder[]
|
||||||
|
-- selene: allow(unscoped_variables)
|
||||||
|
Npc = {}
|
||||||
|
|
||||||
|
---@param runners Runner[]
|
||||||
|
---@param fielders Fielder[]
|
||||||
|
---@return Npc
|
||||||
|
function Npc.new(runners, fielders)
|
||||||
|
return setmetatable({
|
||||||
|
runners = runners,
|
||||||
|
fielders = fielders,
|
||||||
|
}, { __index = Npc })
|
||||||
|
end
|
||||||
|
|
||||||
|
function Npc.update() end
|
||||||
|
|
||||||
|
-- TODO: FAR more nuanced NPC batting.
|
||||||
|
-- luacheck: no unused
|
||||||
|
---@param ball XyPair
|
||||||
|
---@param pitchIsOver boolean
|
||||||
|
---@param deltaSec number
|
||||||
|
---@return number batAngleDeg, number batSpeed
|
||||||
|
function Npc:updateBatAngle(ball, pitchIsOver, deltaSec)
|
||||||
|
if
|
||||||
|
not pitchIsOver
|
||||||
|
and ball.y > 200
|
||||||
|
and ball.y < 230
|
||||||
|
and (ball.x < C.Center.x + 15)
|
||||||
|
and (ball.x > C.Center.x - 12)
|
||||||
|
then
|
||||||
|
npcBatDeg = npcBatDeg + (deltaSec * npcBatSpeed)
|
||||||
|
else
|
||||||
|
npcBatSpeed = (1 + math.random()) * BaseNpcBatSpeed
|
||||||
|
npcBatDeg = utils.moveAtSpeed1d(npcBatDeg, deltaSec * BaseNpcBatSpeed, 230 - 360)
|
||||||
|
end
|
||||||
|
return npcBatDeg, (self:batSpeed() * deltaSec)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return number
|
||||||
|
function Npc:batSpeed()
|
||||||
|
return npcBatSpeed * 1.25
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return number flyTimeMs, number pitchId, number accuracy
|
||||||
|
function Npc:pitch()
|
||||||
|
return C.PitchFlyMs / self:pitchSpeed(), math.random(#Pitches), 0.9
|
||||||
|
end
|
||||||
|
|
||||||
|
local baseRunningSpeed = 25
|
||||||
|
|
||||||
|
---@param runner Runner
|
||||||
|
---@param ball Point3d
|
||||||
|
---@return number
|
||||||
|
function Npc:runningSpeed(runner, ball)
|
||||||
|
if #self.runners == 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local distanceFromBall = utils.distanceBetweenZ(ball.x, ball.y, ball.z, runner.x, runner.y, 0)
|
||||||
|
|
||||||
|
if distanceFromBall > 400 or runner.forcedTo then
|
||||||
|
return baseRunningSpeed
|
||||||
|
end
|
||||||
|
|
||||||
|
local touchedBase = utils.isTouchingBase(runner.x, runner.y)
|
||||||
|
if not touchedBase and runner.nextBase then
|
||||||
|
local distToNext = utils.distanceBetween(runner.x, runner.y, runner.nextBase.x, runner.nextBase.y)
|
||||||
|
local distToPrev = utils.distanceBetween(runner.x, runner.y, runner.prevBase.x, runner.prevBase.y)
|
||||||
|
if distToNext < distToPrev or distanceFromBall > 350 then
|
||||||
|
return baseRunningSpeed
|
||||||
|
else
|
||||||
|
return -1 * baseRunningSpeed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param runners Runner[]
|
||||||
|
---@return Base[]
|
||||||
|
local function getForcedOutTargets(runners)
|
||||||
|
local targets = {}
|
||||||
|
for _, base in ipairs(C.Bases) do
|
||||||
|
local runnerTargetingBase = utils.getRunnerWithNextBase(runners, base)
|
||||||
|
if runnerTargetingBase then
|
||||||
|
targets[#targets + 1] = base
|
||||||
|
else
|
||||||
|
return targets
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return targets
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the position,distance of the base closest to the runner who is *furthest* from a base
|
||||||
|
---@param runners Runner[]
|
||||||
|
---@return Base | nil, number | nil
|
||||||
|
local function getBaseOfStrandedRunner(runners)
|
||||||
|
local farRunnersBase, farDistance
|
||||||
|
for _, runner in pairs(runners) do
|
||||||
|
--if runner ~= batter then
|
||||||
|
local nearestBase, distance = utils.getNearestOf(C.Bases, runner.x, runner.y)
|
||||||
|
if farRunnersBase == nil or farDistance < distance then
|
||||||
|
farRunnersBase = nearestBase
|
||||||
|
farDistance = distance
|
||||||
|
end
|
||||||
|
--end
|
||||||
|
end
|
||||||
|
|
||||||
|
return farRunnersBase, farDistance
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns x,y of the out target
|
||||||
|
---@param runners Runner[]
|
||||||
|
---@return number|nil, number|nil
|
||||||
|
local function getNextOutTarget(runners)
|
||||||
|
-- TODO: Handle missed throws, check for fielders at target, etc.
|
||||||
|
local targets = getForcedOutTargets(runners)
|
||||||
|
if #targets ~= 0 then
|
||||||
|
return targets[#targets].x, targets[#targets].y
|
||||||
|
end
|
||||||
|
|
||||||
|
local baseCloseToStrandedRunner = getBaseOfStrandedRunner(runners)
|
||||||
|
if baseCloseToStrandedRunner then
|
||||||
|
return baseCloseToStrandedRunner.x, baseCloseToStrandedRunner.y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param fielders Fielder[]
|
||||||
|
---@param fielder Fielder
|
||||||
|
---@param runners Runner[]
|
||||||
|
---@param ball { x: number, y: number, heldBy: Fielder | nil, launch: LaunchBall }
|
||||||
|
local function tryToMakeAPlay(fielders, fielder, runners, ball)
|
||||||
|
local targetX, targetY = getNextOutTarget(runners)
|
||||||
|
if targetX == nil or targetY == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local nearestFielder = utils.getNearestOf(fielders, targetX, targetY)
|
||||||
|
nearestFielder.targets = { utils.xy(targetX, targetY) }
|
||||||
|
if nearestFielder == fielder then
|
||||||
|
ball.heldBy = fielder
|
||||||
|
else
|
||||||
|
ball:launch(targetX, targetY, playdate.easingFunctions.linear, nil, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param fielder Fielder
|
||||||
|
---@param outedSomeRunner boolean
|
||||||
|
---@param ball { x: number, y: number, heldBy: Fielder | nil, launch: LaunchBall }
|
||||||
|
function Npc:fielderAction(fielder, outedSomeRunner, ball)
|
||||||
|
if not fielder then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local playDelay = outedSomeRunner and 0.5 or 0.1
|
||||||
|
actionQueue:newOnly("npcFielderAction", 2000, function()
|
||||||
|
local dt = 0
|
||||||
|
while dt < playDelay do
|
||||||
|
dt = dt + coroutine.yield()
|
||||||
|
end
|
||||||
|
tryToMakeAPlay(self.fielders, fielder, self.runners, ball)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return number
|
||||||
|
function Npc:pitchSpeed()
|
||||||
|
return 2
|
||||||
|
end
|
||||||
|
|
||||||
|
if not playdate then
|
||||||
|
return Npc
|
||||||
|
end
|
|
@ -2,6 +2,6 @@ name=Batter Up!
|
||||||
author=Sage Vaillancourt
|
author=Sage Vaillancourt
|
||||||
description=Crush dingers and hustle around the bases!
|
description=Crush dingers and hustle around the bases!
|
||||||
bundleID=space.sagev.batterup
|
bundleID=space.sagev.batterup
|
||||||
imagePath=images/launcher
|
imagePath=assets/images/launcher
|
||||||
version=0.1
|
version=0.1
|
||||||
buildNumber=1
|
buildNumber=1
|
||||||
|
|
|
@ -0,0 +1,222 @@
|
||||||
|
---@alias SimpleAnimator { currentValue: fun(self): number; reset: fun(self, durationMs: number | nil) }
|
||||||
|
---@alias Pitch fun(accuracy: number, ball: Ball): { x: SimpleAnimator, y: SimpleAnimator, z: SimpleAnimator | nil }
|
||||||
|
|
||||||
|
---@type pd_graphics_lib
|
||||||
|
local gfx <const> = playdate.graphics
|
||||||
|
|
||||||
|
local StrikeZoneWidth <const> = C.StrikeZoneEndX - C.StrikeZoneStartX
|
||||||
|
|
||||||
|
-- TODO? Also degrade speed
|
||||||
|
---@param accuracy number
|
||||||
|
---@return number xValueToMissBy
|
||||||
|
function getPitchMissBy(accuracy)
|
||||||
|
accuracy = accuracy or 1.0
|
||||||
|
local missBy = (1 - accuracy) * StrikeZoneWidth * 3
|
||||||
|
if math.random() > 0.5 then
|
||||||
|
missBy = missBy * -1
|
||||||
|
end
|
||||||
|
return missBy
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type Pitch[]
|
||||||
|
Pitches = {
|
||||||
|
-- Fastball
|
||||||
|
function(accuracy)
|
||||||
|
return {
|
||||||
|
x = gfx.animator.new(
|
||||||
|
0,
|
||||||
|
C.PitchStart.x,
|
||||||
|
getPitchMissBy(accuracy) + C.PitchStart.x,
|
||||||
|
playdate.easingFunctions.linear
|
||||||
|
),
|
||||||
|
y = gfx.animator.new(C.PitchFlyMs / 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
-- Curve ball
|
||||||
|
function(accuracy)
|
||||||
|
return {
|
||||||
|
x = gfx.animator.new(
|
||||||
|
C.PitchFlyMs,
|
||||||
|
getPitchMissBy(accuracy) + C.PitchStart.x + 20,
|
||||||
|
C.PitchStart.x,
|
||||||
|
utils.easingHill
|
||||||
|
),
|
||||||
|
y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
-- Slider
|
||||||
|
function(accuracy)
|
||||||
|
return {
|
||||||
|
x = gfx.animator.new(
|
||||||
|
C.PitchFlyMs,
|
||||||
|
getPitchMissBy(accuracy) + C.PitchStart.x - 20,
|
||||||
|
C.PitchStart.x,
|
||||||
|
utils.easingHill
|
||||||
|
),
|
||||||
|
y = gfx.animator.new(C.PitchFlyMs, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
-- Wobbleball
|
||||||
|
function(accuracy, ball)
|
||||||
|
local missBy = getPitchMissBy(accuracy)
|
||||||
|
return {
|
||||||
|
x = {
|
||||||
|
currentValue = function()
|
||||||
|
return missBy
|
||||||
|
+ C.PitchStart.x
|
||||||
|
+ (10 * math.sin((ball.yAnimator:currentValue() - C.PitchStart.y) / 10))
|
||||||
|
end,
|
||||||
|
reset = function() end,
|
||||||
|
},
|
||||||
|
y = gfx.animator.new(C.PitchFlyMs * 1.3, C.PitchStart.y, C.PitchEndY, playdate.easingFunctions.linear),
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@alias PitchOutcome "StrikeOut" | "Walk"
|
||||||
|
|
||||||
|
---@type table<string, PitchOutcome>
|
||||||
|
PitchOutcomes = {
|
||||||
|
StrikeOut = "StrikeOut",
|
||||||
|
Walk = "Walk",
|
||||||
|
}
|
||||||
|
|
||||||
|
pitchTracker = {
|
||||||
|
--- Position of the pitch, or nil, if one has not been recorded.
|
||||||
|
---@type number | nil
|
||||||
|
recordedPitchX = nil,
|
||||||
|
|
||||||
|
-- TODO: Replace with timer, repeatedly reset, instead of constantly setting to 0
|
||||||
|
secondsSinceLastPitch = 0,
|
||||||
|
|
||||||
|
strikes = 0,
|
||||||
|
balls = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
function pitchTracker:reset()
|
||||||
|
self.strikes = 0
|
||||||
|
self.balls = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param ball XyPair
|
||||||
|
function pitchTracker:recordIfPassed(ball)
|
||||||
|
if ball.y < C.StrikeZoneStartY then
|
||||||
|
self.recordedPitchX = nil
|
||||||
|
elseif not self.recordedPitchX then
|
||||||
|
self.recordedPitchX = ball.x
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param didSwing boolean
|
||||||
|
---@param fieldingTeamInningData TeamInningData
|
||||||
|
---@return PitchOutcome | nil
|
||||||
|
function pitchTracker:updatePitchCounts(didSwing, fieldingTeamInningData)
|
||||||
|
if not self.recordedPitchX then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local currentPitchingStats = fieldingTeamInningData.pitching
|
||||||
|
|
||||||
|
if didSwing or self.recordedPitchX > C.StrikeZoneStartX and self.recordedPitchX < C.StrikeZoneEndX then
|
||||||
|
self.strikes = self.strikes + 1
|
||||||
|
currentPitchingStats.strikes = currentPitchingStats.strikes + 1
|
||||||
|
if self.strikes >= 3 then
|
||||||
|
self:reset()
|
||||||
|
return PitchOutcomes.StrikeOut
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.balls = self.balls + 1
|
||||||
|
currentPitchingStats.balls = currentPitchingStats.balls + 1
|
||||||
|
if self.balls >= 4 then
|
||||||
|
self:reset()
|
||||||
|
return PitchOutcomes.Walk
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-----------------
|
||||||
|
-- Throw Meter --
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
throwMeter = {
|
||||||
|
MinCharge = 25,
|
||||||
|
|
||||||
|
idealPower = 50,
|
||||||
|
|
||||||
|
--- Used at draw-time only.
|
||||||
|
---@type number
|
||||||
|
lastReadThrow = nil,
|
||||||
|
|
||||||
|
--- Used at draw-time only.
|
||||||
|
---@type Fielder | nil
|
||||||
|
lastThrower = nil,
|
||||||
|
|
||||||
|
--- Used at draw-time only.
|
||||||
|
---@type boolean
|
||||||
|
wasPerfect = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
local MaxPowerRatio <const> = 1.5
|
||||||
|
|
||||||
|
--- Returns nil when a throw is NOT requested.
|
||||||
|
---@param chargeAmount number
|
||||||
|
---@return number | nil powerRatio, number | nil accuracy, boolean isPerfect
|
||||||
|
function throwMeter:readThrow(chargeAmount)
|
||||||
|
local power = self:readCharge(chargeAmount)
|
||||||
|
if not power then
|
||||||
|
return nil, nil, false
|
||||||
|
end
|
||||||
|
|
||||||
|
local ratio = math.min(power / self.idealPower, MaxPowerRatio)
|
||||||
|
self.wasPerfect = math.abs(ratio - 1) < 0.05
|
||||||
|
|
||||||
|
local accuracy = 1
|
||||||
|
-- Only throw off accuracy on slow throws
|
||||||
|
if ratio >= 1 and not self.wasPerfect then
|
||||||
|
accuracy = 1 / ratio
|
||||||
|
end
|
||||||
|
|
||||||
|
return ratio * 1.5, accuracy, self.wasPerfect
|
||||||
|
end
|
||||||
|
|
||||||
|
local CrankRecordSec <const> = 0.33
|
||||||
|
|
||||||
|
---@alias CrankQueueEntry { time: number, chargeAmount: number }
|
||||||
|
|
||||||
|
---@type CrankQueueEntry[]
|
||||||
|
local crankQueue = {}
|
||||||
|
|
||||||
|
--- If (within approx. a third of a second) the crank has moved more than 45 degrees, call that a throw.
|
||||||
|
---@param chargeAmount number
|
||||||
|
---@return number | nil
|
||||||
|
function throwMeter:readCharge(chargeAmount)
|
||||||
|
if chargeAmount == 0 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local currentTimeMs = playdate.getCurrentTimeMilliseconds()
|
||||||
|
local minTimeHasPassed = false
|
||||||
|
while #crankQueue ~= 0 and (currentTimeMs - crankQueue[1].time) > CrankRecordSec do
|
||||||
|
table.remove(crankQueue, 1)
|
||||||
|
minTimeHasPassed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
crankQueue[#crankQueue + 1] = { time = currentTimeMs, chargeAmount = math.abs(chargeAmount) }
|
||||||
|
|
||||||
|
if not minTimeHasPassed then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local currentCharge = 0
|
||||||
|
for _, v in ipairs(crankQueue) do
|
||||||
|
currentCharge = currentCharge + v.chargeAmount
|
||||||
|
end
|
||||||
|
|
||||||
|
if currentCharge > throwMeter.MinCharge then
|
||||||
|
self.lastReadThrow = currentCharge
|
||||||
|
crankQueue = {}
|
||||||
|
return currentCharge
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,83 +0,0 @@
|
||||||
local ScoreFont <const> = playdate.graphics.font.new("fonts/font-full-circle.pft")
|
|
||||||
local OutBubbleRadius <const> = 5
|
|
||||||
local ScoreboardMarginX <const> = 6
|
|
||||||
local ScoreboardMarginRight <const> = 4
|
|
||||||
local ScoreboardHeight <const> = 55
|
|
||||||
local Indicator = "> "
|
|
||||||
local IndicatorWidth <const> = ScoreFont:getTextWidth(Indicator)
|
|
||||||
|
|
||||||
local BallStrikeMarginY <const> = 4
|
|
||||||
local BallStrikeWidth <const> = 60
|
|
||||||
local BallStrikeHeight <const> = (BallStrikeMarginY * 2) + ScoreFont:getHeight()
|
|
||||||
|
|
||||||
---@param teams any
|
|
||||||
---@param battingTeam any
|
|
||||||
---@return string, number, string, number
|
|
||||||
function getIndicators(teams, battingTeam)
|
|
||||||
if teams.home == battingTeam then
|
|
||||||
return Indicator, 0, "", IndicatorWidth
|
|
||||||
end
|
|
||||||
return "", IndicatorWidth, Indicator, 0
|
|
||||||
end
|
|
||||||
|
|
||||||
function drawBallsAndStrikes(x, y, balls, strikes)
|
|
||||||
if balls == 0 and strikes == 0 then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local gfx = playdate.graphics
|
|
||||||
gfx.setColor(gfx.kColorBlack)
|
|
||||||
gfx.fillRect(x, y, BallStrikeWidth, BallStrikeHeight)
|
|
||||||
local originalDrawMode = gfx.getImageDrawMode()
|
|
||||||
gfx.setImageDrawMode(gfx.kDrawModeInverted)
|
|
||||||
|
|
||||||
local text = tostring(balls) .. " - " .. tostring(strikes)
|
|
||||||
local textWidth = ScoreFont:getTextWidth(text)
|
|
||||||
local widthDiff = BallStrikeWidth - textWidth
|
|
||||||
ScoreFont:drawText(text, x + (widthDiff / 2), y + BallStrikeMarginY)
|
|
||||||
gfx.setImageDrawMode(originalDrawMode)
|
|
||||||
end
|
|
||||||
|
|
||||||
function drawScoreboard(x, y, teams, outs, battingTeam, inning)
|
|
||||||
local gfx = playdate.graphics
|
|
||||||
local homeScore = teams.home.score
|
|
||||||
local awayScore = teams.away.score
|
|
||||||
|
|
||||||
local homeIndicator, homeOffset, awayIndicator, awayOffset = getIndicators(teams, battingTeam)
|
|
||||||
|
|
||||||
local homeScoreText = homeIndicator .. "HOME " .. (homeScore > 9 and homeScore or " " .. homeScore)
|
|
||||||
local awayScoreText = awayIndicator .. "AWAY " .. (awayScore > 9 and awayScore or " " .. awayScore)
|
|
||||||
|
|
||||||
local rectWidth = (ScoreboardMarginX * 2)
|
|
||||||
+ ScoreboardMarginRight
|
|
||||||
+ ScoreFont:getTextWidth(homeScoreText)
|
|
||||||
+ homeOffset
|
|
||||||
|
|
||||||
gfx.setLineWidth(1)
|
|
||||||
gfx.setColor(gfx.kColorBlack)
|
|
||||||
gfx.fillRect(x, y, rectWidth, ScoreboardHeight)
|
|
||||||
|
|
||||||
local originalDrawMode = gfx.getImageDrawMode()
|
|
||||||
gfx.setImageDrawMode(gfx.kDrawModeInverted)
|
|
||||||
|
|
||||||
ScoreFont:drawText(homeScoreText, x + ScoreboardMarginX + homeOffset, y + 6)
|
|
||||||
ScoreFont:drawText(awayScoreText, x + ScoreboardMarginX + awayOffset, y + 22)
|
|
||||||
local inningOffsetX = (x + ScoreboardMarginX + IndicatorWidth) + (4 * 2.5 * OutBubbleRadius)
|
|
||||||
ScoreFont:drawText(inning, inningOffsetX, y + 39)
|
|
||||||
|
|
||||||
gfx.setImageDrawMode(originalDrawMode)
|
|
||||||
|
|
||||||
gfx.setColor(gfx.kColorWhite)
|
|
||||||
|
|
||||||
function circleParams(i)
|
|
||||||
local circleOffset = i * 2.5 * OutBubbleRadius
|
|
||||||
return (x + ScoreboardMarginX + OutBubbleRadius + IndicatorWidth) + circleOffset, y + 46, OutBubbleRadius
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = outs, 2 do
|
|
||||||
gfx.drawCircleAtPoint(circleParams(i))
|
|
||||||
end
|
|
||||||
for i = 0, (outs - 1) do
|
|
||||||
gfx.fillCircleAtPoint(circleParams(i))
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
-- TODO? Some other stats
|
||||||
|
-- * Scroll left and right through games that go into extra innings
|
||||||
|
-- * Scroll up and down through other stats.
|
||||||
|
-- + Balls and strikes
|
||||||
|
-- + Batting average
|
||||||
|
-- + Farthest hit ball
|
||||||
|
|
||||||
|
---@return TeamInningData
|
||||||
|
local function newTeamInning()
|
||||||
|
return {
|
||||||
|
score = 0,
|
||||||
|
pitching = {
|
||||||
|
balls = 0,
|
||||||
|
strikes = 0,
|
||||||
|
},
|
||||||
|
hits = {},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return table<TeamId, TeamInningData>
|
||||||
|
local function newInning()
|
||||||
|
return {
|
||||||
|
home = newTeamInning(),
|
||||||
|
away = newTeamInning(),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@alias TeamInningData { score: number, pitching: { balls: number, strikes: number }, hits: XyPair[] }
|
||||||
|
|
||||||
|
--- E.g. statistics[1].home.pitching.balls
|
||||||
|
---@class Statistics
|
||||||
|
---@field innings (table<TeamId, TeamInningData>)[]
|
||||||
|
Statistics = {}
|
||||||
|
|
||||||
|
---@return Statistics
|
||||||
|
function Statistics.new()
|
||||||
|
return setmetatable({
|
||||||
|
innings = { newInning() },
|
||||||
|
}, { __index = Statistics })
|
||||||
|
end
|
||||||
|
|
||||||
|
function Statistics:pushInning()
|
||||||
|
self.innings[#self.innings + 1] = newInning()
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param inning number
|
||||||
|
---@param finalInning number
|
||||||
|
---@param battingTeam TeamId
|
||||||
|
---@return boolean gameOver
|
||||||
|
function Statistics:gameIsOver(inning, finalInning, battingTeam)
|
||||||
|
local homeScore, awayScore = utils.totalScores(self)
|
||||||
|
local isFinalInning = inning >= finalInning
|
||||||
|
local gameOver = isFinalInning and battingTeam == "home" and awayScore ~= homeScore
|
||||||
|
gameOver = gameOver or battingTeam == "away" and isFinalInning and homeScore > awayScore
|
||||||
|
return gameOver
|
||||||
|
end
|
||||||
|
|
||||||
|
-- luacheck: ignore
|
||||||
|
if not playdate or playdate.TEST_MODE then
|
||||||
|
return Statistics
|
||||||
|
end
|
|
@ -0,0 +1,76 @@
|
||||||
|
utils = require("utils")
|
||||||
|
|
||||||
|
local currentTimeMs = 0
|
||||||
|
|
||||||
|
local mockPlaydate = {}
|
||||||
|
|
||||||
|
mockPlaydate = {
|
||||||
|
TEST_MODE = true,
|
||||||
|
skipMs = function(skip)
|
||||||
|
currentTimeMs = currentTimeMs + skip
|
||||||
|
end,
|
||||||
|
getCurrentTimeMilliseconds = function()
|
||||||
|
currentTimeMs = currentTimeMs + 1
|
||||||
|
return currentTimeMs
|
||||||
|
end,
|
||||||
|
easingFunctions = {},
|
||||||
|
timer = {
|
||||||
|
lastTimer = {
|
||||||
|
mockCompletion = function()
|
||||||
|
error("No lastTimer set!")
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
new = function(_, callback)
|
||||||
|
local timer = {
|
||||||
|
mockCompletion = function()
|
||||||
|
callback()
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
mockPlaydate.timer.lastTimer = timer
|
||||||
|
return timer
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
graphics = {
|
||||||
|
animator = {
|
||||||
|
new = function()
|
||||||
|
return utils.staticAnimator(0)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
animation = {
|
||||||
|
blinker = {
|
||||||
|
new = function()
|
||||||
|
return { start = function() end }
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
font = {
|
||||||
|
new = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
image = {
|
||||||
|
new = function()
|
||||||
|
return {}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sound = {
|
||||||
|
sampleplayer = {
|
||||||
|
new = function()
|
||||||
|
return {
|
||||||
|
play = function() end,
|
||||||
|
setFinishCallback = function() end,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
---@type Announcer
|
||||||
|
local mockAnnouncer = {
|
||||||
|
say = function(self, message)
|
||||||
|
self.lastMessage = message
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
return { mockPlaydate, mockAnnouncer }
|
|
@ -0,0 +1,38 @@
|
||||||
|
import = function() end
|
||||||
|
luaunit = require("luaunit")
|
||||||
|
luaunit.ORDER_ACTUAL_EXPECTED = false
|
||||||
|
|
||||||
|
utils = require("utils")
|
||||||
|
C = require("constants")
|
||||||
|
local mocks = require("test/mocks")
|
||||||
|
playdate, announcer = mocks[1], mocks[2]
|
||||||
|
|
||||||
|
local _f = require("fielding")
|
||||||
|
Fielding, newFielder = _f[1], _f[2]
|
||||||
|
|
||||||
|
HomeTeamSpriteGroup = {}
|
||||||
|
|
||||||
|
-- Print contents of `tbl`, with indentation.
|
||||||
|
-- `indent` sets the initial level of indentation.
|
||||||
|
function str(tbl, indent, nl)
|
||||||
|
if not indent then
|
||||||
|
indent = 1
|
||||||
|
end
|
||||||
|
nl = nl or "\n"
|
||||||
|
|
||||||
|
if type(tbl) == "table" then
|
||||||
|
local indentStr = string.rep(" ", indent)
|
||||||
|
local ret = "{" .. nl
|
||||||
|
for k, v in pairs(tbl) do
|
||||||
|
--ret = ret .. indentStr .. "[" .. str(k, -9999, "") .. "]" .. ": " .. str(v, indent + 1, nl) .. "," .. nl
|
||||||
|
ret = ret .. indentStr .. "[" .. tostring(k) .. "]" .. ": " .. tostring(v) .. "," .. nl
|
||||||
|
end
|
||||||
|
return ret .. indentStr .. nl .. "}"
|
||||||
|
else
|
||||||
|
return tostring(tbl)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function printTable(tbl)
|
||||||
|
print(str(tbl))
|
||||||
|
end
|
|
@ -0,0 +1,87 @@
|
||||||
|
require("test/setup")
|
||||||
|
require("action-queue")
|
||||||
|
|
||||||
|
function testActionQueueRunsToCompletion()
|
||||||
|
actionQueue.queue = {}
|
||||||
|
local invokeTotalSec = 0
|
||||||
|
local hasYielded = false
|
||||||
|
actionQueue:upsert("testAction", 9999999999, function(delta)
|
||||||
|
while invokeTotalSec < 5 do
|
||||||
|
invokeTotalSec = invokeTotalSec + delta
|
||||||
|
hasYielded = true
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
luaunit.assertIsFalse(hasYielded, "Should not have been invoked yet.")
|
||||||
|
for _ = 1, 10 do
|
||||||
|
actionQueue:runWaiting(1)
|
||||||
|
luaunit.assertIsTrue(hasYielded, "Should have been invoked.")
|
||||||
|
end
|
||||||
|
luaunit.assertEquals(5, invokeTotalSec, "Should have run five times and stopped itself")
|
||||||
|
end
|
||||||
|
|
||||||
|
function testActionQueueExpiration()
|
||||||
|
actionQueue.queue = {}
|
||||||
|
local yieldCount = 0
|
||||||
|
actionQueue:upsert("testAction", 2000, function()
|
||||||
|
while true do
|
||||||
|
yieldCount = yieldCount + 1
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
local skipSec = 60
|
||||||
|
playdate.skipMs(60 * 1000)
|
||||||
|
actionQueue:runWaiting(skipSec)
|
||||||
|
luaunit.assertEquals(1, yieldCount, "Should always be invoked at least once")
|
||||||
|
|
||||||
|
playdate.skipMs(1000)
|
||||||
|
actionQueue:runWaiting(1)
|
||||||
|
luaunit.assertEquals(1, yieldCount, "Should not be invoked again after expiry")
|
||||||
|
end
|
||||||
|
|
||||||
|
function testDuplicateUpsertsShouldOnlyRunOnce()
|
||||||
|
actionQueue.queue = {}
|
||||||
|
local yieldCount = 0
|
||||||
|
local yieldId
|
||||||
|
local action = function(id)
|
||||||
|
return function()
|
||||||
|
while true do
|
||||||
|
yieldCount = yieldCount + 1
|
||||||
|
yieldId = id
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, 10 do
|
||||||
|
actionQueue:upsert("testAction", 9999999999, action(i))
|
||||||
|
end
|
||||||
|
actionQueue:runWaiting(1)
|
||||||
|
luaunit.assertEquals(1, yieldCount, "Duplicate upserts should result in only one invocation.")
|
||||||
|
luaunit.assertEquals(10, yieldId, "Most recent upsert should take precedence.")
|
||||||
|
end
|
||||||
|
|
||||||
|
function testNewOnlyActionsShouldNotReplaceExistingActions()
|
||||||
|
actionQueue.queue = {}
|
||||||
|
local yieldCount = 0
|
||||||
|
local yieldId
|
||||||
|
local action = function(id)
|
||||||
|
return function()
|
||||||
|
while true do
|
||||||
|
yieldCount = yieldCount + 1
|
||||||
|
yieldId = id
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, 10 do
|
||||||
|
actionQueue:newOnly("testAction", 9999999999, action(i))
|
||||||
|
end
|
||||||
|
actionQueue:runWaiting(1)
|
||||||
|
luaunit.assertEquals(1, yieldCount, "Duplicate newOnly should result in only one invocation.")
|
||||||
|
luaunit.assertEquals(1, yieldId, "The first newOnly should take precedence.")
|
||||||
|
end
|
||||||
|
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
|
@ -0,0 +1,14 @@
|
||||||
|
require("test/setup")
|
||||||
|
|
||||||
|
local Ball = require("ball")
|
||||||
|
|
||||||
|
function testMarkUncatchable()
|
||||||
|
local ball = Ball.new(playdate.graphics.animator)
|
||||||
|
luaunit.assertIsTrue(ball.catchable, "Ball should start catchable")
|
||||||
|
ball:markUncatchable()
|
||||||
|
luaunit.assertIsFalse(ball.catchable, "Ball should not be catchable immediately after mark")
|
||||||
|
playdate.timer.lastTimer.mockCompletion()
|
||||||
|
luaunit.assertIsTrue(ball.catchable, "Ball should return to catchability after its timer expires")
|
||||||
|
end
|
||||||
|
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
|
@ -0,0 +1,173 @@
|
||||||
|
require("test/setup")
|
||||||
|
|
||||||
|
local Baserunning = require("baserunning")
|
||||||
|
|
||||||
|
---@return Baserunning, { called: boolean }
|
||||||
|
function buildBaserunning()
|
||||||
|
local thirdOutCallbackData = { called = false }
|
||||||
|
local baserunning = Baserunning.new(announcer, function()
|
||||||
|
thirdOutCallbackData.called = true
|
||||||
|
end)
|
||||||
|
return baserunning, thirdOutCallbackData
|
||||||
|
end
|
||||||
|
|
||||||
|
---@alias BaseIndexOrXyPair (number | XyPair)
|
||||||
|
|
||||||
|
--- NOTE: in addition to the given runners, there is implicitly a batter running from first.
|
||||||
|
---@param runnerLocations BaseIndexOrXyPair[]
|
||||||
|
---@return Baserunning
|
||||||
|
function buildRunnersOn(runnerLocations)
|
||||||
|
local baserunning = buildBaserunning()
|
||||||
|
baserunning:convertBatterToRunner()
|
||||||
|
for _, location in ipairs(runnerLocations) do
|
||||||
|
baserunning:pushNewBatter()
|
||||||
|
local runner = baserunning.batter
|
||||||
|
baserunning:convertBatterToRunner()
|
||||||
|
if type(location) == "number" then
|
||||||
|
-- Is a base index
|
||||||
|
-- Push the runner *through* each base.
|
||||||
|
for b = 1, location do
|
||||||
|
runner.x = C.Bases[b].x
|
||||||
|
runner.y = C.Bases[b].y
|
||||||
|
baserunning:updateNonBatterRunners(0.001, false, false, 0.001)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
-- Is a raw XyPair
|
||||||
|
runner.x = location.x
|
||||||
|
runner.y = location.y
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return baserunning
|
||||||
|
end
|
||||||
|
|
||||||
|
---@alias Condition { fielderWithBallAt: XyPair, outWhen: BaseIndexOrXyPair[][], safeWhen: BaseIndexOrXyPair[][] }
|
||||||
|
|
||||||
|
---@param expectedOuts number
|
||||||
|
---@param fielderWithBallAt XyPair
|
||||||
|
---@param when number[][]
|
||||||
|
function assertRunnerOutCondition(expectedOuts, when, fielderWithBallAt)
|
||||||
|
for _, runnersOn in ipairs(when) do
|
||||||
|
local baserunning = buildRunnersOn(runnersOn)
|
||||||
|
baserunning:outEligibleRunners(fielderWithBallAt)
|
||||||
|
luaunit.assertEquals(expectedOuts, baserunning.outs, "Incorrect number of outs.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param condition Condition
|
||||||
|
function assertRunnerStatuses(condition)
|
||||||
|
assertRunnerOutCondition(1, condition.outWhen, condition.fielderWithBallAt)
|
||||||
|
assertRunnerOutCondition(0, condition.safeWhen, condition.fielderWithBallAt)
|
||||||
|
end
|
||||||
|
|
||||||
|
function testForceOutsAtFirst()
|
||||||
|
assertRunnerStatuses({
|
||||||
|
fielderWithBallAt = C.Bases[C.First],
|
||||||
|
outWhen = {
|
||||||
|
{},
|
||||||
|
{ 1 },
|
||||||
|
{ 2 },
|
||||||
|
{ 3 },
|
||||||
|
{ 1, 2 },
|
||||||
|
{ 1, 3 },
|
||||||
|
{ 2, 3 },
|
||||||
|
{ 1, 2, 3 },
|
||||||
|
},
|
||||||
|
safeWhen = {},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function testForceOutsAtSecond()
|
||||||
|
assertRunnerStatuses({
|
||||||
|
fielderWithBallAt = C.Bases[C.Second],
|
||||||
|
outWhen = {
|
||||||
|
{ 1 },
|
||||||
|
{ 1, 2 },
|
||||||
|
{ 1, 3 },
|
||||||
|
{ 1, 2, 3 },
|
||||||
|
},
|
||||||
|
safeWhen = {
|
||||||
|
{},
|
||||||
|
{ 2 },
|
||||||
|
{ 3 },
|
||||||
|
{ 2, 3 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function testForceOutsAtThird()
|
||||||
|
assertRunnerStatuses({
|
||||||
|
fielderWithBallAt = C.Bases[C.Third],
|
||||||
|
outWhen = {
|
||||||
|
{ 1, 2 },
|
||||||
|
{ 1, 2, 3 },
|
||||||
|
},
|
||||||
|
safeWhen = {
|
||||||
|
{ 1 },
|
||||||
|
{ 2 },
|
||||||
|
{ 3 },
|
||||||
|
{ 2, 3 },
|
||||||
|
{ 1, 3 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function testForceOutsAtHome()
|
||||||
|
assertRunnerStatuses({
|
||||||
|
fielderWithBallAt = C.Bases[C.Home],
|
||||||
|
outWhen = {
|
||||||
|
{ 1, 2, 3 },
|
||||||
|
},
|
||||||
|
safeWhen = {
|
||||||
|
{},
|
||||||
|
{ 1 },
|
||||||
|
{ 2 },
|
||||||
|
{ 3 },
|
||||||
|
{ 1, 2 },
|
||||||
|
{ 1, 3 },
|
||||||
|
{ 2, 3 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function testTagOutsShouldHappenOffBase()
|
||||||
|
local fielderWithBallAt = utils.xy(10, 10) -- Some location not on a base.
|
||||||
|
local farFromFielder = utils.xy(100, 100)
|
||||||
|
assertRunnerStatuses({
|
||||||
|
fielderWithBallAt = fielderWithBallAt,
|
||||||
|
outWhen = {
|
||||||
|
{ fielderWithBallAt },
|
||||||
|
},
|
||||||
|
safeWhen = {
|
||||||
|
{ farFromFielder },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function testTagOutsShouldNotHappenOnBase()
|
||||||
|
assertRunnerStatuses({
|
||||||
|
fielderWithBallAt = C.Bases[C.Third],
|
||||||
|
outWhen = {},
|
||||||
|
safeWhen = {
|
||||||
|
{ 2 },
|
||||||
|
{ 3 },
|
||||||
|
{ 2, 3 },
|
||||||
|
{ 2, 3, 4 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function testTagOutsWithMultipleRunnersOnOneBase()
|
||||||
|
assertRunnerStatuses({
|
||||||
|
fielderWithBallAt = C.Bases[C.Third],
|
||||||
|
outWhen = {
|
||||||
|
{ 3, 3 },
|
||||||
|
},
|
||||||
|
safeWhen = {
|
||||||
|
{ 1, 1 },
|
||||||
|
{ 2, 2 },
|
||||||
|
{ 4, 4 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|