diff --git a/Makefile b/Makefile index 0af9a72..81bca62 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,8 @@ all: pdc src BatterUp.pdx check: - stylua -c 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///g' | selene - lint: - stylua src/ + stylua --indent-type Spaces src/ diff --git a/src/announcer.lua b/src/announcer.lua index e1ae328..e7ce4f2 100644 --- a/src/announcer.lua +++ b/src/announcer.lua @@ -3,60 +3,60 @@ local AnnouncementTransitionMs = 300 local AnnouncerMarginX = 26 local AnnouncerAnimatorInY = - playdate.graphics.animator.new(AnnouncementTransitionMs, -70, 0, playdate.easingFunctions.outBounce) + playdate.graphics.animator.new(AnnouncementTransitionMs, -70, 0, playdate.easingFunctions.outBounce) local AnnouncerAnimatorOutY = - playdate.graphics.animator.new(AnnouncementTransitionMs, 0, -70, playdate.easingFunctions.outQuint) + playdate.graphics.animator.new(AnnouncementTransitionMs, 0, -70, playdate.easingFunctions.outQuint) -- selene: allow(unscoped_variables) announcer = { - textQueue = {}, - animatorY = AnnouncerAnimatorInY, + textQueue = {}, + animatorY = AnnouncerAnimatorInY, } local DurationMs = 3000 function announcer.popIn(self) - self.animatorY = AnnouncerAnimatorInY - self.animatorY:reset() + self.animatorY = AnnouncerAnimatorInY + self.animatorY:reset() - playdate.timer.new(DurationMs, function() - self.animatorY = AnnouncerAnimatorOutY - self.animatorY:reset() - -- If this popIn() call was inside a timer, successive messages would be - -- allowed to transition out. However, the Out animation, shortly followed by - -- a new message popping in, is actually *more* jarring than the interrupt. - if #self.textQueue ~= 1 then - self:popIn() - table.remove(self.textQueue, 1) - else - playdate.timer.new(AnnouncementTransitionMs, function() - table.remove(self.textQueue, 1) - end) - end - end) + playdate.timer.new(DurationMs, function() + self.animatorY = AnnouncerAnimatorOutY + self.animatorY:reset() + -- If this popIn() call was inside a timer, successive messages would be + -- allowed to transition out. However, the Out animation, shortly followed by + -- a new message popping in, is actually *more* jarring than the interrupt. + if #self.textQueue ~= 1 then + self:popIn() + table.remove(self.textQueue, 1) + else + playdate.timer.new(AnnouncementTransitionMs, function() + table.remove(self.textQueue, 1) + end) + end + end) end function announcer.say(self, text) - self.textQueue[#self.textQueue + 1] = text - if #self.textQueue == 1 then - self:popIn() - end + self.textQueue[#self.textQueue + 1] = text + if #self.textQueue == 1 then + self:popIn() + end end function announcer.draw(self, x, y) - if #self.textQueue == 0 then - return - end - x = x - 5 -- Infield center is slightly offset from screen center + if #self.textQueue == 0 then + return + end + x = x - 5 -- Infield center is slightly offset from screen center - local gfx = playdate.graphics - local originalDrawMode = gfx.getImageDrawMode() - local width = math.max(150, (AnnouncerMarginX * 2) + AnnouncementFont:getTextWidth(self.textQueue[1])) - local animY = self.animatorY:currentValue() + local gfx = playdate.graphics + local originalDrawMode = gfx.getImageDrawMode() + local width = math.max(150, (AnnouncerMarginX * 2) + AnnouncementFont:getTextWidth(self.textQueue[1])) + local animY = self.animatorY:currentValue() - gfx.setColor(gfx.kColorBlack) - gfx.fillRect(x - (width / 2), y + animY, width, 50) - gfx.setImageDrawMode(gfx.kDrawModeInverted) - AnnouncementFont:drawTextAligned(self.textQueue[1], x, y + 10 + animY, kTextAlignment.center) - gfx.setImageDrawMode(originalDrawMode) + gfx.setColor(gfx.kColorBlack) + gfx.fillRect(x - (width / 2), y + animY, width, 50) + gfx.setImageDrawMode(gfx.kDrawModeInverted) + AnnouncementFont:drawTextAligned(self.textQueue[1], x, y + 10 + animY, kTextAlignment.center) + gfx.setImageDrawMode(originalDrawMode) end diff --git a/src/ecs.lua b/src/ecs.lua index 9b72e6f..e0e75fc 100644 --- a/src/ecs.lua +++ b/src/ecs.lua @@ -9,46 +9,46 @@ local systems = {} -- 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 + 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 + 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 + return Placeholder end function allKeysIncluded(entity, filter) - for k, _ in pairs(filter) do - if not entity[k] then - return false - end - end - return true + 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 + for _, shape in pairs(shapes) do + if not allKeysIncluded(entity, shape) then + return false + end + end + return true end ---@generic T @@ -61,66 +61,66 @@ end ---@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 + 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 + 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, - } + systems[#systems + 1] = { + callback = callback, + keys = keys, + shapes = shapes, + entityCache = nil, + } end ---@return boolean function is(entity, shape) - return allKeysIncluded(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 + 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. @@ -139,26 +139,26 @@ end ---@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 + 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 + 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 + local keys = {} + for _, shape in pairs(shapes) do + for key, _ in pairs(shape) do + keys[#keys + 1] = key + end + end - addSystem(callback, keys, shapes) + addSystem(callback, keys, shapes) end local f = ecs.field() @@ -168,15 +168,15 @@ 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) + 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 }, + position = { x = 0, y = 0 }, + velocity = { x = 1, y = 2 }, } ---@generic T @@ -184,7 +184,7 @@ local data = { ---@param entity unknown ---@return T function ecs.get(shape, entity) - return entity + return entity end ---@generic T @@ -192,29 +192,29 @@ end ---@param shape `T` ---@param value `T` function ecs.set(entity, shape, value) - for key, v in pairs(shape) do - entity[key] = value[v] - end + 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} - }) + 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") + 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) diff --git a/src/graphics.lua b/src/graphics.lua index 43ec415..5c3a7cd 100644 --- a/src/graphics.lua +++ b/src/graphics.lua @@ -3,21 +3,21 @@ --- XOX --- Where each character is the size of the screen, and 'O' is the default view. function getDrawOffset(screenW, screenH, ballX, ballY) - local offsetX, offsetY - if ballY > screenH then - return 0, 0 - end - offsetY = math.max(0, -1 * ballY) + local offsetX, offsetY + if ballY > screenH then + return 0, 0 + end + offsetY = math.max(0, -1 * ballY) - if ballX > 0 and ballX < screenW then - offsetX = 0 - elseif ballX < 0 then - offsetX = math.max(-1 * screenW, ballX * -1) - elseif ballX > screenW then - offsetX = math.min(screenW * 2, (ballX * -1) + screenW) - end + if ballX > 0 and ballX < screenW then + offsetX = 0 + elseif ballX < 0 then + offsetX = math.max(-1 * screenW, ballX * -1) + elseif ballX > screenW then + offsetX = math.min(screenW * 2, (ballX * -1) + screenW) + end - return offsetX * 1.3, offsetY * 1.5 + return offsetX * 1.3, offsetY * 1.5 end -- selene: allow(unscoped_variables) @@ -26,15 +26,15 @@ blipper = {} --- 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. function blipper.new(msInterval, imagePath1, imagePath2) - local blinker = playdate.graphics.animation.blinker.new(msInterval, msInterval, true) - blinker:start() - return { - blinker = blinker, - image1 = playdate.graphics.image.new(imagePath1), - image2 = playdate.graphics.image.new(imagePath2), - draw = function(self, disableBlipping, x, y) - local currentImage = (disableBlipping or self.blinker.on) and self.image2 or self.image1 - currentImage:draw(x, y) - end, - } + local blinker = playdate.graphics.animation.blinker.new(msInterval, msInterval, true) + blinker:start() + return { + blinker = blinker, + image1 = playdate.graphics.image.new(imagePath1), + image2 = playdate.graphics.image.new(imagePath2), + draw = function(self, disableBlipping, x, y) + local currentImage = (disableBlipping or self.blinker.on) and self.image2 or self.image1 + currentImage:draw(x, y) + end, + } end diff --git a/src/main.lua b/src/main.lua index 3eaa76b..2d8c7d6 100644 --- a/src/main.lua +++ b/src/main.lua @@ -41,8 +41,8 @@ import 'utils.lua' local gfx = playdate.graphics local Screen = { - W = playdate.display.getWidth(), - H = playdate.display.getHeight(), + W = playdate.display.getWidth(), + H = playdate.display.getHeight(), } local Center = xy(Screen.W / 2, Screen.H / 2) @@ -65,7 +65,7 @@ FielderDanceAnimator.repeatCount = DanceBounceCount - 1 -- selene: allow(unused_variable) function fieldersDance() - FielderDanceAnimator:reset(DanceBounceMs) + FielderDanceAnimator:reset(DanceBounceMs) end local BallOffscreen = 999 @@ -78,31 +78,31 @@ local ballAnimatorY = gfx.animator.new(0, BallOffscreen, BallOffscreen, playdate local ballAnimatorX = gfx.animator.new(0, BallOffscreen, BallOffscreen, playdate.easingFunctions.linear) local Pitches = { - -- Fastball - { - x = gfx.animator.new(0, PitchStartX, PitchStartX, playdate.easingFunctions.linear), - y = gfx.animator.new(PitchFlyMs / 1.3, PitchStartY, PitchEndY, playdate.easingFunctions.linear), - }, - -- Slider - { - x = gfx.animator.new(PitchFlyMs, PitchStartX - 20, PitchStartX, easingHill), - y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear), - }, - -- Curve ball - { - x = gfx.animator.new(PitchFlyMs, PitchStartX + 20, PitchStartX, easingHill), - y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear), - }, - -- Wobbbleball - { - x = { - currentValue = function() - return PitchStartX + (10 * math.sin((ballAnimatorY:currentValue() - PitchStartY) / 10)) - end, - reset = function() end, - }, - y = gfx.animator.new(PitchFlyMs * 1.3, PitchStartY, PitchEndY, playdate.easingFunctions.linear), - }, + -- Fastball + { + x = gfx.animator.new(0, PitchStartX, PitchStartX, playdate.easingFunctions.linear), + y = gfx.animator.new(PitchFlyMs / 1.3, PitchStartY, PitchEndY, playdate.easingFunctions.linear), + }, + -- Slider + { + x = gfx.animator.new(PitchFlyMs, PitchStartX - 20, PitchStartX, easingHill), + y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear), + }, + -- Curve ball + { + x = gfx.animator.new(PitchFlyMs, PitchStartX + 20, PitchStartX, easingHill), + y = gfx.animator.new(PitchFlyMs, PitchStartY, PitchEndY, playdate.easingFunctions.linear), + }, + -- Wobbbleball + { + x = { + currentValue = function() + return PitchStartX + (10 * math.sin((ballAnimatorY:currentValue() - PitchStartY) / 10)) + end, + reset = function() end, + }, + y = gfx.animator.new(PitchFlyMs * 1.3, PitchStartY, PitchEndY, playdate.easingFunctions.linear), + }, } local CrankOffsetDeg = 90 @@ -116,17 +116,17 @@ local TagDistance = 20 local SmallestBallRadius = 6 local ball = { - x = Center.x --[[@as number]], - y = Center.y --[[@as number]], - size = SmallestBallRadius, - heldBy = nil --[[@type Runner | nil]], + x = Center.x --[[@as number]], + y = Center.y --[[@as number]], + size = SmallestBallRadius, + heldBy = nil --[[@type Runner | nil]], } local BatLength = 50 --45 local Modes = { - batting = {}, - running = {}, + batting = {}, + running = {}, } local currentMode = Modes.batting @@ -145,10 +145,10 @@ local First , Second , Third , Home = 1, 2, 3, 4 ---@type Base[] local Bases = { - xy(Screen.W * 0.93, Screen.H * 0.52), - xy(Screen.W * 0.47, Screen.H * 0.19), - xy(Screen.W * 0.03, Screen.H * 0.52), - xy(Screen.W * 0.474, Screen.H * 0.79), + xy(Screen.W * 0.93, Screen.H * 0.52), + xy(Screen.W * 0.47, Screen.H * 0.19), + xy(Screen.W * 0.03, Screen.H * 0.52), + xy(Screen.W * 0.474, Screen.H * 0.79), } -- Pseudo-base for batter to target @@ -156,55 +156,55 @@ local RightHandedBattersBox = xy(Bases[Home].x - 35, Bases[Home].y) ---@type table local NextBaseMap = { - [Bases[First]] = Bases[Second], - [Bases[Second]] = Bases[Third], - [Bases[Third]] = Bases[Home], + [Bases[First]] = Bases[Second], + [Bases[Second]] = Bases[Third], + [Bases[Third]] = Bases[Home], } function newFielder(name, speed) - return { - name = name, - speed = speed, - } + return { + name = name, + speed = speed, + } end ---@type table local fielders = { - first = newFielder("First", 40), - second = newFielder("Second", 40), - shortstop = newFielder("Shortstop", 40), - third = newFielder("Third", 40), - pitcher = newFielder("Pitcher", 30), - catcher = newFielder("Catcher", 20), - left = newFielder("Left", 40), - center = newFielder("Center", 40), - right = newFielder("Right", 40), + first = newFielder("First", 40), + second = newFielder("Second", 40), + shortstop = newFielder("Shortstop", 40), + third = newFielder("Third", 40), + pitcher = newFielder("Pitcher", 30), + catcher = newFielder("Catcher", 20), + left = newFielder("Left", 40), + center = newFielder("Center", 40), + right = newFielder("Right", 40), } local PitcherStartPos = { - x = Screen.W * 0.48, - y = Screen.H * 0.40, + x = Screen.W * 0.48, + y = Screen.H * 0.40, } --- Resets the target positions of all fielders to their defaults (at their field positions). ---@param fromOffTheField boolean If provided, also sets all runners' current position to one centralized location. function resetFielderPositions(fromOffTheField) - if fromOffTheField then - for _, fielder in pairs(fielders) do - fielder.x = Center.x - fielder.y = Screen.H - end - end + if fromOffTheField then + for _, fielder in pairs(fielders) do + fielder.x = Center.x + fielder.y = Screen.H + end + end - fielders.first.target = xy(Screen.W - 65, Screen.H * 0.48) - fielders.second.target = xy(Screen.W * 0.70, Screen.H * 0.30) - fielders.shortstop.target = xy(Screen.W * 0.30, Screen.H * 0.30) - fielders.third.target = xy(Screen.W * 0.1, Screen.H * 0.48) - fielders.pitcher.target = xy(PitcherStartPos.x, PitcherStartPos.y) - fielders.catcher.target = xy(Screen.W * 0.475, Screen.H * 0.92) - fielders.left.target = xy(Screen.W * -1, Screen.H * -0.2) - fielders.center.target = xy(Center.x, Screen.H * -0.4) - fielders.right.target = xy(Screen.W * 2, fielders.left.target.y) + fielders.first.target = xy(Screen.W - 65, Screen.H * 0.48) + fielders.second.target = xy(Screen.W * 0.70, Screen.H * 0.30) + fielders.shortstop.target = xy(Screen.W * 0.30, Screen.H * 0.30) + fielders.third.target = xy(Screen.W * 0.1, Screen.H * 0.48) + fielders.pitcher.target = xy(PitcherStartPos.x, PitcherStartPos.y) + fielders.catcher.target = xy(Screen.W * 0.475, Screen.H * 0.92) + fielders.left.target = xy(Screen.W * -1, Screen.H * -0.2) + fielders.center.target = xy(Center.x, Screen.H * -0.4) + fielders.right.target = xy(Screen.W * 2, fielders.left.target.y) end local BatterStartingX = Bases[Home].x - 40 @@ -218,15 +218,15 @@ local outRunners = {} ---@return Runner function newRunner() - local new = { - x = BatterStartingX - 60, - y = BatterStartingY + 60, - nextBase = RightHandedBattersBox, - prevBase = nil, - forcedTo = Bases[First], - } - runners[#runners + 1] = new - return new + local new = { + x = BatterStartingX - 60, + y = BatterStartingY + 60, + nextBase = RightHandedBattersBox, + prevBase = nil, + forcedTo = Bases[First], + } + runners[#runners + 1] = new + return new end ---@type Runner | nil @@ -234,21 +234,21 @@ local batter = newRunner() --- "Throws" the ball from its current position to the given destination. function throwBall(destX, destY, easingFunc, flyTimeMs, floaty, customBallScaler) - if not flyTimeMs then - flyTimeMs = distanceBetween(ball.x, ball.y, destX, destY) * 5 - end - ball.heldBy = nil - if customBallScaler then - ballSizeAnimator = customBallScaler - else - -- TODO? Scale based on distance? - ballSizeAnimator = gfx.animator.new(flyTimeMs, 9, SmallestBallRadius, easingHill) - end - ballAnimatorY = gfx.animator.new(flyTimeMs, ball.y, destY, easingFunc) - ballAnimatorX = gfx.animator.new(flyTimeMs, ball.x, destX, easingFunc) - if floaty then - ballFloatAnimator:reset(flyTimeMs) - end + if not flyTimeMs then + flyTimeMs = distanceBetween(ball.x, ball.y, destX, destY) * 5 + end + ball.heldBy = nil + if customBallScaler then + ballSizeAnimator = customBallScaler + else + -- TODO? Scale based on distance? + ballSizeAnimator = gfx.animator.new(flyTimeMs, 9, SmallestBallRadius, easingHill) + end + ballAnimatorY = gfx.animator.new(flyTimeMs, ball.y, destY, easingFunc) + ballAnimatorX = gfx.animator.new(flyTimeMs, ball.x, destX, easingFunc) + if floaty then + ballFloatAnimator:reset(flyTimeMs) + end end local PitchAfterSeconds = 7 @@ -258,23 +258,23 @@ local secondsSincePitchAllowed = -5 local catcherThrownBall = false function pitch() - catcherThrownBall = false - currentMode = Modes.batting + catcherThrownBall = false + currentMode = Modes.batting - local current = Pitches[math.random(#Pitches)] - ballAnimatorX = current.x - ballAnimatorY = current.y or Pitches[1].y + local current = Pitches[math.random(#Pitches)] + ballAnimatorX = current.x + ballAnimatorY = current.y or Pitches[1].y - -- TODO: This would need to be sanely replaced in throwBall() etc. - -- if current.z then - -- ballFloatAnimator = current.z - -- ballFloatAnimator:reset() - -- end + -- TODO: This would need to be sanely replaced in throwBall() etc. + -- if current.z then + -- ballFloatAnimator = current.z + -- ballFloatAnimator:reset() + -- end - ballAnimatorX:reset() - ballAnimatorY:reset() + ballAnimatorX:reset() + ballAnimatorY:reset() - secondsSincePitchAllowed = 0 + secondsSincePitchAllowed = 0 end local elapsedSec = 0 @@ -284,28 +284,28 @@ local acceleratedChange local BaseHitbox = 13 --- Returns the base being touched by the runner at (x,y), or nil, if no base is being touched function isTouchingBase(x, y) - for _, base in ipairs(Bases) do - if distanceBetween(x, y, base.x, base.y) < BaseHitbox then - return base - end - end + for _, base in ipairs(Bases) do + if distanceBetween(x, y, base.x, base.y) < BaseHitbox then + return base + end + end - return nil + return nil end local BallCatchHitbox = 3 function isTouchingBall(x, y) - local ballDistance = distanceBetween(x, y, ball.x, ball.y) - return ballDistance < BallCatchHitbox + local ballDistance = distanceBetween(x, y, ball.x, ball.y) + return ballDistance < BallCatchHitbox end local teams = { - home = { - score = 0, - }, - away = { - score = 0, - }, + home = { + score = 0, + }, + away = { + score = 0, + }, } local battingTeam = teams.away @@ -315,231 +315,231 @@ local inning = 1 ---@param base Base ---@return Runner | nil function getRunnerTargeting(base) - for _, runner in pairs(runners) do - if runner.nextBase == base then - return runner - end - end - return nil + for _, runner in pairs(runners) do + if runner.nextBase == base then + return runner + end + end + return nil end function updateForcedRunners() - local stillForced = true - for _, base in ipairs(Bases) do - local runnerTargetingBase = getRunnerTargeting(base) - if runnerTargetingBase then - if stillForced then - runnerTargetingBase.forcedTo = base - else - runnerTargetingBase.forcedTo = nil - end - else - stillForced = false - end - end + local stillForced = true + for _, base in ipairs(Bases) do + local runnerTargetingBase = getRunnerTargeting(base) + if runnerTargetingBase then + if stillForced then + runnerTargetingBase.forcedTo = base + else + runnerTargetingBase.forcedTo = nil + end + else + stillForced = false + end + end end ---@param runnerIndex integer function outRunner(runnerIndex) - outs = outs + 1 - outRunners[#outRunners + 1] = runners[runnerIndex] - table.remove(runners, runnerIndex) - updateForcedRunners() + outs = outs + 1 + outRunners[#outRunners + 1] = runners[runnerIndex] + table.remove(runners, runnerIndex) + updateForcedRunners() - announcer:say("YOU'RE OUT!") - if outs == 3 then - local gameOver = inning == 9 and teams.away.score ~= teams.home.score - if not gameOver then - fieldersDance() - announcer:say("SWITCHING SIDES...") - end - while #runners > 0 do - outRunners[#outRunners + 1] = table.remove(runners, #runners) - end - -- Delay to keep end-of-inning on the scoreboard for a few seconds - playdate.timer.new(3000, function() - outs = 0 - if battingTeam == teams.home then - battingTeam = teams.away - inning = inning + 1 - else - battingTeam = teams.home - end - if gameOver then - announcer:say("AND THAT'S THE BALL GAME!") - end - end) - end + announcer:say("YOU'RE OUT!") + if outs == 3 then + local gameOver = inning == 9 and teams.away.score ~= teams.home.score + if not gameOver then + fieldersDance() + announcer:say("SWITCHING SIDES...") + end + while #runners > 0 do + outRunners[#outRunners + 1] = table.remove(runners, #runners) + end + -- Delay to keep end-of-inning on the scoreboard for a few seconds + playdate.timer.new(3000, function() + outs = 0 + if battingTeam == teams.home then + battingTeam = teams.away + inning = inning + 1 + else + battingTeam = teams.home + end + if gameOver then + announcer:say("AND THAT'S THE BALL GAME!") + end + end) + end end -- TODO: Away score function score(runnerIndex) - outRunners[#outRunners + 1] = runners[runnerIndex] - table.remove(runners, runnerIndex) - battingTeam.score = battingTeam.score + 1 - announcer:say("SCORE!") + outRunners[#outRunners + 1] = runners[runnerIndex] + table.remove(runners, runnerIndex) + battingTeam.score = battingTeam.score + 1 + announcer:say("SCORE!") end ---@return Base[] function getForcedOutTargets() - local targets = {} - for _, base in ipairs(Bases) do - local runnerTargetingBase = getRunnerTargeting(base) - if runnerTargetingBase then - targets[#targets + 1] = base - else - return targets - end - end - return targets + local targets = {} + for _, base in ipairs(Bases) do + local runnerTargetingBase = getRunnerTargeting(base) + if runnerTargetingBase then + targets[#targets + 1] = base + else + return targets + end + end + return targets end --- Returns the position,distance of the basest closest to the runner furthest from a base ---@return Base | nil, number | nil function getBaseOfStrandedRunner() - local farRunnersBase, farDistance - for _, runner in pairs(runners) do - local nearestBase, distance = getNearestOf(Bases, runner.x, runner.y, function(base) - return runner.nextBase == base - end) - if farRunnersBase == nil or farDistance < distance then - farRunnersBase = nearestBase - farDistance = distance - end - end + local farRunnersBase, farDistance + for _, runner in pairs(runners) do + local nearestBase, distance = getNearestOf(Bases, runner.x, runner.y, function(base) + return runner.nextBase == base + end) + if farRunnersBase == nil or farDistance < distance then + farRunnersBase = nearestBase + farDistance = distance + end + end - return farRunnersBase, farDistance + return farRunnersBase, farDistance end --- Returns x,y of the throw target ---@return number|nil, number|nil function getNextThrowTarget() - -- TODO: Handle missed throws, check for fielders at target, etc. - local targets = getForcedOutTargets() - if #targets ~= 0 then - return targets[#targets].x, targets[#targets].y - end + -- TODO: Handle missed throws, check for fielders at target, etc. + local targets = getForcedOutTargets() + if #targets ~= 0 then + return targets[#targets].x, targets[#targets].y + end - local baseCloseToStrandedRunner = getBaseOfStrandedRunner() - if baseCloseToStrandedRunner then - return baseCloseToStrandedRunner.x, baseCloseToStrandedRunner.y - end + local baseCloseToStrandedRunner = getBaseOfStrandedRunner() + if baseCloseToStrandedRunner then + return baseCloseToStrandedRunner.x, baseCloseToStrandedRunner.y + end end function tryToThrowOut(thrower) - local targetX, targetY = getNextThrowTarget() - if targetX ~= nil and targetY ~= nil then - local nearestFielder = getNearestOf(fielders, targetX, targetY) - nearestFielder.target = xy(targetX, targetY) - if nearestFielder == thrower then - ball.heldBy = thrower - else - throwBall(targetX, targetY, playdate.easingFunctions.linear, nil, true) - end - end + local targetX, targetY = getNextThrowTarget() + if targetX ~= nil and targetY ~= nil then + local nearestFielder = getNearestOf(fielders, targetX, targetY) + nearestFielder.target = xy(targetX, targetY) + if nearestFielder == thrower then + ball.heldBy = thrower + else + throwBall(targetX, targetY, playdate.easingFunctions.linear, nil, true) + end + end end function updateFielders() - local touchingBaseCache = buildCache(function(runner) - return isTouchingBase(runner.x, runner.y) - end) + local touchingBaseCache = buildCache(function(runner) + return isTouchingBase(runner.x, runner.y) + end) - for _, fielder in pairs(fielders) do - -- TODO: Target unforced runners (or their target bases) for tagging - -- With new Position-based scheme, fielders are now able to set `fielder.target = runner` to track directly + for _, fielder in pairs(fielders) do + -- TODO: Target unforced runners (or their target bases) for tagging + -- With new Position-based scheme, fielders are now able to set `fielder.target = runner` to track directly - if fielder.target ~= nil then - local x, y, distance = normalizeVector(fielder.x, fielder.y, fielder.target.x, fielder.target.y) + if fielder.target ~= nil then + local x, y, distance = normalizeVector(fielder.x, fielder.y, fielder.target.x, fielder.target.y) - if distance > 1 then - fielder.x = fielder.x - (x * fielder.speed * deltaSeconds) - fielder.y = fielder.y - (y * fielder.speed * deltaSeconds) - else - fielder.target = nil - end - end + if distance > 1 then + fielder.x = fielder.x - (x * fielder.speed * deltaSeconds) + fielder.y = fielder.y - (y * fielder.speed * deltaSeconds) + else + fielder.target = nil + end + end - if currentMode == Modes.running and isTouchingBall(fielder.x, fielder.y) then - -- TODO: Check for double-plays or other available outs. - local touchedBase = isTouchingBase(fielder.x, fielder.y) - for i, runner in pairs(runners) do - if - ( -- Force out - touchedBase - and runner.prevBase -- Make sure the runner is not standing at home - and runner.forcedTo == touchedBase - and touchedBase ~= touchingBaseCache.get(runner) - ) - or ( -- Tag out - not touchingBaseCache.get(runner) - and distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < TagDistance - ) - then - outRunner(i) - playdate.timer.new(750, function() - tryToThrowOut(fielder) - end) - else - tryToThrowOut(fielder) - end - end - end - end + if currentMode == Modes.running and isTouchingBall(fielder.x, fielder.y) then + -- TODO: Check for double-plays or other available outs. + local touchedBase = isTouchingBase(fielder.x, fielder.y) + for i, runner in pairs(runners) do + if + ( -- Force out + touchedBase + and runner.prevBase -- Make sure the runner is not standing at home + and runner.forcedTo == touchedBase + and touchedBase ~= touchingBaseCache.get(runner) + ) + or ( -- Tag out + not touchingBaseCache.get(runner) + and distanceBetween(runner.x, runner.y, fielder.x, fielder.y) < TagDistance + ) + then + outRunner(i) + playdate.timer.new(750, function() + tryToThrowOut(fielder) + end) + else + tryToThrowOut(fielder) + end + end + end + end end --- Returns true if at least one runner is still moving ---@return boolean function updateRunners(currentRunners) - local autoRunSpeed = 20 * deltaSeconds - --autoRunSpeed = 140 - -- TODO: Filter for the runner closest to the currently-held direction button + local autoRunSpeed = 20 * deltaSeconds + --autoRunSpeed = 140 + -- TODO: Filter for the runner closest to the currently-held direction button - local runnerMoved = false - for runnerIndex, runner in ipairs(currentRunners) do - local appliedSpeed = crankChange -- TODO: Allow for individual runner control via buttons - local nearestBase, nearestBaseDistance = getNearestOf(Bases, runner.x, runner.y) + local runnerMoved = false + for runnerIndex, runner in ipairs(currentRunners) do + local appliedSpeed = crankChange -- TODO: Allow for individual runner control via buttons + local nearestBase, nearestBaseDistance = getNearestOf(Bases, runner.x, runner.y) - if - nearestBaseDistance < 5 - and runner.prevBase - and runner.nextBase == Bases[Home] - and nearestBase == Bases[Home] - then - score(runnerIndex) - end + if + nearestBaseDistance < 5 + and runner.prevBase + and runner.nextBase == Bases[Home] + and nearestBase == Bases[Home] + then + score(runnerIndex) + end - if runner.nextBase then - local nb = runner.nextBase - local x, y, distance = normalizeVector(runner.x, runner.y, nb.x, nb.y) + if runner.nextBase then + local nb = runner.nextBase + local x, y, distance = normalizeVector(runner.x, runner.y, nb.x, nb.y) - if distance > 1 then - 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 + if distance > 1 then + 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: Also move if forced to 😅 - local autoRun = (nearestBaseDistance > 40 or runner.forcedTo) and mult * autoRunSpeed - or nearestBaseDistance < 5 and 0 - or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed) - mult = autoRun + (appliedSpeed / 20) - runner.x = runner.x - (x * mult) - runner.y = runner.y - (y * mult) + -- TODO: Also move if forced to 😅 + local autoRun = (nearestBaseDistance > 40 or runner.forcedTo) and mult * autoRunSpeed + or nearestBaseDistance < 5 and 0 + or (nearestBase == runner.nextBase and autoRunSpeed or -1 * autoRunSpeed) + mult = autoRun + (appliedSpeed / 20) + runner.x = runner.x - (x * mult) + runner.y = runner.y - (y * mult) - runnerMoved = runnerMoved or prevX ~= runner.x or prevY ~= runner.y - else - runner.nextBase = NextBaseMap[runner.nextBase] - runner.forcedTo = nil - end - end - end + runnerMoved = runnerMoved or prevX ~= runner.x or prevY ~= runner.y + else + runner.nextBase = NextBaseMap[runner.nextBase] + runner.forcedTo = nil + end + end + end - return runnerMoved + return runnerMoved end local ResetFieldersAfterSeconds = 2 @@ -547,203 +547,203 @@ local ResetFieldersAfterSeconds = 2 local secondsSinceLastRunnerMove = 0 function init() - playdate.display.setRefreshRate(50) - gfx.setBackgroundColor(gfx.kColorWhite) - playdate.setMenuImage(gfx.image.new("images/game/menu-image.png")) - resetFielderPositions(true) - playdate.getSystemMenu():addMenuItem("Restart game", function() end) + playdate.display.setRefreshRate(50) + gfx.setBackgroundColor(gfx.kColorWhite) + playdate.setMenuImage(gfx.image.new("images/game/menu-image.png")) + resetFielderPositions(true) + playdate.getSystemMenu():addMenuItem("Restart game", function() end) -- TODO? - playdate.timer.new(2000, function() - throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, false) - end) - BootTune:play() - BootTune:setFinishCallback(function() - TinnyBackground:play() - end) + playdate.timer.new(2000, function() + throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, false) + end) + BootTune:play() + BootTune:setFinishCallback(function() + TinnyBackground:play() + end) end local batAngleDeg function updateBatting() - if ball.y < BallOffscreen then - ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue() - ball.size = SmallestBallRadius -- ballFloatAnimator:currentValue() - end + if ball.y < BallOffscreen then + ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue() + ball.size = SmallestBallRadius -- ballFloatAnimator:currentValue() + end - batAngleDeg = (playdate.getCrankPosition() + CrankOffsetDeg) % 360 - local batAngle = math.rad(batAngleDeg) - -- TODO: animate bat-flip or something - batBase.x = batter and (batter.x + BatOffset.x) or 0 - batBase.y = batter and (batter.y + BatOffset.y) or 0 - batTip.x = batBase.x + (BatLength * math.sin(batAngle)) - batTip.y = batBase.y + (BatLength * math.cos(batAngle)) + batAngleDeg = (playdate.getCrankPosition() + CrankOffsetDeg) % 360 + local batAngle = math.rad(batAngleDeg) + -- TODO: animate bat-flip or something + batBase.x = batter and (batter.x + BatOffset.x) or 0 + batBase.y = batter and (batter.y + BatOffset.y) or 0 + batTip.x = batBase.x + (BatLength * math.sin(batAngle)) + batTip.y = batBase.y + (BatLength * math.cos(batAngle)) - if - acceleratedChange >= 0 - and pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, Screen.H) - then - BatCrackSound:play() - currentMode = Modes.running - local ballAngle = batAngle + math.rad(90) + if + acceleratedChange >= 0 + and pointDirectlyUnderLine(ball.x, ball.y, batBase.x, batBase.y, batTip.x, batTip.y, Screen.H) + then + BatCrackSound:play() + currentMode = Modes.running + local ballAngle = batAngle + math.rad(90) - local mult = math.abs(crankChange / 15) - local ballVelX = mult * 10 * math.sin(ballAngle) - local ballVelY = mult * 5 * math.cos(ballAngle) - if ballVelY > 0 then - ballVelX = ballVelX * -1 - ballVelY = ballVelY * -1 - end - local ballDestX = ball.x + (ballVelX * HitMult) - local ballDestY = ball.y + (ballVelY * HitMult) - -- Hit! - throwBall( - ballDestX, - ballDestY, - playdate.easingFunctions.outQuint, - 2000, - nil, - gfx.animator.new(2000, 9 + (mult * mult * 0.5), SmallestBallRadius, easingHill) - ) + local mult = math.abs(crankChange / 15) + local ballVelX = mult * 10 * math.sin(ballAngle) + local ballVelY = mult * 5 * math.cos(ballAngle) + if ballVelY > 0 then + ballVelX = ballVelX * -1 + ballVelY = ballVelY * -1 + end + local ballDestX = ball.x + (ballVelX * HitMult) + local ballDestY = ball.y + (ballVelY * HitMult) + -- Hit! + throwBall( + ballDestX, + ballDestY, + playdate.easingFunctions.outQuint, + 2000, + nil, + gfx.animator.new(2000, 9 + (mult * mult * 0.5), SmallestBallRadius, easingHill) + ) - fielders.first.target = Bases[First] - batter.nextBase = Bases[First] - batter.prevBase = Bases[Home] - updateForcedRunners() - batter.forcedTo = Bases[First] - batter = nil -- Demote batter to a mere runner + fielders.first.target = Bases[First] + batter.nextBase = Bases[First] + batter.prevBase = Bases[Home] + updateForcedRunners() + batter.forcedTo = Bases[First] + batter = nil -- Demote batter to a mere runner - local chasingFielder = getNearestOf(fielders, ballDestX, ballDestY) - chasingFielder.target = { x = ballDestX, y = ballDestY } - end + local chasingFielder = getNearestOf(fielders, ballDestX, ballDestY) + chasingFielder.target = { x = ballDestX, y = ballDestY } + end end function updateRunning() - local nonBatterRunners = filter(runners, function(runner) - return runner ~= batter - end) - ball.size = ballSizeAnimator:currentValue() - if updateRunners(nonBatterRunners) then - secondsSinceLastRunnerMove = 0 - else - secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds - if secondsSinceLastRunnerMove > ResetFieldersAfterSeconds then - throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true) - resetFielderPositions(false) - currentMode = Modes.batting - batter = newRunner() - end - end + local nonBatterRunners = filter(runners, function(runner) + return runner ~= batter + end) + ball.size = ballSizeAnimator:currentValue() + if updateRunners(nonBatterRunners) then + secondsSinceLastRunnerMove = 0 + else + secondsSinceLastRunnerMove = secondsSinceLastRunnerMove + deltaSeconds + if secondsSinceLastRunnerMove > ResetFieldersAfterSeconds then + throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true) + resetFielderPositions(false) + currentMode = Modes.batting + batter = newRunner() + end + end end function updateOutRunners() - for i, runner in ipairs(outRunners) do - if runner.x < Screen.W + 50 and runner.y < Screen.H + 50 then - runner.x = runner.x + (deltaSeconds * 25) - runner.y = runner.y + (deltaSeconds * 25) - else - table.remove(outRunners, i) - end - end + for i, runner in ipairs(outRunners) do + if runner.x < Screen.W + 50 and runner.y < Screen.H + 50 then + runner.x = runner.x + (deltaSeconds * 25) + runner.y = runner.y + (deltaSeconds * 25) + else + table.remove(outRunners, i) + end + end end function updateGameState() - deltaSeconds = playdate.getElapsedTime() or 0 - playdate.resetElapsedTime() - elapsedSec = elapsedSec + deltaSeconds - crankChange, acceleratedChange = playdate.getCrankChange() --[[@as number, number]] + deltaSeconds = playdate.getElapsedTime() or 0 + playdate.resetElapsedTime() + elapsedSec = elapsedSec + deltaSeconds + crankChange, acceleratedChange = playdate.getCrankChange() --[[@as number, number]] - if ball.heldBy then - ball.x = ball.heldBy.x - ball.y = ball.heldBy.y - else - ball.x = ballAnimatorX:currentValue() - ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue() - end + if ball.heldBy then + ball.x = ball.heldBy.x + ball.y = ball.heldBy.y + else + ball.x = ballAnimatorX:currentValue() + ball.y = ballAnimatorY:currentValue() + ballFloatAnimator:currentValue() + end - if currentMode == Modes.batting then - secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds - if secondsSincePitchAllowed > 3.5 and not catcherThrownBall then - throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true) - catcherThrownBall = true - end - if secondsSincePitchAllowed > PitchAfterSeconds then - pitch() - end - updateBatting() - updateRunners({ batter }) - elseif currentMode == Modes.running then - updateRunning() - end + if currentMode == Modes.batting then + secondsSincePitchAllowed = secondsSincePitchAllowed + deltaSeconds + if secondsSincePitchAllowed > 3.5 and not catcherThrownBall then + throwBall(PitchStartX, PitchStartY, playdate.easingFunctions.linear, nil, true) + catcherThrownBall = true + end + if secondsSincePitchAllowed > PitchAfterSeconds then + pitch() + end + updateBatting() + updateRunners({ batter }) + elseif currentMode == Modes.running then + updateRunning() + end - updateFielders() - updateOutRunners() + updateFielders() + updateOutRunners() end -- TODO function drawMinimap() end function playdate.update() - playdate.timer.updateTimers() + playdate.timer.updateTimers() - updateGameState() - gfx.animation.blinker.updateAll() + updateGameState() + gfx.animation.blinker.updateAll() - gfx.clear() - gfx.setColor(gfx.kColorBlack) + gfx.clear() + gfx.setColor(gfx.kColorBlack) - local offsetX, offsetY = 0, 0 - if ball.x < BallOffscreen then - offsetX, offsetY = getDrawOffset(Screen.W, Screen.H, ball.x, ball.y) - gfx.setDrawOffset(offsetX, offsetY) - end + local offsetX, offsetY = 0, 0 + if ball.x < BallOffscreen then + offsetX, offsetY = getDrawOffset(Screen.W, Screen.H, ball.x, ball.y) + gfx.setDrawOffset(offsetX, offsetY) + end - GrassBackground:draw(-400, -240) + GrassBackground:draw(-400, -240) - local fielderDanceHeight = FielderDanceAnimator:currentValue() - for _, fielder in pairs(fielders) do - gfx.fillRect(fielder.x, fielder.y - fielderDanceHeight, 14, 25) - end + local fielderDanceHeight = FielderDanceAnimator:currentValue() + for _, fielder in pairs(fielders) do + gfx.fillRect(fielder.x, fielder.y - fielderDanceHeight, 14, 25) + end - if currentMode == Modes.batting then - gfx.setLineWidth(5) - gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y) - end + if currentMode == Modes.batting then + gfx.setLineWidth(5) + gfx.drawLine(batBase.x, batBase.y, batTip.x, batTip.y) + end - if playdate.isCrankDocked() then - playdate.ui.crankIndicator:draw() - end + if playdate.isCrankDocked() then + playdate.ui.crankIndicator:draw() + end - -- TODO? Change blip speed depending on runner speed? - for _, runner in pairs(runners) do - if runner == batter then - if batAngleDeg > 50 and batAngleDeg < 200 then - PlayerBack:draw(runner.x, runner.y) - else - PlayerSmile:draw(runner.x, runner.y) - end - else - -- TODO? Scale sprites down as y increases - PlayerImageBlipper:draw(false, runner.x, runner.y) - end - end - for _, runner in pairs(outRunners) do - PlayerFrown:draw(runner.x, runner.y) - end + -- TODO? Change blip speed depending on runner speed? + for _, runner in pairs(runners) do + if runner == batter then + if batAngleDeg > 50 and batAngleDeg < 200 then + PlayerBack:draw(runner.x, runner.y) + else + PlayerSmile:draw(runner.x, runner.y) + end + else + -- TODO? Scale sprites down as y increases + PlayerImageBlipper:draw(false, runner.x, runner.y) + end + end + for _, runner in pairs(outRunners) do + PlayerFrown:draw(runner.x, runner.y) + end - gfx.setLineWidth(2) + gfx.setLineWidth(2) - gfx.setColor(gfx.kColorWhite) - gfx.fillCircleAtPoint(ball.x, ball.y, ball.size) + gfx.setColor(gfx.kColorWhite) + gfx.fillCircleAtPoint(ball.x, ball.y, ball.size) - gfx.setColor(gfx.kColorBlack) - gfx.drawCircleAtPoint(ball.x, ball.y, ball.size) + gfx.setColor(gfx.kColorBlack) + gfx.drawCircleAtPoint(ball.x, ball.y, ball.size) - gfx.setDrawOffset(0, 0) - if offsetX > 0 or offsetY > 0 then - drawMinimap() - end - drawScoreboard(0, Screen.H * 0.77, teams, outs, battingTeam, inning) - announcer:draw(Center.x, 10) + gfx.setDrawOffset(0, 0) + if offsetX > 0 or offsetY > 0 then + drawMinimap() + end + drawScoreboard(0, Screen.H * 0.77, teams, outs, battingTeam, inning) + announcer:draw(Center.x, 10) end init() diff --git a/src/scoreboard.lua b/src/scoreboard.lua index 3c035cc..2887855 100644 --- a/src/scoreboard.lua +++ b/src/scoreboard.lua @@ -10,53 +10,53 @@ local IndicatorWidth = ScoreFont:getTextWidth(Indicator) ---@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 + if teams.home == battingTeam then + return Indicator, 0, "", IndicatorWidth + end + return "", IndicatorWidth, Indicator, 0 end function drawScoreboard(x, y, teams, outs, battingTeam, inning) - local gfx = playdate.graphics + local gfx = playdate.graphics - local homeScore = teams.home.score - local awayScore = teams.away.score + local homeScore = teams.home.score + local awayScore = teams.away.score - local homeIndicator, homeOffset, awayIndicator, awayOffset = getIndicators(teams, battingTeam) + 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 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 + local rectWidth = (ScoreboardMarginX * 2) + + ScoreboardMarginRight + + ScoreFont:getTextWidth(homeScoreText) + + homeOffset - gfx.setLineWidth(1) - gfx.setColor(gfx.kColorBlack) - gfx.fillRect(x, y, rectWidth, ScoreboardHeight) + gfx.setLineWidth(1) + gfx.setColor(gfx.kColorBlack) + gfx.fillRect(x, y, rectWidth, ScoreboardHeight) - local originalDrawMode = gfx.getImageDrawMode() - gfx.setImageDrawMode(gfx.kDrawModeInverted) + 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) + 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.setImageDrawMode(originalDrawMode) - gfx.setColor(gfx.kColorWhite) + gfx.setColor(gfx.kColorWhite) - function circleParams(i) - local circleOffset = i * 2.5 * OutBubbleRadius - return (x + ScoreboardMarginX + OutBubbleRadius + IndicatorWidth) + circleOffset, y + 46, OutBubbleRadius - end + 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 + for i = outs, 2 do + gfx.drawCircleAtPoint(circleParams(i)) + end + for i = 0, (outs - 1) do + gfx.fillCircleAtPoint(circleParams(i)) + end end diff --git a/src/utils.lua b/src/utils.lua index bce070d..e5a9df2 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -4,40 +4,40 @@ import 'CoreLibs/graphics.lua' -- stylua: ignore end function easingHill(t, b, c, d) - c = c + 0.0 -- convert to float to prevent integer overflow - t = t / d - t = ((t * 2) - 1) - t = t * t - return (c * t) + b + c = c + 0.0 -- convert to float to prevent integer overflow + t = t / d + t = ((t * 2) - 1) + t = t * t + return (c * t) + b end -- Useful for quick print-the-value-in-place debugging. -- selene: allow(unused_variable) function label(value, name) - if type(value) == "table" then - print(name .. ":") - printTable(value) - else - print(name .. ": " .. value) - end - return value + if type(value) == "table" then + print(name .. ":") + printTable(value) + else + print(name .. ": " .. value) + end + return value end ---@param x number ---@param y number ---@return XYPair function xy(x, y) - return { - x = x, - y = y, - } + return { + x = x, + y = y, + } end --- Returns the normalized vector as two values, plus the distance between the given points. ---@return number, number, number function normalizeVector(x1, y1, x2, y2) - local distance, a, b = distanceBetween(x1, y1, x2, y2) - return a / distance, b / distance, distance + local distance, a, b = distanceBetween(x1, y1, x2, y2) + return a / distance, b / distance, distance end ---@generic T @@ -45,41 +45,41 @@ end ---@param condition fun(T): boolean ---@return T[] function filter(array, condition) - local newArray = {} - for _, element in pairs(array) do - if condition(element) then - newArray[#newArray + 1] = element - end - end - return newArray + local newArray = {} + for _, element in pairs(array) do + if condition(element) then + newArray[#newArray + 1] = element + end + end + return newArray end ---@return number, number, number function distanceBetween(x1, y1, x2, y2) - local a = x1 - x2 - local b = y1 - y2 - return math.sqrt((a * a) + (b * b)), a, b + local a = x1 - x2 + local b = y1 - y2 + return math.sqrt((a * a) + (b * b)), a, b end --- Returns true only if the point is below the given line, within the x bounds of said line, and above the bottomBound --- @return boolean function pointDirectlyUnderLine(pointX, pointY, lineX1, lineY1, lineX2, lineY2, bottomBound) - -- This check currently assumes right-handedness. - -- I.e. it assumes the ball is to the right of batBaseX - if pointX < lineX1 or pointX > lineX2 or pointY > bottomBound then - return false - end + -- This check currently assumes right-handedness. + -- I.e. it assumes the ball is to the right of batBaseX + if pointX < lineX1 or pointX > lineX2 or pointY > bottomBound then + return false + end - local m = (lineY2 - lineY1) / (lineX2 - lineX1) + local m = (lineY2 - lineY1) / (lineX2 - lineX1) - -- y = mx + b - -- b = y1 - (m * x1) - local b = lineY1 - (m * lineX1) - local yOnLine = (m * pointX) + b - local yP = pointY - local yDelta = yOnLine - yP + -- y = mx + b + -- b = y1 - (m * x1) + local b = lineY1 - (m * lineX1) + local yOnLine = (m * pointX) + b + local yP = pointY + local yDelta = yOnLine - yP - return yDelta <= 0 + return yDelta <= 0 end --- Returns the nearest position object from the given point, as well as its distance from that point @@ -89,23 +89,23 @@ end ---@param y number ---@return T,number|nil function getNearestOf(array, x, y, extraCondition) - local nearest, nearestDistance = nil, nil - for _, element in pairs(array) do - if not extraCondition or extraCondition(element) then - if nearest == nil then - nearest = element - nearestDistance = distanceBetween(element.x, element.y, x, y) - else - local distance = distanceBetween(element.x, element.y, x, y) - if distance < nearestDistance then - nearest = element - nearestDistance = distance - end - end - end - end + local nearest, nearestDistance = nil, nil + for _, element in pairs(array) do + if not extraCondition or extraCondition(element) then + if nearest == nil then + nearest = element + nearestDistance = distanceBetween(element.x, element.y, x, y) + else + local distance = distanceBetween(element.x, element.y, x, y) + if distance < nearestDistance then + nearest = element + nearestDistance = distance + end + end + end + end - return nearest, nearestDistance + return nearest, nearestDistance end --- Marker used by buildCache to indicate a cached `nil` value. @@ -122,18 +122,18 @@ local NoValue = {} ---@generic Value ---@return { get: fun(key: Key): Value } function buildCache(fetcher) - local cacheData = {} - return { - get = function(key) - if cacheData[key] == NoValue then - return nil - end - if cacheData[key] ~= nil then - return cacheData[key] - end - local fetched = fetcher(key) - cacheData[key] = fetched ~= nil and fetched or NoValue - return cacheData[key] - end, - } + local cacheData = {} + return { + get = function(key) + if cacheData[key] == NoValue then + return nil + end + if cacheData[key] ~= nil then + return cacheData[key] + end + local fetched = fetcher(key) + cacheData[key] = fetched ~= nil and fetched or NoValue + return cacheData[key] + end, + } end