Start sketching out how scoring might work.

Import some fixes from tiny-ecs-love-template.
Add some simple sprites for dice.
Sketch out some basic bus/slot logic.
Implement simple sprite animation system.
This commit is contained in:
Sage Vaillancourt 2025-03-22 01:06:56 -04:00
parent ab655c1b8b
commit cb7c3ac779
19 changed files with 349 additions and 32 deletions

BIN
assets/images/DiceFive.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

BIN
assets/images/DiceFour.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

BIN
assets/images/DiceOne.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

BIN
assets/images/DiceSix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

BIN
assets/images/DiceThree.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

BIN
assets/images/DiceTwo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

33
components.lua Normal file
View File

@ -0,0 +1,33 @@
local components = {}
---@param entity Entity
function components.allowedInEngine(entity)
entity.allowedInEngine = T.marker
end
---@param entity Entity
function components.allowedInWheels(entity)
entity.allowedInWheels = T.marker
end
---@param entity Entity
function components.allowedInPropulsion(entity)
entity.allowedInPropulsion = T.marker
end
---@param entity Entity
function components.allowedInSeating(entity)
entity.allowedInSeating = T.marker
end
---@param entity Entity
function components.allowedInStyling(entity)
entity.allowedInStyling = T.marker
end
---@param entity Entity
function components.allowedInBrakes(entity)
entity.allowedInBrakes = T.marker
end
return components

View File

@ -1,8 +1,12 @@
require("../systems/animation")
require("../systems/camera-pan")
require("../systems/collision-detection")
require("../systems/collision-resolution")
require("../systems/decay")
require("../systems/diceRoll")
require("../systems/draw")
require("../systems/gravity")
require("../systems/input")
require("../systems/scoreDisplay")
require("../systems/upgradeScorer")
require("../systems/velocity")

View File

@ -1,11 +1,35 @@
-- GENERATED FILE - DO NOT EDIT
-- Instead, edit the source file directly: assets.lua2p.
-- luacheck: ignore
---@type love.Texture
DiceFive = love.graphics.newImage("assets/images/DiceFive.png")
-- luacheck: ignore
---@type love.Texture
DiceFour = love.graphics.newImage("assets/images/DiceFour.png")
-- luacheck: ignore
---@type love.Texture
DiceOne = love.graphics.newImage("assets/images/DiceOne.png")
-- luacheck: ignore
---@type love.Texture
DiceSix = love.graphics.newImage("assets/images/DiceSix.png")
-- luacheck: ignore
---@type love.Texture
DiceThree = love.graphics.newImage("assets/images/DiceThree.png")
-- luacheck: ignore
---@type love.Texture
DiceTwo = love.graphics.newImage("assets/images/DiceTwo.png")
-- luacheck: ignore
---@type FontData
---@type fun(fontSize: number | nil): love.Font
EtBt7001Z0xa = function(fontSize)
return love.graphics.newFont("assets/fonts/EtBt7001Z0xa.ttf", fontSize)
end

View File

