diff --git a/audio.lua b/audio.lua new file mode 100644 index 0000000..9866abf --- /dev/null +++ b/audio.lua @@ -0,0 +1,111 @@ +local love = love +local passChimes = {} +local failChimes = {} +local drones = {} +local N = 8 + +local function StartDrones() + love.audio.setEffect( "droneReverb", { + type = "reverb", + volume = 1.0, } + +) + + 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.alto:setVolume( 0 ) + drones.bass:setPitch( 0.5 ) + drones.bass:setEffect( "droneReverb", true ) + + love.audio.play( drones.bass ) + love.audio.play( drones.alto ) +end + +love.audio.setEffect( "passChimeReverb", { + type = "reverb", + volume = 1.0, } ) + +local passChime = love.audio.newSource("sounds/chimeb.flac", "static") +passChime:setEffect( "passChimeReverb", true ) +local failChime = love.audio.newSource("sounds/chime.flac", "static") +failChime:setEffect( "passChimeReverb", true ) + +for i = 1, N do + passChimes[i] = passChime:clone() + failChimes[i] = failChime:clone() +end + +local idxNextPassChime = 1 +local idxNextFailChime = 1 +local function GetNextPassChime( pass ) + idxNextPassChime = idxNextPassChime % N + 1 + return passChimes[ idxNextPassChime ] +end + +local function GetNextFailChime( pass ) + idxNextFailChime = idxNextFailChime % N + 1 + return failChimes[ idxNextFailChime ] +end + +local pitches = +{ + 1, + 2/3, + 3/2, + 9/8, + 27/16, + 81/64, + 243/128, + 2 --Safety +} + +local function GetPitch( th ) + th = 0.5 + 0.5 * th / math.pi + return pitches[ 1 + math.floor( th * 7 )] +end + +local function OnImpact( impact, score, pass, level ) + + -- + if not pass then + --drones.bass:seek( level ) + -- drones.alto:seek( level ) + end + + local th = impact.th + local r = impact.r + + -- Chimes + local chime = pass and GetNextPassChime() or GetNextFailChime() + chime:setPitch( GetPitch( th ) ) + chime:setPosition( r * math.cos( th ), r * math.sin( th ) ) + local highgain = math.max( 0, math.min( score, 1.0 ) - (pass and 0.0 or 0.5)) + chime:setFilter{ + type = "lowpass", + volume = 0.5 * impact.speed, + highgain = highgain, + } + love.audio.stop( chime ) + love.audio.play( chime ) + +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 ) +end + +local function Reset( ) + for i = 1, N do + love.audio.stop( passChimes[i] ) + love.audio.stop( failChimes[i] ) + end + StartDrones() +end + +Reset() +return { OnImpact = OnImpact, Update = Update, Reset = Reset} \ No newline at end of file diff --git a/main.lua b/main.lua index 9039522..5956b11 100644 --- a/main.lua +++ b/main.lua @@ -4,12 +4,12 @@ local sitelenpona local text local marble local wave +local particles +local audio +local recorder local DetectCollision - -local sounds = { - - -} +local OnImpact +local OnVictory local state @@ -52,17 +52,52 @@ function love.load() love.graphics.setBackgroundColor( 245 / 255, 169 / 255, 184 / 255 ) --Trans pink. --love.graphics.setBackgroundColor( 91 / 255, 206 / 255, 250 / 255 ) --Trans blue. - sounds.goodPing = love.audio.newSource("sounds/soundTest.ogg", "static") - sounds.badPing = love.audio.newSource("sounds/chime8.ogg", "static") + + do--particle system setup + particles = love.graphics.newParticleSystem( + love.graphics.newImage( "prideflag.png" ), + 1024) + + particles:setSizes( 0.00015, 0.0001, 0.00018 ) + particles:setSizeVariation( 1 ) + particles:setRadialAcceleration( 0, 0.5 ) + particles:setSpeed( 0.2, 1 ) + particles:setLinearDamping( 1, 10 ) + particles:setRelativeRotation( true ) + particles:setEmitterLifetime( -1 ) + particles:setParticleLifetime( 0, 15 ) + particles:setSpread( 0.05 ) + particles:setColors( + 1, 1, 1, 1, + 91 / 255, 206 / 255, 250 / 255, 1, + 245 / 255, 169 / 255, 184 / 255, 1, + 1,1,1,0 + + ) + end + + sitelenpona = assert( require "sitelenpona" ) text = assert( require "text" ) marble = assert( require "marble" ) wave = assert( require "wave" ) DetectCollision = assert ( require "collision" ) + audio = assert( require "audio" ) + recorder = assert( require "recorder" ) return state.Reset() end +local function ExtrapolateBeatScore( ) + 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 ) + return t * t * t * t +end local function BeatScore( t ) local beat = state.beat @@ -82,63 +117,61 @@ local function BeatScore( t ) return 2.0 end + local score = 1.1 * math.sin( math.pi * dt / beat.mu ) + score = score * score * score * score + --General case: update average beat length. - local WEIGHT = 0.25 --High number makes the last beat more significant. - local mu = ( 1.0 - WEIGHT ) * beat.mu + WEIGHT * dt - beat.mu = mu + if dt > 0.2 then + + local WEIGHT = 0.85 --High number makes the last beat more significant. + local mu = ( 1.0 - WEIGHT ) * beat.mu + WEIGHT * dt + beat.mu = mu - --Safety: avoid a dbz. - if mu < 0.001 or dt < 0.001 then - return 0.0 - --error( "DBZ! Beat length too small." ) - end - - --Calculate beat score. - local score = dt * dt / ( mu * mu ) - local TOLERANCE = 1.04 - if dt < mu then - return TOLERANCE * score - else - return TOLERANCE / score end + return score end -local function OnVictory() - -end - -local function OnImpact( impact ) +OnImpact = function( impact ) if not impact then return end local score = BeatScore( impact.t ) --DEBUG state.lastBeatScore = score + local pass = false - local sound - if score > state.beatScoreThreshold then - sound = sounds.goodPing + if score > 1.0 then - state.beatScoreThreshold = 1.0 + pass = true state.currentBeat = state.currentBeat + 1 if state.currentBeat >= 120 then return OnVictory() end - - - else - - sound = sounds.badPing - state.beatScoreThreshold = state.beatScoreThreshold - 0.05 - end - love.audio.play( sound ) - + + 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() + OnImpact = function() end +end function love.draw() @@ -149,36 +182,42 @@ function love.draw() love.graphics.print( state.beatScoreThreshold, 0, 20) love.graphics.print( state.lastBeatScore, 0, 30 )]] - + local score = ExtrapolateBeatScore() love.graphics.push( "transform" ) love.graphics.applyTransform( transform ) - wave.Draw() - - if debugRenderImpact then - - love.graphics.setLineWidth( 0.01 ) - love.graphics.setColor( 1, 0, 0, 0.5 ) --Red: Incoming - love.graphics.line( - debugRenderImpact.xi, - debugRenderImpact.yi, - debugRenderImpact.xf, - debugRenderImpact.yf) - love.graphics.setColor( 0, 1, 0, 0.5 ) --Green: Normal - love.graphics.line( - debugRenderImpact.xi, - debugRenderImpact.yi, - debugRenderImpact.xn, - debugRenderImpact.yn) - love.graphics.setColor( 0, 0, 1, 0.5 ) -- Blue: Outgoing + + + + wave.Draw( score ) + + love.graphics.setColor( 1.0, 1.0, 1.0, 1.0 ) + love.graphics.draw(particles, 0, 0) + + --[[if debugRenderImpact then + + love.graphics.setLineWidth( 0.01 ) + love.graphics.setColor( 1, 0, 0, 0.5 ) --Red: Incoming love.graphics.line( - debugRenderImpact.xi, - debugRenderImpact.yi, - debugRenderImpact.vxout, - debugRenderImpact.vyout) - - end - + debugRenderImpact.xi, + debugRenderImpact.yi, + debugRenderImpact.xf, + debugRenderImpact.yf) + love.graphics.setColor( 0, 1, 0, 0.5 ) --Green: Normal + love.graphics.line( + debugRenderImpact.xi, + debugRenderImpact.yi, + debugRenderImpact.xn, + debugRenderImpact.yn) + love.graphics.setColor( 0, 0, 1, 0.5 ) -- Blue: Outgoing + love.graphics.line( + debugRenderImpact.xi, + debugRenderImpact.yi, + debugRenderImpact.vxout, + debugRenderImpact.vyout) + + end]] + love.graphics.pop() sitelenpona.Draw( text.tok[state.currentBeat] ) @@ -191,19 +230,26 @@ end function love.update( dt ) + + audio.Update( ExtrapolateBeatScore() ) + particles:update( dt ) dt = dt + state.timeToSimulate + + --Physics tick. while dt > step do + recorder.Update( marble.GetAcceleration() ) --For savegames. + marble.Integrate( step ) wave.Integrate( step ) - + OnImpact( DetectCollision( marble.Current(), marble.Next(), wave.Current(), wave.Next() )) - + marble.Update() wave.Update() - + dt = dt - step end state.timeToSimulate = dt @@ -211,7 +257,8 @@ end function love.keypressed( key, code, isRepeat ) if key == "escape" then return love.event.quit() end - if key == "return" then return OnImpact{ t = love.timer.getTime() } end + if key == "return" then return OnVictory() end + if key == "space" then return NewGame() end return marble.OnKey() end diff --git a/marble.lua b/marble.lua index 06db986..595b4bf 100644 --- a/marble.lua +++ b/marble.lua @@ -4,14 +4,18 @@ local oldBuffer = love.graphics.newCanvas() local newBuffer = love.graphics.newCanvas() local oldState, curState, newState -local FRICTION = 0.01 -local MAXSPEED = 5.0 +local FRICTION = 0.05 +local MAXSPEED = 4 local ddx, ddy = 0.0, 0.0 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 @@ -60,13 +64,13 @@ local function OnImpact( impact ) inward * (- math.sin(impact.th) ) - (1.0 - inward) * ( x * s + vy * c ) curState.x, curState.y = x, y - curState.dx, curState.dy = 0.5 * vxout, 0.5 * vyout + curState.dx, curState.dy = vxout, vyout 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} - return Integrate( math.max( impact.dt , 1 / 60 ) ) --Hmm! Maybe this should be a fixed step instead for stability's sake. + return Integrate( math.max( impact.dt , 1 / 120 ) ) --Hmm! Maybe this should be a fixed step instead for stability's sake. end local function Update() @@ -120,7 +124,7 @@ local function Draw() love.graphics.draw( newBuffer ) love.graphics.setColor( 1, 1, 1, 1.0 ) --White. love.graphics.setLineWidth( 1.0 ) - love.graphics.circle( "line", xn, yn, 4 ) + --love.graphics.circle( "line", xn, yn, 4 ) oldBuffer, newBuffer = newBuffer, oldBuffer @@ -159,4 +163,5 @@ return { Resize = Resize, Current = Current, Next = Next, + GetAcceleration = GetAcceleration, } \ No newline at end of file diff --git a/prideflag.png b/prideflag.png new file mode 100644 index 0000000..64f051c Binary files /dev/null and b/prideflag.png differ diff --git a/recorder.lua b/recorder.lua new file mode 100644 index 0000000..2ac4364 --- /dev/null +++ b/recorder.lua @@ -0,0 +1,32 @@ +--Record demos. +local love = love +local recorder = {} +recorder.isLoaded = false +local i = 0 + +local function Reset() + i = 0 +end + +function recorder.Update( ddx, ddy ) + local byte = 0 + if ddx > 0.5 then byte = byte + 1 end + if ddy > 0.5 then byte = byte + 2 end + if ddx < -0.5 then byte = byte + 4 end + if ddy < -0.5 then byte = byte + 8 end + + i = i + 1 + recorder[i] = string.char( byte ) +end + +function recorder.Load( ) + local s = love.filesystem.read( "demo" ) + + recorder.isLoaded = true +end + +function recorder.Save( ) + assert( love.filesystem.write( "demo", table.concat( recorder ) ) ) +end + +return recorder \ No newline at end of file diff --git a/sounds/chime.flac b/sounds/chime.flac new file mode 100644 index 0000000..92a5a8f Binary files /dev/null and b/sounds/chime.flac differ diff --git a/sounds/chime8.ogg b/sounds/chime8.ogg index 96174b9..ecfc4d7 100644 Binary files a/sounds/chime8.ogg and b/sounds/chime8.ogg differ diff --git a/sounds/chimeb.flac b/sounds/chimeb.flac new file mode 100644 index 0000000..2490258 Binary files /dev/null and b/sounds/chimeb.flac differ diff --git a/sounds/drone.flac b/sounds/drone.flac new file mode 100644 index 0000000..2801876 Binary files /dev/null and b/sounds/drone.flac differ diff --git a/wave.lua b/wave.lua index d493deb..8e32c11 100644 --- a/wave.lua +++ b/wave.lua @@ -2,7 +2,7 @@ local love = love local N = 33 local SOUNDSPEED = 0.5 -local IMPULSESIZE = 20 +local IMPULSESIZE = 5 local DAMPING = 0.01 @@ -20,6 +20,7 @@ local shader = love.graphics.newShader([[ uniform float re[33]; uniform float im[33]; + uniform float score; //Slow IDFT float r( float x ) @@ -47,7 +48,7 @@ local shader = love.graphics.newShader([[ float r = r( atan(p.y, p.x) ) - length( p ); - return vec4(color.x, color.y, color.z, (r > -0.01)) ; + return vec4( (1.0 + float(score > 1.0) * r * 0.1) * color.xyz, float(r > -0.01)) ; } ]]) @@ -175,8 +176,6 @@ local function Wave( ) local t = { --radii[k] = radius of point on curve at angle (k - 1) / N radii = {}, - --TIME derivative of radius - vrad = {}, --SPACE DFT of radius function (which is periodic) dftre = {}, @@ -184,21 +183,21 @@ local function Wave( ) } for i = 1, N do - t.radii[i] = 1.0 + 0.05 * ( i/N + math.sin( i * 2.0 * math.pi / N )) - t.vrad[i] = 0.0 + t.radii[i] = 1.0 end DFT( t ) return setmetatable(t, mt) end -local function Draw() +local function Draw( score ) -- Blue circle. love.graphics.setColor( 91 / 255, 206 / 255, 250 / 255 ) shader:send( "re", unpack( cur.dftre ) ) shader:send( "im", unpack( cur.dftim ) ) + shader:send( "score", score ) love.graphics.setShader( shader ) love.graphics.circle("fill", 0, 0, 1.5) local t = love.timer.getTime() @@ -253,7 +252,7 @@ local function Draw() end -local function Update() +local function Update( ) --Deep copy of current state to old state. for name, t in pairs( cur ) do @@ -290,6 +289,11 @@ local function DetectCollision( px, py, vpx, vpy ) end local function Reset() + + SOUNDSPEED = 0.5 + IMPULSESIZE = 5 + DAMPING = 0.02 + old = Wave() cur = Wave() new = Wave()