BIG big commit, tweak physics parameters. Fix beat detection bug.

Scale down particle sprite. Fix sitelen pona rendering, fix text rendering.
Invert white border feedback, more intuitive semantics.
&c. &c.
This commit is contained in:
yaw-man 2023-01-15 16:46:32 -04:00
parent c5407266eb
commit acffdea826
15 changed files with 338 additions and 250 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
build/
screenshots/
cover.png

View File

@ -4,20 +4,26 @@ local failChimes = {}
local drones = {}
local N = 8
love.audio.setDistanceModel( "exponentclamped" )
local function StartDrones()
love.audio.setEffect( "droneReverb", {
type = "reverb",
volume = 1.0, }
volume = 1.0,
decaytime = 20}
)
)
drones.bass = love.audio.newSource("sounds/drone.flac", "stream")
drones.alto = love.audio.newSource("sounds/drone.flac", "stream")
drones.bass:setVolume( 0 )
drones.bass:setPitch( 0.25 )
drones.bass:setPitch( 0.125 )
drones.alto:setVolume( 0 )
drones.bass:setPitch( 0.5 )
drones.bass:setPitch( 0.25 )
drones.bass:setEffect( "droneReverb", true )
drones.alto:setEffect( "droneReverb", true )
drones.bass:stop()
drones.alto:stop()
love.audio.play( drones.bass )
love.audio.play( drones.alto )
@ -72,6 +78,13 @@ local function OnImpact( impact, score, pass, level )
if not pass then
--drones.bass:seek( level )
-- drones.alto:seek( level )
else
love.audio.setEffect( "passChimeReverb", {
type = "reverb",
volume = 1.0,
gain = 0.1 + level / 200} )
end
local th = impact.th
@ -81,6 +94,7 @@ local function OnImpact( impact, score, pass, level )
local chime = pass and GetNextPassChime() or GetNextFailChime()
chime:setPitch( GetPitch( th ) )
chime:setPosition( r * math.cos( th ), r * math.sin( th ) )
chime:setVolume( 0.1 + level / 200 )
local highgain = math.max( 0, math.min( score, 1.0 ) - (pass and 0.0 or 0.5))
chime:setFilter{
type = "lowpass",
@ -92,11 +106,22 @@ local function OnImpact( impact, score, pass, level )
end
local function OnVictory( )
love.audio.setEffect( "passChimeReverb", {
type = "reverb",
volume = 1.0,
decaytime = 20,} )
end
--score is the hypothetical "beat score" that would be attained
--if an impact took place at this instant.
local function Update( score )
drones.bass:setVolume( 0.5 * math.sqrt( score ) )
--drones.alto:setVolume( 0.3 * score )
local function Update( score, level )
if level > 2 then
drones.bass:setVolume( 0.5 * math.sqrt( score ) )
drones.alto:setVolume( 0.3 * score )
end
end
local function Reset( )
@ -108,4 +133,4 @@ local function Reset( )
end
Reset()
return { OnImpact = OnImpact, Update = Update, Reset = Reset}
return { OnImpact = OnImpact, Update = Update, Reset = Reset, OnVictory = OnVictory}

146
main.lua
View File

@ -10,7 +10,10 @@ local recorder
local DetectCollision
local OnImpact
local OnVictory
local BeatScore
local Draw
local ExtrapolateBeatScore
local _ExtrapolateBeatScore
local state
state = {
@ -22,9 +25,9 @@ state = {
state.timeToSimulate = 0.0
end,
finishTime = 0.0,
timeToSimulate = 0.0,
isGameStarted = false,
beatScoreThreshold = 1.0,
lastBeatScore = 0.0,
currentBeat = 1,
startTime = 0.0,
@ -47,12 +50,51 @@ local function UpdateWindowTransform( w, h )
transform = tf
end
OnImpact = function( impact )
if not impact then return end
local score = BeatScore( impact.t )
--DEBUG
state.lastBeatScore = score
local pass = false
if score > 1.0 then
pass = true
state.currentBeat = state.currentBeat + 1
if state.currentBeat >= 120 then
return OnVictory()
end
end
local x, y = math.cos(impact.th), math.sin(impact.th)
particles:setPosition( impact.r * x, impact.r * y )
particles:setEmissionArea( "normal", 0.1, 0.2, impact.th, true )
particles:emit( 50 * score * score )
audio.OnImpact( impact, score, pass, state.currentBeat )
marble.OnImpact( impact )
wave.OnImpact( impact, state.currentBeat )
end
local _OnImpact = OnImpact
local function NewGame()
love.graphics.setBackgroundColor( 245 / 255, 169 / 255, 184 / 255 ) --Trans pink.
OnImpact = _OnImpact
ExtrapolateBeatScore = _ExtrapolateBeatScore
love.draw = Draw
particles:reset()
state.Reset()
marble.Reset()
wave.Reset()
text.Reset()
end
function love.load()
UpdateWindowTransform( love.graphics.getDimensions() )
love.graphics.setBackgroundColor( 245 / 255, 169 / 255, 184 / 255 ) --Trans pink.
--love.graphics.setBackgroundColor( 91 / 255, 206 / 255, 250 / 255 ) --Trans blue.
do--particle system setup
particles = love.graphics.newParticleSystem(
love.graphics.newImage( "prideflag.png" ),
@ -84,22 +126,23 @@ function love.load()
DetectCollision = assert ( require "collision" )
audio = assert( require "audio" )
recorder = assert( require "recorder" )
return state.Reset()
return NewGame()
end
local function ExtrapolateBeatScore( )
ExtrapolateBeatScore = function( )
local t = love.timer.getTime()
local beat = state.beat
if not beat.t then return 2.0 end
if not beat.mu then return 2.0 end
if beat.mu < 0.001 then return 2.0 end
t = 1.1 * math.sin( math.pi * ( t - beat.t ) / beat.mu )
t = 1.1 * math.sin( 0.5 * math.pi * ( t - beat.t ) / beat.mu )
return t * t * t * t
end
_ExtrapolateBeatScore = ExtrapolateBeatScore
local function BeatScore( t )
BeatScore = function( t )
local beat = state.beat
--Base case 1: first tap.
@ -117,73 +160,51 @@ local function BeatScore( t )
return 2.0
end
local score = 1.1 * math.sin( math.pi * dt / beat.mu )
local score = 1.1 * math.sin( 0.5 * math.pi * dt / beat.mu )
score = score * score * score * score
--General case: update average beat length.
if dt > 0.2 then
--Debounce: max BPM 200
if dt > 60.0 / 200.0 then
local WEIGHT = 0.85 --High number makes the last beat more significant.
local WEIGHT = 0.75 --High number makes the last beat more significant.
local mu = ( 1.0 - WEIGHT ) * beat.mu + WEIGHT * dt
beat.mu = mu
else
score = 0
end
return score
end
OnImpact = function( impact )
if not impact then return end
local score = BeatScore( impact.t )
--DEBUG
state.lastBeatScore = score
local pass = false
if score > 1.0 then
pass = true
state.currentBeat = state.currentBeat + 1
if state.currentBeat >= 120 then
return OnVictory()
end
end
local x, y = math.cos(impact.th), math.sin(impact.th)
particles:setPosition( impact.r * x, impact.r * y )
particles:setEmissionArea( "normal", 0.1, 0.2, impact.th, true )
particles:emit( 50 * score * score )
audio.OnImpact( impact, score, pass, state.currentBeat )
marble.OnImpact( impact )
wave.OnImpact( impact )
end
local _OnImpact = OnImpact
local function NewGame()
OnImpact = _OnImpact
state.Reset()
marble.Reset()
wave.Reset()
end
OnVictory = function()
local totalTime = love.timer.getTime() - state.startTime
OnImpact = function() end
love.draw = function()
love.graphics.setColor( 1, 1, 1, 1 )
text.Draw( 120 )
love.graphics.printf(
"your.time:\n"..totalTime,
0, 0.5 * love.graphics.getHeight(),
love.graphics.getWidth(),
"center"
)
marble.Draw()
end
marble.OnVictory()
love.graphics.setBackgroundColor( 91 / 255, 206 / 255, 250 / 255 ) --Trans blue.
end
function love.draw()
--[[love.graphics.setColor(1.0, 1.0, 1.0)
love.graphics.print( state.beat.mu or 0, 0)
love.graphics.print( state.beat.t or 0, 0, 10)
love.graphics.print( state.beatScoreThreshold, 0, 20)
love.graphics.print( state.lastBeatScore, 0, 30 )]]
Draw = function()
local score = ExtrapolateBeatScore()
love.graphics.push( "transform" )
love.graphics.applyTransform( transform )
@ -219,10 +240,9 @@ function love.draw()
end]]
love.graphics.pop()
sitelenpona.Draw( text.tok[state.currentBeat] )
sitelenpona.Draw( text.words[state.currentBeat] )
marble.Draw()
text.Draw( state.currentBeat )
end
@ -231,7 +251,7 @@ end
function love.update( dt )
audio.Update( ExtrapolateBeatScore() )
audio.Update( ExtrapolateBeatScore(), state.currentBeat )
particles:update( dt )
dt = dt + state.timeToSimulate
@ -269,4 +289,8 @@ end
function love.resize( w, h )
UpdateWindowTransform( w, h )
if marble then marble.Resize() end
end
function love.mousepressed( x, y, button, istouch, presses )
return NewGame()
end

View File

@ -1,5 +1,6 @@
--Character controller. Renders point particle.
local love = love
local marble = {}
local oldBuffer = love.graphics.newCanvas()
local newBuffer = love.graphics.newCanvas()
local oldState, curState, newState
@ -12,14 +13,14 @@ local function State( )
return { t = 0, x = 0, y = 0, dx = 0, dy = 0 }
end
local function GetAcceleration( )
return ddx, ddy
end
local function Current() return curState end
local function Next() return newState end
function marble.Current() return curState end
local function Integrate( step )
function marble.Next() return newState end
function marble.GetAcceleration( ) return ddx, ddy end
function marble.Integrate( step )
newState.t = love.timer.getTime()
newState.dx = (1.0 - FRICTION) * curState.dx + FRICTION * ddx
newState.dy = (1.0 - FRICTION) * curState.dy + FRICTION * ddy
@ -27,7 +28,7 @@ local function Integrate( step )
newState.y = curState.y + newState.dy * step * MAXSPEED
end
local function OnImpact( impact )
function marble.OnImpact( impact )
--Adjust current trajectory according to collision.
if not impact.dt then return end
@ -66,14 +67,14 @@ local function OnImpact( impact )
curState.x, curState.y = x, y
curState.dx, curState.dy = vxout, vyout
debugRenderImpact = { xi = x, yi = y, xf = x - vx, yf = y - vy,
--[[debugRenderImpact = { xi = x, yi = y, xf = x - vx, yf = y - vy,
xn = 0.2 * unx + x, yn = 0.2 * uny + y,
vxout = x + vxout, vyout = y + vyout}
vxout = x + vxout, vyout = y + vyout}]]
return Integrate( math.max( impact.dt , 1 / 120 ) ) --Hmm! Maybe this should be a fixed step instead for stability's sake.
return marble.Integrate( math.max( impact.dt , 1 / 120 ) ) --Hmm! Maybe this should be a fixed step instead for stability's sake.
end
local function Update()
function marble.Update()
--Roll the log.
for k, v in pairs( oldState ) do
oldState[k] = curState[k]
@ -83,7 +84,7 @@ end
local function OnKey()
function marble.OnKey()
ddx = (love.keyboard.isScancodeDown( "d" ) and 1.0 or 0.0) - (love.keyboard.isScancodeDown( "a" ) and 1.0 or 0.0)
ddy = (love.keyboard.isScancodeDown( "w" ) and 1.0 or 0.0) - (love.keyboard.isScancodeDown( "s" ) and 1.0 or 0.0)
@ -92,11 +93,35 @@ local function OnKey()
ddx, ddy = ddx / n, ddy / n
end
local function Draw()
function marble.OnVictory()
marble.Draw = function()
--Extrapolate forward for slightly smoother rendering.
local dt = love.timer.getTime() - curState.t
marble.Integrate( dt + 1 / 60.0 )
local xp, yp = transform:transformPoint( oldState.x, oldState.y )
local xc, yc = transform:transformPoint( curState.x, curState.y )
local xn, yn = transform:transformPoint( newState.x, newState.y )
love.graphics.setColor( 91 / 255, 206 / 255, 250 / 255, 1.0 ) --Trans blue.
love.graphics.setLineJoin( "bevel" )
love.graphics.setLineStyle( "smooth" )
love.graphics.setLineWidth( 8.0 )
love.graphics.line( xp, yp, xc, yc, xn, yn)
love.graphics.circle( "fill", xn, yn, 4.0 )
love.graphics.setCanvas()
end
end
function marble.Draw()
--Extrapolate forward for slightly smoother rendering.
local dt = love.timer.getTime() - curState.t
Integrate( dt + 1 / 60.0 )
marble.Integrate( dt + 1 / 60.0 )
local xp, yp = transform:transformPoint( oldState.x, oldState.y )
local xc, yc = transform:transformPoint( curState.x, curState.y )
@ -130,12 +155,14 @@ local function Draw()
end
local function Impact( impact )
marble._Draw = marble.Draw
function marble.Impact( impact )
end
--Window resize.
local function Resize()
function marble.Resize()
newBuffer = love.graphics.newCanvas()
--TODO: render oldBuffer to new newBuffer, but scaled down.
oldBuffer = love.graphics.newCanvas()
@ -143,25 +170,18 @@ local function Resize()
end
local function Reset()
function marble.Reset()
oldState, curState, newState = State(), State(), State()
Resize()
marble.Resize()
marble.Draw = marble._Draw
end
function marble.Canvas()
return newBuffer
end
Reset()
marble.Reset()
return {
Integrate = Integrate,
OnImpact = OnImpact,
Update = Update,
OnKey = OnKey,
Draw = Draw,
Impact = Impact,
Reset = Reset,
Resize = Resize,
Current = Current,
Next = Next,
GetAcceleration = GetAcceleration,
}
return marble

BIN
screenshots/a.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
screenshots/acrop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
screenshots/b.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
screenshots/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

BIN
screenshots/headboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -6,12 +6,32 @@ local info = love.filesystem.getInfo( "sitelenpona" )
for _, filename in ipairs(love.filesystem.getDirectoryItems( "sitelenpona" )) do
sp[filename:gsub( ".png", "" )] = love.graphics.newImage( "sitelenpona/"..filename )
end
--Render one glyph in the center.
sp.Draw = function( str )
local w, h = love.graphics.getDimensions()
local x, y = 0.5 * w - 128, 0.5 * h - 128
--Render one or two glyphs in the center.
sp.Draw = function( t )
local cx, cy = love.graphics.getDimensions()
cx, cy = 0.5 * cx, 0.5 * cy
love.graphics.setColor( 1.0, 1.0, 1.0, 0.5 )
love.graphics.draw( sp[str] or sp.q, x, y )
if #t == 1 then
love.graphics.draw( sp[t[1]] or sp.pilin, cx - 128, cy - 128 )
elseif #t == 2 then
love.graphics.draw( sp[t[1]] or sp.pilin, cx - 128, cy - 64, 0, 0.5, 0.5 )
love.graphics.draw( sp[t[2]] or sp.pilin, cx, cy - 64, 0, 0.5, 0.5 )
elseif #t == 3 then
love.graphics.draw( sp[t[1]] or sp.pilin, cx - 128, cy - 64, 0, 0.5, 0.5 )
love.graphics.draw( sp[t[1]] or sp.pilin, cx - 128, cy - 64, 0, 0.5, 0.5 )
love.graphics.draw( sp[t[2]] or sp.pilin, cx, cy - 64, 0, 0.5, 0.5 )
end
--[[ w in str:gmatch( "%a+") do
t[i] = w
i = i + 1
end
for _, str in ipairs( t ) do
local w, h = love.graphics.getDimensions()
local x, y = 0.5 * w - 128, 0.5 * h - 128
love.graphics.draw( sp[str] or sp.pilin, x, y )
end]]
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 903 B

View File

@ -1,17 +1,72 @@
--Load poems.
local love = love
local txt = {}
local feet = {}
local feetNL = {}
local words = {} --Sequence of tables.
local poem = {[0] = ""}
local poemLines = {[0] = 0}
local info = love.filesystem.getInfo( "text" )
for _, filename in ipairs(love.filesystem.getDirectoryItems( "text" )) do
local s = love.filesystem.read( "text/"..filename )
local t = {}
local i = 1
for w in s:gmatch( "%a+") do
t[i] = w
i = i + 1
end
txt[filename:gsub( ".txt", "" )] = t
local mt = { __index = function() return "linja" end }
local s = love.filesystem.read( "text/tok.txt" )
local smallFont = love.graphics.setNewFont( "text/linja-sike.ttf", 12 )
local largeFont = love.graphics.setNewFont( "text/linja-sike.ttf", 32 )
local i = 1
--Split string into feet.
for foot in s:gmatch( ".-%-") do
feet[i] = foot:gsub( "-", "" ):gsub( " $", "" ):gsub( "%c", "") --Remove hyphens, trailing spaces, newlines.
feetNL[i] = foot:gsub( "-", "" ):gsub(" ", "")--:gsub( "%.", " " ) --Remove hyphens, spaces, transform periods to spaces
poem[i] = poem[ i - 1 ]..feetNL[i]
poemLines[i] = poemLines[i - 1] + (feetNL[i]:match( "\n") and 1 or 0)
i = i + 1
end
return txt
--Split string into words.
i = 1
local isTe = true
for foot in s:gmatch( ".-%-" ) do
--Split foot into words.
local word = {}
local j = 1
for w in foot:gmatch( [["]] ) do
word[j] = isTe and "te" or "to"
isTe = not( isTe )
j = j + 1
end
for w in foot:gmatch( "%a+" ) do
word[j] = w
j = j + 1
end
words[i] = word
i = i + 1
end
local function Draw( beat )
local foot = feet[ beat ]
love.graphics.setColor(1.0, 1.0, 1.0, 1.0)
love.graphics.printf(
foot,
0, (9/10) * love.graphics.getHeight(),
love.graphics.getWidth(),
"center")
love.graphics.setColor(1.0, 1.0, 1.0, 0.5)
love.graphics.print( poem[beat], 0, (3 - poemLines[beat]) * 32 )
end
local Reset = function() end
return { feet = feet, words = words, feetNL = feetNL, Reset = Reset, Draw = Draw }

BIN
text/linja-sike.ttf Normal file

Binary file not shown.

View File

@ -1,120 +1,53 @@
a
akesi
ala
alasa
ale
anpa
ante
anu
awen
e
en
esun
ijo
ike
ilo
insa
jaki
jan
jelo
jo
kala
kalama
kama
kasi
ken
kepeken
kili
kiwen
ko
kon
kule
kulupu
kute
la
lape
laso
lawa
len
lete
li
lili
linja
lipu
loje
lon
luka
lukin
lupa
ma
mama
mani
meli
mi
mije
moku
moli
monsi
mu
mun
musi
mute
nanpa
nasa
nasin
nena
ni
nimi
noka
o
olin
ona
open
pakala
pali
palisa
pan
pana
pi
pilin
pimeja
pini
pipi
poka
poki
pona
pu
sama
seli
selo
seme
sewi
sijelo
sike
sin
sina
sinpin
sitelen
sona
soweli
suli
suno
supa
suwi
tan
taso
tawa
telo
tenpo
toki
tomo
tu
unpa
uta
utala
walo
wan
waso
wawa
weka
wile
open -la mi -jan pi -toki-
ala -li mu -taso -li ken-
ala -pana -toki -pilin-
toki -mu li -wawa -wawa
nasa -la jan -ante -li ken-
awen -pona -tawa -mi li-
awen -ala-sa e -pona.-
moku -mi li -tan jan -pona-
mama -mi li -pona -mute-
mama -tu li -lon. tu -ona-
o mi -taso. -wile -mute-
li lon -sije-lo mi -ike.-
mute -la mi -pilin -nasa-
a mi -wile -musi -mute-
wile -mi li -ni: ko -jelo-
ni o -kama -tomo -wawa-
tomo -ni li -jo e -poki-
poki l-a jan -mi li -lawa-
lawa -la jan -ni li -toki-
"tomo -mi li -pona -mute-
paka-la o -kama -ala-
weka -o ken -ala -kama"-
wile -ala -mi la -ni li-
kama: -jan li -paka-la e-
tomo -suli -pona -mi a-
ni li -ike -tawa -pilin-
jan li -ni tan -seme -ike-
mi ken -ala -sona -pona-
wile -toki -li lon -ala-
la mi -tawa -mama -kiwen-
tenpo ni -mute li -kama li -weka la-
sijelo -ike li -ante a -mute
pilin mi -ala li -awen kin -sama-
wile mi su-li en wi-le mi li-li li-
nasa lon -tenpo ni. -mi kama -suli-
toki pi jan mute li kama ike. jan
mute li wile e pakala taso
lili li wile e sona e nasin
mute li wile e ni ala. nasa
mi toki ala lon poka pi jan ni
seme la mi wile alasa toki
jan ni la pilin mi li nasa mute
jan ni la musi mi li musi ala
seme la mi o lon poka jan ike
jan ike ni li ken pona e ala
mute la ona li wile e tawa
ala la wile ona li lon pali
mute la mi awen lon insa lawa
lawa la mi toki mute e pilin
lon lawa ala la mi lukin mute
lukin li wile e musi e sona
sitelen la m ken moku ni mute

View File

@ -1,9 +1,9 @@
--Render and simulate 1D wave equation.
local love = love
local N = 33
local SOUNDSPEED = 0.5
local IMPULSESIZE = 5
local DAMPING = 0.01
local SOUNDSPEED
local IMPULSESIZE
local DAMPING
local old, cur, new --States add beginning of last tick, current tick, and next tick respectively.
@ -47,8 +47,9 @@ local shader = love.graphics.newShader([[
p.y = -p.y;
float r = r( atan(p.y, p.x) ) - length( p );
float q = float( r < 0.01 ) * clamp( 1.0 - score, 0.0, 1.0 ) ;
return vec4( (1.0 + float(score > 1.0) * r * 0.1) * color.xyz, float(r > -0.01)) ;
return vec4( q + (1.0 + clamp( score, 0.0, 1.0 ) * r * r * 0.2) * color.xyz, float(r > 0.0)) ;
}
]])
@ -146,9 +147,15 @@ local function AliasedSinc( theta, x )
return n / d
end
--Apply bandlimited impulse to wave.
local function OnImpact( impact )
--Apply bandlimited impulse to wave, adjust free parameters according to game state.
local function OnImpact( impact, level )
IMPULSESIZE = 10.0 * math.sqrt( level / 120.0 )
SOUNDSPEED = 50.0 - 35 * level / 120
DAMPING = 0.03 * ( 1.0 - 0.4 * level / 120 )
--Apply bandlimited impulse.
local r = cur.radii
local theta = impact.th
local magnitude = IMPULSESIZE * impact.speed
@ -272,11 +279,14 @@ end
Integrate = function( step )
for i = 1, N do
local rxx = cur:SecondDerivative( math.pi * 2.0 * ( i - 1 ) / N )
local r = ( 1.0 - DAMPING ) * ( 2.0 * cur.radii[i] - old.radii[i] + step * step * SOUNDSPEED * rxx ) --Verlet
+ DAMPING --Damping: oscillate toward 1.
if r > 1.5 then r = 1.5 end
if r < 0.5 then r = 0.5 end
--Avoid explosions.
r = math.max( 0.5, math.min( r, 4.0 ) )
new.radii[i] = r
end
@ -290,10 +300,10 @@ end
local function Reset()
SOUNDSPEED = 0.5
IMPULSESIZE = 5
DAMPING = 0.02
IMPULSESIZE = 1 / 10.0
SOUNDSPEED = 5.0
DAMPING = 0.1 / 1
old = Wave()
cur = Wave()
new = Wave()