@ -25,9 +25,11 @@ function generatedFileWarning()
return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: assets.lua2p."
end)!!(generatedFileWarning())
!!(dirLookup('assets/images', 'png', 'love.graphics.newImage', 'Image'))
!!(dirLookup('assets/sounds', 'wav', 'love.sound.newSoundData', 'SoundData'))
!!(dirLookup('assets/music', 'wav', 'love.sound.newSoundData', 'SoundData'))
!!(dirLookup('assets/fonts', 'ttf', 'love.graphics.newFont', 'FontData', function(varName, newFunc, file)
!!(dirLookup('assets/images', 'png', 'love.graphics.newImage', 'love.Texture'))
!!(dirLookup('assets/sounds', 'ogg', 'love.audio.newSource', 'love.Source', function(varName, newFunc, file)
return varName .. ' = ' .. newFunc .. '("' .. file .. '", "static")'
end))
!!(dirLookup('assets/music', 'wav', 'love.sound.newSoundData', 'love.SoundData'))
!!(dirLookup('assets/fonts', 'ttf', 'love.graphics.newFont', 'fun(fontSize: number | nil): love.Font', function(varName, newFunc, file)
return varName .. ' = function(fontSize)\n return ' .. newFunc .. '("' .. file .. '", fontSize)\nend'
end))

View File

@ -8,6 +8,7 @@ local SOME_TABLE = {}
---@alias AnyComponent any
---@alias BitMask number
---@alias Collision { collidedInto: Entity, collider: Entity }
---@alias Drawable love.Drawable
---@alias Entity table
---@alias FontData love.FontData
---@alias KeyState table<string, boolean>
@ -31,6 +32,9 @@ T = {
---@type Collision
Collision = SOME_TABLE,
---@type Drawable
Drawable = SOME_TABLE,
---@type Entity
Entity = SOME_TABLE,

View File

@ -60,6 +60,7 @@ local SOME_TABLE = {}
Collision = "{ collidedInto: Entity, collider: Entity }",
BitMask = "number",
FontData = "love.FontData",
Drawable = "love.Drawable",
KeyState = "table<string, boolean>",
}))
T = {

View File

@ -4,42 +4,38 @@ require("generated/filter-types")
require("generated/assets")
require("generated/all-systems")
UiZIndex = math.huge
local smallFont = EtBt7001Z0xa(12)
local scenarios = require("scenarios")
local world = require("world")
local scenarios = {
default = function()
-- TODO: Add default entities
end,
textTestScenario = function()
world:addEntity({
position = { x = 0, y = 600 },
drawAsText = {
text = "Hello, world!",
style = TextStyle.Inverted,
},
velocity = { x = 240, y = -500 },
mass = 1,
decayAfterSeconds = 10,
})
end,
}
local currentScenario = scenarios.textTestScenario
local currentScenario = scenarios.default
local freeze = false
local delta
local playerBus
function love.load()
currentScenario()
love.graphics.setBackgroundColor(1, 1, 1)
playerBus = currentScenario(world)
love.graphics.setBackgroundColor(0, 0, 0)
love.graphics.setFont(EtBt7001Z0xa(32))
end
local nth = 1
world:addEntity({
triggerDiceRoll = "",
})
function love.update(dt)
delta = dt
if love.keyboard.isDown("r") then
world:clearEntities()
currentScenario()
playerBus = currentScenario(world)
freeze = false
end
@ -51,6 +47,18 @@ function love.update(dt)
return
end
-- if nth == 3 then
-- world:addEntity({
-- scoringTrigger = {
-- mask = 1,
-- target = playerBus,
-- },
-- })
-- nth = 1
-- else
-- nth = nth + 1
-- end
world:update(delta, function(_, system)
if system.deferToEnd then
return false
@ -60,6 +68,10 @@ function love.update(dt)
end
function love.draw()
love.graphics.setColor(1, 1, 1)
love.graphics.setFont(smallFont)
love.graphics.print("FPS: "..tostring(love.timer.getFPS( )), 10, 10)
world:update(delta, function(_, system)
if system.deferToEnd then
return false

81
scenarios.lua Normal file
View File

@ -0,0 +1,81 @@
local scenarios = {}
local width, height = love.graphics.getWidth(), love.graphics.getHeight()
local bigFont = EtBt7001Z0xa(96)
local bigHeight = bigFont:getHeight()
local midFont = EtBt7001Z0xa(32)
local midHeight = midFont:getHeight()
function BitAt(n)
return math.floor(2 ^ (n - 1))
end
---@param world World
function scenarios.default(world)
-- TODO: Add default entities
local busText = "BUS"
local textWidth = bigFont:getWidth(busText)
local playerScoreDisplay = world:addEntity({
position = { x = width - 100, y = 20 },
z = UiZIndex,
drawAsText = {
text = "0",
style = TextStyle.Inverted,
font = midFont,
},
})
local bus = world:addEntity({
position = { x = 400, y = (height - bigHeight) / 2 },
drawAsText = {
font = bigFont,
text = busText,
style = TextStyle.Inverted,
},
score = 0,
scoreDisplay = playerScoreDisplay,
slots = {},
})
local slotNum = 0
local function buildSlot(text, otherEntityData)
otherEntityData.position = { x = 20, y = 40 + (slotNum * midHeight * 1.2) }
otherEntityData.drawAsText = {
font = midFont,
text = text,
style = TextStyle.Inverted,
}
otherEntityData.upgrades = {}
bus.slots[#bus.slots + 1] = world:addEntity(otherEntityData)
slotNum = slotNum + 1
return otherEntityData
end
local wheels = buildSlot("Wheels", {
scoredOn = BitAt(1),
})
wheels.upgrades[#wheels.upgrades + 1] = {
flatScore = 1,
}
buildSlot("Propulsion", {
scoredOn = BitAt(2),
})
buildSlot("Passengers", {
scoredOn = BitAt(3),
})
buildSlot("Style", {
scoredOn = BitAt(4),
})
buildSlot("Brakes", {
scoredOn = BitAt(5),
})
buildSlot("Engine", {
scoredOn = BitAt(6),
})
return bus
end
return scenarios

41
systems/animation.lua Normal file
View File

@ -0,0 +1,41 @@
local world = require("world")
local function getCurrentTimeMilliseconds()
return love.timer.getTime() * 1000
end
local Animation = {
animation = {
initialMs = T.number,
msPerFrame = T.number,
frames = Arr({ drawable = T.Drawable }),
},
position = T.XyPair,
drawAsSprite = T.Drawable
}
---@return { initialMs: number, frames: Entity[], msPerFrame: number }
function BuildAnimation(frames, msPerFrame)
return {
initialMs = getCurrentTimeMilliseconds(),
frames = frames,
msPerFrame = msPerFrame,
}
end
local currentMs = getCurrentTimeMilliseconds()
local animation = world:filteredSystem("animation", Animation, function(e, _, system)
local nextFrameIndex = 1 + math.floor((currentMs - e.animation.initialMs) / e.animation.msPerFrame)
if nextFrameIndex > #e.animation.frames then
e.animation = nil
system.world:addEntity(e)
else
e.drawAsSprite = e.animation.frames[nextFrameIndex]
end
end)
function animation:preProcess()
currentMs = getCurrentTimeMilliseconds()
end

27
systems/diceRoll.lua Normal file
View File

@ -0,0 +1,27 @@
local world = require("world")
local width, height = love.graphics.getWidth(), love.graphics.getHeight()
local dice = {
DiceOne,
DiceTwo,
DiceThree,
DiceFour,
DiceFive,
DiceSix,
}
world:filteredSystem("dice", { triggerDiceRoll = T.marker }, function(e, dt, system)
system.world:removeEntity(e)
local frames = {}
for _ = 1, 20 do
frames[#frames + 1] = dice[math.random(#dice)]
end
system.world:addEntity({
position = {
x = 20,
y = height - DiceOne:getHeight() - 20,
},
drawAsSprite = dice[math.random(#dice)],
animation = BuildAnimation(frames, 100)
})
end)

View File

@ -40,7 +40,13 @@ drawSystem(
"drawText",
{ position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str), font = Maybe(T.FontData) } },
function(e)
local font = e.drawAsText.font or gfx.getFont() -- e.drawAsText.font or AshevilleSans14Bold
local font
if e.drawAsText.font ~= nil then
font = e.drawAsText.font
gfx.setFont(font)
else
font = gfx.getFont()
end
local textHeight = font:getHeight()
local textWidth = font:getWidth(e.drawAsText.text)
@ -49,14 +55,14 @@ drawSystem(
local bgWidth, bgHeight = textWidth + (margin * 2), textHeight + 2
if e.drawAsText.style == TextStyle.Inverted then
gfx.setColor(0, 0, 0)
gfx.setColor(1, 1, 1)
gfx.rectangle("fill", bgLeftEdge, bgTopEdge, textWidth + margin, textHeight + 2)
gfx.setColor(1, 1, 1)
gfx.setColor(0, 0, 0)
elseif e.drawAsText.style == TextStyle.Bordered then
gfx.setColor(1, 1, 1)
gfx.setColor(0, 0, 0)
gfx.rectangle("fill", bgLeftEdge, bgTopEdge, bgWidth, bgHeight)
gfx.setColor(0, 0, 0)
gfx.setColor(1, 1, 1)
gfx.rectangle("line", bgLeftEdge, bgTopEdge, bgWidth, bgHeight)
end

40
systems/scoreDisplay.lua Normal file
View File

@ -0,0 +1,40 @@
local world = require("world")
local width, height = love.graphics.getWidth(), love.graphics.getHeight()
local midFont = EtBt7001Z0xa(32)
local AddScore = {
addingScoreTo = {
score = T.number,
scoreDisplay = Maybe({ drawAsText = { text = T.str } }),
},
scoreToAdd = T.number,
}
local z = 1
world:filteredSystem("score", { addScore = AddScore }, function(e, _, system)
system.world:removeEntity(e)
local target, scoreToAdd = e.addScore.addingScoreTo, e.addScore.scoreToAdd
target.score = target.score + scoreToAdd
if target.scoreDisplay ~= nil then
target.scoreDisplay.drawAsText.text = "" .. target.score
system.world:addEntity({
z = z,
drawAsText = {
text = "+" .. scoreToAdd,
style = TextStyle.Inverted,
font = midFont,
},
position = {
x = target.scoreDisplay.position.x,
y = target.scoreDisplay.position.y,
},
velocity = { x = 100 * (math.random() - 0.5), y = -100 },
mass = 1,
decayAfterSeconds = 3,
})
-- Prevents z-fighting of falling score updates
z = (z + 1) % 1000
end
end)

42
systems/upgradeScorer.lua Normal file
View File

@ -0,0 +1,42 @@
local world = require("world")
local width, height = love.graphics.getWidth(), love.graphics.getHeight()
---@class ScoringTarget
local ScoringTarget = {
slots = Arr({
upgrades = Arr(T.Entity),
}),
score = T.number,
}
local ScoringTrigger = {
mask = T.number,
target = ScoringTarget,
}
--- Probably need a way to access the total so far.
---@param w World
---@param target ScoringTarget
local function addSlotScores(w, target, slotIndex)
local upgrades = target.slots[slotIndex].upgrades
for i, upgrade in ipairs(upgrades) do
if upgrade.flatScore ~= nil then
w:addEntity({
addScore = {
addingScoreTo = target,
scoreToAdd = upgrade.flatScore,
},
})
end
end
end
world:filteredSystem("upgradeScorer", { scoringTrigger = ScoringTrigger }, function(e, _, system)
system.world:removeEntity(e)
local target, mask = e.scoringTrigger.target, e.scoringTrigger.mask
for i, slot in ipairs(target.slots) do
if bit.band(BitAt(i), mask) ~= 0 then
addSlotScores(system.world, target, i)
end
end
end)