From 7a0f64c30b465e23993b92cd61f94eaa15a11e31 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Fri, 17 Apr 2020 22:41:08 -0400 Subject: [PATCH 01/11] Add some stubby stubs --- docs/assets/js/sounds/soundassetmap.js | 5 +++++ docs/assets/js/sounds/soundcontext.js | 27 +++++++++++++++++++++++++ docs/assets/js/sounds/sounds.js | 13 ++++++++++++ docs/assets/js/sounds/soundsnippet.js | 28 ++++++++++++++++++++++++++ docs/index.html | 8 ++++++++ 5 files changed, 81 insertions(+) create mode 100644 docs/assets/js/sounds/soundassetmap.js create mode 100644 docs/assets/js/sounds/soundcontext.js create mode 100644 docs/assets/js/sounds/sounds.js create mode 100644 docs/assets/js/sounds/soundsnippet.js diff --git a/docs/assets/js/sounds/soundassetmap.js b/docs/assets/js/sounds/soundassetmap.js new file mode 100644 index 0000000..faa51cb --- /dev/null +++ b/docs/assets/js/sounds/soundassetmap.js @@ -0,0 +1,5 @@ + +// A mapping of asset names to their files +let soundAssets = { + +} \ No newline at end of file diff --git a/docs/assets/js/sounds/soundcontext.js b/docs/assets/js/sounds/soundcontext.js new file mode 100644 index 0000000..555b202 --- /dev/null +++ b/docs/assets/js/sounds/soundcontext.js @@ -0,0 +1,27 @@ + +class SoundChannel{ + + /** + * Create a sound channel + * @param {number} max_queue_size Maximum number of sounds that can be queued before sounds are skipped + */ + constructor(max_queue_size) { + this.max_size = max_queue_size + } +} + + +class SoundContext{ + + constructor() { + + // Define all sound channels + this.channels = { + bgm: new SoundChannel(2) + } + } + +} + +// The global context for sounds +let globalSoundContext = new SoundContext(); \ No newline at end of file diff --git a/docs/assets/js/sounds/sounds.js b/docs/assets/js/sounds/sounds.js new file mode 100644 index 0000000..547b678 --- /dev/null +++ b/docs/assets/js/sounds/sounds.js @@ -0,0 +1,13 @@ + +// All available sounds +let sounds = { + +} + +/** + * Cache all sounds in browser, then notify a callback of success + * @param {function} callback Callback for completion + */ +function preCacheSounds(callback) { + +} \ No newline at end of file diff --git a/docs/assets/js/sounds/soundsnippet.js b/docs/assets/js/sounds/soundsnippet.js new file mode 100644 index 0000000..6ee8178 --- /dev/null +++ b/docs/assets/js/sounds/soundsnippet.js @@ -0,0 +1,28 @@ + +class SoundSnippet{ + + /** + * Load a sound asset to a snipper + * @param {string} asset_name Asset name as defined in soundassetmap.js + */ + constructor(asset_name) { + + } + + /** + * Cache this sound, then notify a callback of completion + * @param {function} callback callback to notify + */ + cache(callback) { + + } + + + play() { + + } + + stop() { + + } +} \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index e925d13..08c34f0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -32,6 +32,14 @@ + + + + + + + + From 44a42ba4c86acebca118a1b69be55644e5611e83 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Sat, 18 Apr 2020 04:36:23 -0400 Subject: [PATCH 02/11] Heartrate monitor --- .gitignore | 5 ++- docs/assets/js/UI/ui.js | 54 ++++++++++++++++++++------ docs/assets/js/constants.js | 11 +++--- docs/assets/js/game.js | 5 ++- docs/assets/js/player/lifeFunctions.js | 5 +-- docs/assets/js/playing/playing.js | 2 + docs/index.html | 8 ++-- 7 files changed, 63 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 2f41c87..10c4a25 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ docs/_site/* docs/.sass-cache/* docs/.jekyll-cache/* -docs/node_modules \ No newline at end of file +docs/node_modules + +# idea +.idea \ No newline at end of file diff --git a/docs/assets/js/UI/ui.js b/docs/assets/js/UI/ui.js index a2dfd21..5a8175a 100644 --- a/docs/assets/js/UI/ui.js +++ b/docs/assets/js/UI/ui.js @@ -1,23 +1,33 @@ +//Colors + // UI for title screen -function drawTitleScreenUI() {}; +function drawTitleScreenUI() { +} // UI for level transition -function drawLevelTransitionUI() {}; +function drawLevelTransitionUI() { +} // UI for playing -function drawPlayingUI() {}; +function drawPlayingUI() { + heartBeatUI(cw/4*3-8,ch/8*7-8,cw/4,ch/8); +} //UI for pause screen -function drawPausedUI() {}; +function drawPausedUI() { +} //UI for game end -function drawEndUI() {}; +function drawEndUI() { +} // Construct a rectangular UI function rectUI() {}; //Heart rate monitor history -var heartBeatHistory = [].fill(0,0, constants.ui.heartRate.history_length); +var heartBeatHistory = [] + heartBeatHistory.length = constants.ui.heartRate.history_length; + heartBeatHistory.fill(0); //Shift accumulation var shiftAccum = 0; @@ -31,6 +41,7 @@ function heartBeatUI(x, y, width, height){ shiftAccum += constants.ui.heartRate.scroll_speed; if(shiftAccum>=1){ shiftAccum%=1; + beatTimeElapsed += 0.04; heartBeatHistory.shift(); pushNextBeatValue(); } @@ -39,19 +50,38 @@ function heartBeatUI(x, y, width, height){ beatTimeElapsed = 0; } + rect(x+width/2,y+height/2,width,height,"black"); for (let index = 0; index < heartBeatHistory.length; index++) { const qrsValueAtPosition = heartBeatHistory[index]; - line(x+index, y+(2*height/3), x+index, y+(2*height/3)+qrsValueAtPosition); + const qrsValueAtNextPosition = heartBeatHistory[index+1]; + line(x+(index*width/heartBeatHistory.length), y+(2*height/3)+(qrsValueAtPosition*width/heartBeatHistory.length), x+((index+1)*width/heartBeatHistory.length), y+(2*height/3)+(qrsValueAtNextPosition*width/heartBeatHistory.length), "red"); } } function pushNextBeatValue(){ - var nextBeatValue; + var nextBeatValue = 0; - beatTimeElapsed %= constants.ui.heartRate.complex_width; - if(beatTimeElapsed<=constants.ui.heartRate.pr_width){ - nextBeatValue = -0.25((x - 1.5)**2) + 0.5625; - } else if (beatTimeElapsed >= constants.ui.heartRate.pr_width + 1 && beatTimeElapsed <= constants.ui.heartRate.pr_width + 1 + constants.ui.heartRate.qrs_width/4) { + const squareSize = constants.ui.heartRate.square_size; + const complexTime = constants.ui.heartRate.complex_width*squareSize; + const prTime = constants.ui.heartRate.pr_width*squareSize; + const qrsTime = constants.ui.heartRate.qrs_width*squareSize; + const qtTime = constants.ui.heartRate.qt_width*squareSize; + + + if(beatTimeElapsed<=complexTime) { + if (beatTimeElapsed <= prTime) { + nextBeatValue = 0.5*(Math.pow((beatTimeElapsed/squareSize - (prTime/2/squareSize)), 2)) - 2; + } else if (beatTimeElapsed > prTime + squareSize && beatTimeElapsed <= prTime + squareSize + (qrsTime / 4)) { + nextBeatValue = -4 + beatTimeElapsed/squareSize; + } else if (beatTimeElapsed > prTime + squareSize + qrsTime / 4 && beatTimeElapsed <= prTime + squareSize + qrsTime / 2) { + nextBeatValue = -14 * (beatTimeElapsed/squareSize - 4.5) - 0.5; + } else if (beatTimeElapsed > prTime + squareSize + qrsTime / 2 && beatTimeElapsed <= prTime + squareSize + (3*qrsTime / 4)) { + nextBeatValue = 7 * (beatTimeElapsed/squareSize - 5) - 6.5; + } else if (beatTimeElapsed > prTime + squareSize + (3*qrsTime / 4) && beatTimeElapsed <= prTime + squareSize + qrsTime) { + nextBeatValue = 2 * (beatTimeElapsed/squareSize - 6); + } else if (beatTimeElapsed > prTime + squareSize*2 + qrsTime && beatTimeElapsed <= prTime + squareSize*2 + qrsTime + qtTime) { + nextBeatValue = 0.5 * Math.pow((beatTimeElapsed/squareSize - (prTime + squareSize*2 + qrsTime + qtTime/2)/squareSize),2) - 3; + } } heartBeatHistory.push(nextBeatValue); diff --git a/docs/assets/js/constants.js b/docs/assets/js/constants.js index 4570ac8..d5d7b7e 100644 --- a/docs/assets/js/constants.js +++ b/docs/assets/js/constants.js @@ -26,11 +26,12 @@ var constants = { history_length: 100, //300 squares/min - scroll_speed: 0.13333, - pr_width: 0.16, - qrs_width: 0.1, - qt_width: 0.39, - complex_width: 0.65 + scroll_speed: 0.8, + square_size: 0.08, + pr_width: 4, + qrs_width: 2, + qt_width: 5, + complex_width: 18 } }, legs:{ diff --git a/docs/assets/js/game.js b/docs/assets/js/game.js index dbbfc67..02bb89b 100644 --- a/docs/assets/js/game.js +++ b/docs/assets/js/game.js @@ -350,9 +350,10 @@ function circle(x,y,r,color) { function line(x1, y1, x2, y2, color) { curCtx.beginPath(); - curCtx.style = color; + curCtx.strokeStyle = color; curCtx.moveTo(x1 + camera.x + difx, y1 + camera.y + dify); - curCtx.lineTo(x2 + camera.x + difx, y2 + camera.y + dify); + curCtx.lineTo(x2 + camera.x + difx , y2 + camera.y + dify); + curCtx.stroke(); } function shape(x,y,relitivePoints,color) { diff --git a/docs/assets/js/player/lifeFunctions.js b/docs/assets/js/player/lifeFunctions.js index deb1cd0..c0edc9b 100644 --- a/docs/assets/js/player/lifeFunctions.js +++ b/docs/assets/js/player/lifeFunctions.js @@ -8,13 +8,13 @@ var timeSinceLastBeat = 0; function updateLife() { - if(keyDown[k.Z]) { + if(keyDown[k.z]) { breathe(); } else { breath--; } - if(keyPress[k.X]) { + if(keyPress[k.x]) { heartbeat(); } else { timeSinceLastBeat++; @@ -38,7 +38,6 @@ function breathe() { }; function heartbeat() { - timeSinceLastBeat = 0; }; \ No newline at end of file diff --git a/docs/assets/js/playing/playing.js b/docs/assets/js/playing/playing.js index 995b15f..006c491 100644 --- a/docs/assets/js/playing/playing.js +++ b/docs/assets/js/playing/playing.js @@ -3,4 +3,6 @@ function handlePlaying() { if(keyPress[k.BACKSLASH]) { globalState = globalStates.building; } + + updateLife(); } \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 051e7d8..ce9f65c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -33,13 +33,13 @@ + --> + - + + From 138f27a6916eec26934169851470e4a3437b34e2 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Sat, 18 Apr 2020 05:15:44 -0400 Subject: [PATCH 03/11] Heartrate monitor optimizations & comments --- docs/assets/js/UI/ui.js | 38 ++++++++++++++++++-------- docs/assets/js/player/lifeFunctions.js | 12 ++++---- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/docs/assets/js/UI/ui.js b/docs/assets/js/UI/ui.js index 5a8175a..de9eb84 100644 --- a/docs/assets/js/UI/ui.js +++ b/docs/assets/js/UI/ui.js @@ -25,32 +25,42 @@ function drawEndUI() { function rectUI() {}; //Heart rate monitor history -var heartBeatHistory = [] +let heartBeatHistory = [] heartBeatHistory.length = constants.ui.heartRate.history_length; heartBeatHistory.fill(0); //Shift accumulation -var shiftAccum = 0; +let shiftAccum = 0; //Beat progression -var beatTimeElapsed = 0; +let beatTimeElapsed = 0; // Draw heartbeat UI function heartBeatUI(x, y, width, height){ + //Shift monitor over once a full scrolling unit is accumulated shiftAccum += constants.ui.heartRate.scroll_speed; if(shiftAccum>=1){ shiftAccum%=1; beatTimeElapsed += 0.04; + + //Remove oldest value heartBeatHistory.shift(); + + //Append new value pushNextBeatValue(); } - if(timeSinceLastBeat===0){ + //If heart is beaten, reset beat timer. + if(heartBeat){ beatTimeElapsed = 0; + heartBeat = false; } + //Backdrop rect(x+width/2,y+height/2,width,height,"black"); + + //Graph for (let index = 0; index < heartBeatHistory.length; index++) { const qrsValueAtPosition = heartBeatHistory[index]; const qrsValueAtNextPosition = heartBeatHistory[index+1]; @@ -58,28 +68,34 @@ function heartBeatUI(x, y, width, height){ } } +//Determine next value to be added to the graph function pushNextBeatValue(){ - var nextBeatValue = 0; + let nextBeatValue = 0; + //Timespan of one "square" on the EKG const squareSize = constants.ui.heartRate.square_size; + //Length of full complex const complexTime = constants.ui.heartRate.complex_width*squareSize; + //Length of PR segment of complex const prTime = constants.ui.heartRate.pr_width*squareSize; + //Length of QRS component of complex const qrsTime = constants.ui.heartRate.qrs_width*squareSize; + //Length of QT component of complex const qtTime = constants.ui.heartRate.qt_width*squareSize; - if(beatTimeElapsed<=complexTime) { + //PR Segment if (beatTimeElapsed <= prTime) { nextBeatValue = 0.5*(Math.pow((beatTimeElapsed/squareSize - (prTime/2/squareSize)), 2)) - 2; - } else if (beatTimeElapsed > prTime + squareSize && beatTimeElapsed <= prTime + squareSize + (qrsTime / 4)) { + } else if (beatTimeElapsed > prTime + squareSize && beatTimeElapsed <= prTime + squareSize + (qrsTime / 4)) { //QRS Segment pt. 1 nextBeatValue = -4 + beatTimeElapsed/squareSize; - } else if (beatTimeElapsed > prTime + squareSize + qrsTime / 4 && beatTimeElapsed <= prTime + squareSize + qrsTime / 2) { + } else if (beatTimeElapsed > prTime + squareSize + qrsTime / 4 && beatTimeElapsed <= prTime + squareSize + qrsTime / 2) { //QRS Segment pt. 2 nextBeatValue = -14 * (beatTimeElapsed/squareSize - 4.5) - 0.5; - } else if (beatTimeElapsed > prTime + squareSize + qrsTime / 2 && beatTimeElapsed <= prTime + squareSize + (3*qrsTime / 4)) { + } else if (beatTimeElapsed > prTime + squareSize + qrsTime / 2 && beatTimeElapsed <= prTime + squareSize + (3*qrsTime / 4)) { //QRS Segment pt. 3 nextBeatValue = 7 * (beatTimeElapsed/squareSize - 5) - 6.5; - } else if (beatTimeElapsed > prTime + squareSize + (3*qrsTime / 4) && beatTimeElapsed <= prTime + squareSize + qrsTime) { + } else if (beatTimeElapsed > prTime + squareSize + (3*qrsTime / 4) && beatTimeElapsed <= prTime + squareSize + qrsTime) { //QRS Segment pt. 4 nextBeatValue = 2 * (beatTimeElapsed/squareSize - 6); - } else if (beatTimeElapsed > prTime + squareSize*2 + qrsTime && beatTimeElapsed <= prTime + squareSize*2 + qrsTime + qtTime) { + } else if (beatTimeElapsed > prTime + squareSize*2 + qrsTime && beatTimeElapsed <= prTime + squareSize*2 + qrsTime + qtTime) { //PT Segment nextBeatValue = 0.5 * Math.pow((beatTimeElapsed/squareSize - (prTime + squareSize*2 + qrsTime + qtTime/2)/squareSize),2) - 3; } } diff --git a/docs/assets/js/player/lifeFunctions.js b/docs/assets/js/player/lifeFunctions.js index c0edc9b..9fdf42f 100644 --- a/docs/assets/js/player/lifeFunctions.js +++ b/docs/assets/js/player/lifeFunctions.js @@ -1,9 +1,9 @@ -var breath = 180; -var fullBreathTimer = 0; -var heartRate = 60; +let breath = 180; +let fullBreathTimer = 0; +let heartRate = 60; -var timeSinceLastBeat = 0; +let heartBeat = false; function updateLife() { @@ -16,8 +16,6 @@ function updateLife() { if(keyPress[k.x]) { heartbeat(); - } else { - timeSinceLastBeat++; } }; @@ -38,6 +36,6 @@ function breathe() { }; function heartbeat() { - timeSinceLastBeat = 0; + heartBeat = true; }; \ No newline at end of file From f3332786d363260a2ffa4bd62e628cf1c3bc1220 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Sat, 18 Apr 2020 06:02:20 -0400 Subject: [PATCH 04/11] Simple Breath Gauge --- docs/assets/js/UI/ui.js | 32 ++++++++++++++++++++------ docs/assets/js/constants.js | 5 ++++ docs/assets/js/game.js | 8 ++++++- docs/assets/js/player/lifeFunctions.js | 6 ++--- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/docs/assets/js/UI/ui.js b/docs/assets/js/UI/ui.js index de9eb84..5350b2e 100644 --- a/docs/assets/js/UI/ui.js +++ b/docs/assets/js/UI/ui.js @@ -1,5 +1,3 @@ -//Colors - // UI for title screen function drawTitleScreenUI() { } @@ -10,7 +8,12 @@ function drawLevelTransitionUI() { // UI for playing function drawPlayingUI() { + + //Heart Rate Monitor heartBeatUI(cw/4*3-8,ch/8*7-8,cw/4,ch/8); + + //Respiration Monitor + respiratoryUI(cw/8*5,ch/8*7-8, cw/16, ch/8); } //UI for pause screen @@ -21,11 +24,26 @@ function drawPausedUI() { function drawEndUI() { } -// Construct a rectangular UI -function rectUI() {}; + +/*** + * + * RESPIRATORY UI + * + */ + +function respiratoryUI(x, y, width, height){ + cartesianRect(x,y,width,height, "black"); + cartesianRect(x,y+(height-breath/constants.lifeFuncs.breath.fullBreath*height), width, breath/constants.lifeFuncs.breath.fullBreath*height, "teal"); +} + +/*** + * + * HEART RATE MONITOR UI + * + */ //Heart rate monitor history -let heartBeatHistory = [] +let heartBeatHistory = []; heartBeatHistory.length = constants.ui.heartRate.history_length; heartBeatHistory.fill(0); @@ -33,7 +51,7 @@ let heartBeatHistory = [] let shiftAccum = 0; //Beat progression -let beatTimeElapsed = 0; +let beatTimeElapsed = Infinity; // Draw heartbeat UI function heartBeatUI(x, y, width, height){ @@ -64,7 +82,7 @@ function heartBeatUI(x, y, width, height){ for (let index = 0; index < heartBeatHistory.length; index++) { const qrsValueAtPosition = heartBeatHistory[index]; const qrsValueAtNextPosition = heartBeatHistory[index+1]; - line(x+(index*width/heartBeatHistory.length), y+(2*height/3)+(qrsValueAtPosition*width/heartBeatHistory.length), x+((index+1)*width/heartBeatHistory.length), y+(2*height/3)+(qrsValueAtNextPosition*width/heartBeatHistory.length), "red"); + line(x+(index*width/heartBeatHistory.length), y+(2*height/3)+(qrsValueAtPosition*width/heartBeatHistory.length), x+((index+1)*width/heartBeatHistory.length), y+(2*height/3)+(qrsValueAtNextPosition*width/heartBeatHistory.length),Math.min(3,Math.max(3/beatTimeElapsed,1)), "red"); } } diff --git a/docs/assets/js/constants.js b/docs/assets/js/constants.js index d5d7b7e..c4ef125 100644 --- a/docs/assets/js/constants.js +++ b/docs/assets/js/constants.js @@ -34,6 +34,11 @@ var constants = { complex_width: 18 } }, + lifeFuncs:{ + breath:{ + fullBreath: 200 + } + }, legs:{ size:{ maximumMovement: 30 diff --git a/docs/assets/js/game.js b/docs/assets/js/game.js index 02bb89b..262094c 100644 --- a/docs/assets/js/game.js +++ b/docs/assets/js/game.js @@ -341,6 +341,11 @@ function rect(x,y,w,h,color) { curCtx.fillRect(x-(w/2)+camera.x+difx,y-(h/2)+camera.y+dify,w,h); } +function cartesianRect(x,y,w,h,color) { + curCtx.fillStyle = color; + curCtx.fillRect(x+camera.x+difx,y+camera.y+dify,w,h); +} + function circle(x,y,r,color) { curCtx.beginPath(); curCtx.arc(x+camera.x+difx, y+camera.y+dify, r, 0, 2 * Math.PI, false); @@ -348,9 +353,10 @@ function circle(x,y,r,color) { curCtx.fill(); } -function line(x1, y1, x2, y2, color) { +function line(x1, y1, x2, y2, weight, color) { curCtx.beginPath(); curCtx.strokeStyle = color; + curCtx.lineWidth = weight; curCtx.moveTo(x1 + camera.x + difx, y1 + camera.y + dify); curCtx.lineTo(x2 + camera.x + difx , y2 + camera.y + dify); curCtx.stroke(); diff --git a/docs/assets/js/player/lifeFunctions.js b/docs/assets/js/player/lifeFunctions.js index 9fdf42f..0cd075e 100644 --- a/docs/assets/js/player/lifeFunctions.js +++ b/docs/assets/js/player/lifeFunctions.js @@ -17,14 +17,13 @@ function updateLife() { if(keyPress[k.x]) { heartbeat(); } - }; function breathe() { breath += 5; - if(breath >= 200) { - breath = 200; + if(breath >= constants.lifeFuncs.breath.fullBreath) { + breath = constants.lifeFuncs.breath.fullBreath; fullBreathTimer++; if(fullBreathTimer >= 60) { //cough and lose breath or something @@ -37,5 +36,4 @@ function breathe() { function heartbeat() { heartBeat = true; - }; \ No newline at end of file From 047f870f49ccb36842e582ecf3304cbf6396e93b Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Sat, 18 Apr 2020 06:23:05 -0400 Subject: [PATCH 05/11] Backdrop --- docs/assets/js/UI/ui.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/assets/js/UI/ui.js b/docs/assets/js/UI/ui.js index 5350b2e..60c8aa9 100644 --- a/docs/assets/js/UI/ui.js +++ b/docs/assets/js/UI/ui.js @@ -9,6 +9,8 @@ function drawLevelTransitionUI() { // UI for playing function drawPlayingUI() { + cartesianRect(0,ch/3*2, cw, ch/3, "#333333") + //Heart Rate Monitor heartBeatUI(cw/4*3-8,ch/8*7-8,cw/4,ch/8); From 3b7efa3ebcb43b3a24ce9185e4997d3ce655c3ae Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Sat, 18 Apr 2020 07:53:29 -0400 Subject: [PATCH 06/11] more betterer breathing --- docs/assets/js/UI/ui.js | 4 +- docs/assets/js/player/lifeFunctions.js | 57 +++++++++++++++++++------- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/docs/assets/js/UI/ui.js b/docs/assets/js/UI/ui.js index 5350b2e..d2bf028 100644 --- a/docs/assets/js/UI/ui.js +++ b/docs/assets/js/UI/ui.js @@ -32,8 +32,8 @@ function drawEndUI() { */ function respiratoryUI(x, y, width, height){ - cartesianRect(x,y,width,height, "black"); - cartesianRect(x,y+(height-breath/constants.lifeFuncs.breath.fullBreath*height), width, breath/constants.lifeFuncs.breath.fullBreath*height, "teal"); + cartesianRect(x,y,width,height, "rgb("+noBreathTimer/180*255+","+0+","+0+")"); + cartesianRect(x,y+(height-breath/constants.lifeFuncs.breath.fullBreath*height), width, breath/constants.lifeFuncs.breath.fullBreath*height, "rgb("+255+","+(255-fullBreathTimer/180*255)+","+(255-fullBreathTimer/180*255)+")"); } /*** diff --git a/docs/assets/js/player/lifeFunctions.js b/docs/assets/js/player/lifeFunctions.js index 0cd075e..6557e2c 100644 --- a/docs/assets/js/player/lifeFunctions.js +++ b/docs/assets/js/player/lifeFunctions.js @@ -1,37 +1,64 @@ let breath = 180; let fullBreathTimer = 0; +let noBreathTimer = 0; let heartRate = 60; let heartBeat = false; +var breathMode = { + inhale: 0, + exhale: 1 +}; + +let currentBreathMode = breathMode.exhale; + + function updateLife() { - if(keyDown[k.z]) { - breathe(); - } else { - breath--; + if(keyDown[k.UP]) { + currentBreathMode = breathMode.inhale; } + if(keyDown[k.DOWN]) { + currentBreathMode = breathMode.exhale; + } + + breathe(); + if(keyPress[k.x]) { heartbeat(); } }; function breathe() { - - breath += 5; - if(breath >= constants.lifeFuncs.breath.fullBreath) { - breath = constants.lifeFuncs.breath.fullBreath; - fullBreathTimer++; - if(fullBreathTimer >= 60) { - //cough and lose breath or something - } - } else { - fullBreathTimer = 0; + switch (currentBreathMode) { + case breathMode.inhale: + breath += 1; + if(breath >= constants.lifeFuncs.breath.fullBreath) { + breath = constants.lifeFuncs.breath.fullBreath; + fullBreathTimer++; + if(fullBreathTimer >= 180) { + //cough and lose breath or something + } + } else { + fullBreathTimer = 0; + } + break; + case breathMode.exhale: + breath -= 1; + if(breath <= 0) { + breath = 0; + noBreathTimer++; + if(noBreathTimer >= 180) { + //cough and lose breath or something + } + } else { + noBreathTimer = 0; + } + break; } - }; function heartbeat() { From 69ef6261a0e2202596f2552e46af040605821345 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Sat, 18 Apr 2020 12:18:40 -0400 Subject: [PATCH 07/11] Finish up audio playing --- docs/assets/js/crypto/core.min.js | 1 + docs/assets/js/crypto/md5.js | 268 +++++++++++++++++++++ docs/assets/js/index.js | 31 ++- docs/assets/js/sounds/permissionhandler.js | 22 ++ docs/assets/js/sounds/soundassetmap.js | 5 - docs/assets/js/sounds/soundcontext.js | 135 ++++++++++- docs/assets/js/sounds/sounds.js | 56 ++++- docs/assets/js/sounds/soundsnippet.js | 67 +++++- docs/assets/sounds/debug-ding.mp3 | Bin 0 -> 42536 bytes docs/index.html | 8 +- 10 files changed, 567 insertions(+), 26 deletions(-) create mode 100644 docs/assets/js/crypto/core.min.js create mode 100644 docs/assets/js/crypto/md5.js create mode 100644 docs/assets/js/sounds/permissionhandler.js delete mode 100644 docs/assets/js/sounds/soundassetmap.js create mode 100644 docs/assets/sounds/debug-ding.mp3 diff --git a/docs/assets/js/crypto/core.min.js b/docs/assets/js/crypto/core.min.js new file mode 100644 index 0000000..1fbb437 --- /dev/null +++ b/docs/assets/js/crypto/core.min.js @@ -0,0 +1 @@ +!function(t,n){"object"==typeof exports?module.exports=exports=n():"function"==typeof define&&define.amd?define([],n):t.CryptoJS=n()}(this,function(){var t=t||function(f){var t;if("undefined"!=typeof window&&window.crypto&&(t=window.crypto),!t&&"undefined"!=typeof window&&window.msCrypto&&(t=window.msCrypto),!t&&"undefined"!=typeof global&&global.crypto&&(t=global.crypto),!t&&"function"==typeof require)try{t=require("crypto")}catch(t){}function i(){if(t){if("function"==typeof t.getRandomValues)try{return t.getRandomValues(new Uint32Array(1))[0]}catch(t){}if("function"==typeof t.randomBytes)try{return t.randomBytes(4).readInt32LE()}catch(t){}}throw new Error("Native crypto module could not be used to get secure random number.")}var e=Object.create||function(t){var n;return r.prototype=t,n=new r,r.prototype=null,n};function r(){}var n={},o=n.lib={},s=o.Base={extend:function(t){var n=e(this);return t&&n.mixIn(t),n.hasOwnProperty("init")&&this.init!==n.init||(n.init=function(){n.$super.init.apply(this,arguments)}),(n.init.prototype=n).$super=this,n},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var n in t)t.hasOwnProperty(n)&&(this[n]=t[n]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},p=o.WordArray=s.extend({init:function(t,n){t=this.words=t||[],this.sigBytes=null!=n?n:4*t.length},toString:function(t){return(t||c).stringify(this)},concat:function(t){var n=this.words,e=t.words,i=this.sigBytes,r=t.sigBytes;if(this.clamp(),i%4)for(var o=0;o>>2]>>>24-o%4*8&255;n[i+o>>>2]|=s<<24-(i+o)%4*8}else for(o=0;o>>2]=e[o>>>2];return this.sigBytes+=r,this},clamp:function(){var t=this.words,n=this.sigBytes;t[n>>>2]&=4294967295<<32-n%4*8,t.length=f.ceil(n/4)},clone:function(){var t=s.clone.call(this);return t.words=this.words.slice(0),t},random:function(t){for(var n=[],e=0;e>>2]>>>24-r%4*8&255;i.push((o>>>4).toString(16)),i.push((15&o).toString(16))}return i.join("")},parse:function(t){for(var n=t.length,e=[],i=0;i>>3]|=parseInt(t.substr(i,2),16)<<24-i%8*4;return new p.init(e,n/2)}},u=a.Latin1={stringify:function(t){for(var n=t.words,e=t.sigBytes,i=[],r=0;r>>2]>>>24-r%4*8&255;i.push(String.fromCharCode(o))}return i.join("")},parse:function(t){for(var n=t.length,e=[],i=0;i>>2]|=(255&t.charCodeAt(i))<<24-i%4*8;return new p.init(e,n)}},d=a.Utf8={stringify:function(t){try{return decodeURIComponent(escape(u.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return u.parse(unescape(encodeURIComponent(t)))}},h=o.BufferedBlockAlgorithm=s.extend({reset:function(){this._data=new p.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=d.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(t){var n,e=this._data,i=e.words,r=e.sigBytes,o=this.blockSize,s=r/(4*o),a=(s=t?f.ceil(s):f.max((0|s)-this._minBufferSize,0))*o,c=f.min(4*a,r);if(a){for(var u=0;u>> 24)) & 0x00ff00ff) | + (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00) + ); + } + + // Shortcuts + var H = this._hash.words; + + var M_offset_0 = M[offset + 0]; + var M_offset_1 = M[offset + 1]; + var M_offset_2 = M[offset + 2]; + var M_offset_3 = M[offset + 3]; + var M_offset_4 = M[offset + 4]; + var M_offset_5 = M[offset + 5]; + var M_offset_6 = M[offset + 6]; + var M_offset_7 = M[offset + 7]; + var M_offset_8 = M[offset + 8]; + var M_offset_9 = M[offset + 9]; + var M_offset_10 = M[offset + 10]; + var M_offset_11 = M[offset + 11]; + var M_offset_12 = M[offset + 12]; + var M_offset_13 = M[offset + 13]; + var M_offset_14 = M[offset + 14]; + var M_offset_15 = M[offset + 15]; + + // Working varialbes + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + + // Computation + a = FF(a, b, c, d, M_offset_0, 7, T[0]); + d = FF(d, a, b, c, M_offset_1, 12, T[1]); + c = FF(c, d, a, b, M_offset_2, 17, T[2]); + b = FF(b, c, d, a, M_offset_3, 22, T[3]); + a = FF(a, b, c, d, M_offset_4, 7, T[4]); + d = FF(d, a, b, c, M_offset_5, 12, T[5]); + c = FF(c, d, a, b, M_offset_6, 17, T[6]); + b = FF(b, c, d, a, M_offset_7, 22, T[7]); + a = FF(a, b, c, d, M_offset_8, 7, T[8]); + d = FF(d, a, b, c, M_offset_9, 12, T[9]); + c = FF(c, d, a, b, M_offset_10, 17, T[10]); + b = FF(b, c, d, a, M_offset_11, 22, T[11]); + a = FF(a, b, c, d, M_offset_12, 7, T[12]); + d = FF(d, a, b, c, M_offset_13, 12, T[13]); + c = FF(c, d, a, b, M_offset_14, 17, T[14]); + b = FF(b, c, d, a, M_offset_15, 22, T[15]); + + a = GG(a, b, c, d, M_offset_1, 5, T[16]); + d = GG(d, a, b, c, M_offset_6, 9, T[17]); + c = GG(c, d, a, b, M_offset_11, 14, T[18]); + b = GG(b, c, d, a, M_offset_0, 20, T[19]); + a = GG(a, b, c, d, M_offset_5, 5, T[20]); + d = GG(d, a, b, c, M_offset_10, 9, T[21]); + c = GG(c, d, a, b, M_offset_15, 14, T[22]); + b = GG(b, c, d, a, M_offset_4, 20, T[23]); + a = GG(a, b, c, d, M_offset_9, 5, T[24]); + d = GG(d, a, b, c, M_offset_14, 9, T[25]); + c = GG(c, d, a, b, M_offset_3, 14, T[26]); + b = GG(b, c, d, a, M_offset_8, 20, T[27]); + a = GG(a, b, c, d, M_offset_13, 5, T[28]); + d = GG(d, a, b, c, M_offset_2, 9, T[29]); + c = GG(c, d, a, b, M_offset_7, 14, T[30]); + b = GG(b, c, d, a, M_offset_12, 20, T[31]); + + a = HH(a, b, c, d, M_offset_5, 4, T[32]); + d = HH(d, a, b, c, M_offset_8, 11, T[33]); + c = HH(c, d, a, b, M_offset_11, 16, T[34]); + b = HH(b, c, d, a, M_offset_14, 23, T[35]); + a = HH(a, b, c, d, M_offset_1, 4, T[36]); + d = HH(d, a, b, c, M_offset_4, 11, T[37]); + c = HH(c, d, a, b, M_offset_7, 16, T[38]); + b = HH(b, c, d, a, M_offset_10, 23, T[39]); + a = HH(a, b, c, d, M_offset_13, 4, T[40]); + d = HH(d, a, b, c, M_offset_0, 11, T[41]); + c = HH(c, d, a, b, M_offset_3, 16, T[42]); + b = HH(b, c, d, a, M_offset_6, 23, T[43]); + a = HH(a, b, c, d, M_offset_9, 4, T[44]); + d = HH(d, a, b, c, M_offset_12, 11, T[45]); + c = HH(c, d, a, b, M_offset_15, 16, T[46]); + b = HH(b, c, d, a, M_offset_2, 23, T[47]); + + a = II(a, b, c, d, M_offset_0, 6, T[48]); + d = II(d, a, b, c, M_offset_7, 10, T[49]); + c = II(c, d, a, b, M_offset_14, 15, T[50]); + b = II(b, c, d, a, M_offset_5, 21, T[51]); + a = II(a, b, c, d, M_offset_12, 6, T[52]); + d = II(d, a, b, c, M_offset_3, 10, T[53]); + c = II(c, d, a, b, M_offset_10, 15, T[54]); + b = II(b, c, d, a, M_offset_1, 21, T[55]); + a = II(a, b, c, d, M_offset_8, 6, T[56]); + d = II(d, a, b, c, M_offset_15, 10, T[57]); + c = II(c, d, a, b, M_offset_6, 15, T[58]); + b = II(b, c, d, a, M_offset_13, 21, T[59]); + a = II(a, b, c, d, M_offset_4, 6, T[60]); + d = II(d, a, b, c, M_offset_11, 10, T[61]); + c = II(c, d, a, b, M_offset_2, 15, T[62]); + b = II(b, c, d, a, M_offset_9, 21, T[63]); + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + + var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000); + var nBitsTotalL = nBitsTotal; + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = ( + (((nBitsTotalH << 8) | (nBitsTotalH >>> 24)) & 0x00ff00ff) | + (((nBitsTotalH << 24) | (nBitsTotalH >>> 8)) & 0xff00ff00) + ); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ( + (((nBitsTotalL << 8) | (nBitsTotalL >>> 24)) & 0x00ff00ff) | + (((nBitsTotalL << 24) | (nBitsTotalL >>> 8)) & 0xff00ff00) + ); + + data.sigBytes = (dataWords.length + 1) * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var hash = this._hash; + var H = hash.words; + + // Swap endian + for (var i = 0; i < 4; i++) { + // Shortcut + var H_i = H[i]; + + H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | + (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00); + } + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + function FF(a, b, c, d, x, s, t) { + var n = a + ((b & c) | (~b & d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function GG(a, b, c, d, x, s, t) { + var n = a + ((b & d) | (c & ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function HH(a, b, c, d, x, s, t) { + var n = a + (b ^ c ^ d) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function II(a, b, c, d, x, s, t) { + var n = a + (c ^ (b | ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.MD5('message'); + * var hash = CryptoJS.MD5(wordArray); + */ + C.MD5 = Hasher._createHelper(MD5); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacMD5(message, key); + */ + C.HmacMD5 = Hasher._createHmacHelper(MD5); + }(Math)); + + + return CryptoJS.MD5; + +})); \ No newline at end of file diff --git a/docs/assets/js/index.js b/docs/assets/js/index.js index df0eaaa..23661b1 100644 --- a/docs/assets/js/index.js +++ b/docs/assets/js/index.js @@ -50,6 +50,12 @@ function update() { } function draw() { + + // If draw is being called, the user has interacted with the page at least once. + // This signal can be used to notify the audio permission handler + unlockAudioPermission(); + + // Handle game state switch (globalState) { // title screen case globalStates.titleScreen: @@ -110,6 +116,25 @@ function onAssetsLoaded() { setup(60); -// Hide the preloader -// This should actually run after all assets have been downloaded -page_preloader.hide(false); \ No newline at end of file +/* Preload actions */ + +// To make something happen before the preloader is hidden, add it to this list +// The function must take a callback that will be run when the function finishes +let actions = [preCacheSounds]; +let actionsCompleted = 0; + +// Loop through every action, and load it +actions.forEach((action) => { + + // Run the action & handle loading + action(() => { + + // Incr the number of successfully loaded actions + actionsCompleted += 1; + + // If this is the last aciton, hide the loader + if (actionsCompleted == actions.length) { + page_preloader.hide(false); + } + }) +}); diff --git a/docs/assets/js/sounds/permissionhandler.js b/docs/assets/js/sounds/permissionhandler.js new file mode 100644 index 0000000..1a5f2fb --- /dev/null +++ b/docs/assets/js/sounds/permissionhandler.js @@ -0,0 +1,22 @@ +/** + * This file just exists to keep track of weather the user has interacted with the + * webpage. Most browsers will block autoplay if no interaction has been made. + */ + + +// Tracker for permission unlock +let _audioPermUnlock = false; + +/** + * Call this when the user interacts with the page + */ +function unlockAudioPermission() { + _audioPermUnlock = true; +} + +/** + * Check if autoplay is enabled + */ +function canPlayAudio() { + return _audioPermUnlock; +} \ No newline at end of file diff --git a/docs/assets/js/sounds/soundassetmap.js b/docs/assets/js/sounds/soundassetmap.js deleted file mode 100644 index faa51cb..0000000 --- a/docs/assets/js/sounds/soundassetmap.js +++ /dev/null @@ -1,5 +0,0 @@ - -// A mapping of asset names to their files -let soundAssets = { - -} \ No newline at end of file diff --git a/docs/assets/js/sounds/soundcontext.js b/docs/assets/js/sounds/soundcontext.js index 555b202..9804973 100644 --- a/docs/assets/js/sounds/soundcontext.js +++ b/docs/assets/js/sounds/soundcontext.js @@ -1,5 +1,28 @@ +/** + * This file contains classes for playing sounds. + * The SoundContext works by providing multiple audio channels, + * just like an old ATARI, or even GameBoy sound system. Each + * channel can be controlled individually. + * + * ---- Usage ---- + * + * // Load all sounds + * preCacheSounds(()=>{ + * // Code can be run here as soon as all sounds are loaded + * // ... + * }); + * + * // Play a sound + * globalSoundContext.playSound(globalSoundContext.channels., sounds.); + * + * // Stop a channel + * globalSoundContext.mute(globalSoundContext.channels.); + */ -class SoundChannel{ +/** + * A sound channel can play 1 sound at a time, and supports sound queueing + */ +class _SoundChannel { /** * Create a sound channel @@ -7,21 +30,123 @@ class SoundChannel{ */ constructor(max_queue_size) { this.max_size = max_queue_size + this.sound_queue = [] + } + + /** + * Add a snippet to the queue + * @param {SoundSnippet} snippet + */ + enqueue(snippet) { + + // If the queue is full, cut out the next sound in the queue to make room + if (this.sound_queue.length >= this.max_size) { + this.sound_queue.splice(1, 1); + } + + // Append the sound to the queue + this.sound_queue.push(snippet); + + // If this is the first sound in the queue, spawn a watcher job, and play it + if (this.sound_queue.length == 1) { + this.sound_queue[0].play(); + this._spawnWatcher(this.sound_queue[0]); + } + } + + /** + * Start a job to run when the sound finishes to remove the sound from the queue + * @param {SoundSnippet} snippet + */ + _spawnWatcher(snippet) { + + // Read the snippet length + let length = snippet.getLengthSeconds(); + + // Spawn a clean action for that time in the future + setTimeout(() => { + this._cleanQueue(snippet); + }, length); + } + + /** + * This should be run when every sound finishes. This will remove that + * sound from from the queue, start the next sound, and spawn a + * new watcher for that sound. + * @param {SoundSnippet} snippet + */ + _cleanQueue(snippet) { + + // Get the snippet hash + let hash = snippet.getHash(); + + // Make sure there are actually sounds playing + if (this.sound_queue.length > 0) { + + // If the first snippet in the queue matches this hash, remove it. + // If not, something must have happened. Just ignore it, and move on + if (this.sound_queue[0].getHash() == hash) { + + // Popoff the snippet + this.sound_queue.shift(); + + } + + + // Spawn a watcher for the next sound & play it + if (this.sound_queue.length > 0) { + this.sound_queue[0].play(); + this._spawnWatcher(this.sound_queue[0]); + } + } + } + + /** + * Clear the entire queue, and stop all sounds + */ + clear() { + + // Stop every sound + this.sound_queue.forEach((sound) => { + sound.stop(); + }) + + // Clear the queue + this.sound_queue = []; + } } -class SoundContext{ +class _SoundContext { constructor() { - + // Define all sound channels this.channels = { - bgm: new SoundChannel(2) + bgm: new _SoundChannel(2) } } + /** + * Play a sound in a channel + * @param {*} channel + * @param {SoundSnippet} snippet + */ + playSound(channel, snippet) { + console.log(`[SoundContext] Playing snippet: ${snippet.getName()}`); + channel.enqueue(snippet); + } + + /** + * Stop all sounds in a channel + * @param {*} channel + */ + mute(channel) { + channel.clear(); + } + } // The global context for sounds -let globalSoundContext = new SoundContext(); \ No newline at end of file +let globalSoundContext = new _SoundContext(); \ No newline at end of file diff --git a/docs/assets/js/sounds/sounds.js b/docs/assets/js/sounds/sounds.js index 547b678..935dc6a 100644 --- a/docs/assets/js/sounds/sounds.js +++ b/docs/assets/js/sounds/sounds.js @@ -1,7 +1,23 @@ +/** + * This file handles all sound assets, and loading them + * To add a new sound asset: + * 1) Make a mapping from the asset name to it's filename in soundAssetMap + * 2) Define the SoundSnippet in soundAssets + * + * The preloader will handle asset loading for you. + * Make sure to check the console for any errors with loading your file + */ + + +// A mapping of asset names to their files +// This exists to give nicer names to files +let soundAssetMap = { + "debug-ding":"/assets/sounds/debug-ding.mp3" +} // All available sounds -let sounds = { - +let soundAssets = { + debug_ding: new SoundSnippet("debug-ding") } /** @@ -9,5 +25,39 @@ let sounds = { * @param {function} callback Callback for completion */ function preCacheSounds(callback) { - + + // Counter for number of sounds cached + let cachedCount = 0; + + Object.keys(soundAssets).forEach((key) => { + + // Get the SoundSnippet + let sound = soundAssets[key]; + + // Cache the sound + sound.cache(() => { + + // Incr the cache count + cachedCount += 1; + + // If this is the last sound, fire off the callback + if (cachedCount == Object.keys(soundAssets).length) { + callback(); + } + + }); + + }); + + + // Spawn a notifier for loading issues + setTimeout(() => { + + // If not all sounds have been cached by the time this is called, send a warning + if (cachedCount < Object.keys(soundAssets).length) { + console.warn(`[preCacheSounds] Only ${cachedCount} of ${Object.keys(soundAssets).length} sounds have been cached after 2 seconds. Is there a missing asset? or is the user on a slow connection?`); + } + + }, 2000); + } \ No newline at end of file diff --git a/docs/assets/js/sounds/soundsnippet.js b/docs/assets/js/sounds/soundsnippet.js index 6ee8178..af03267 100644 --- a/docs/assets/js/sounds/soundsnippet.js +++ b/docs/assets/js/sounds/soundsnippet.js @@ -1,12 +1,28 @@ -class SoundSnippet{ - +// Counter for hash generation +let _hashCounter = 0; + +class SoundSnippet { + /** * Load a sound asset to a snipper * @param {string} asset_name Asset name as defined in soundassetmap.js */ constructor(asset_name) { - + + // Store asset name + this.asset_name = asset_name; + + // Compute an asset hash + this.asset_hash = CryptoJS.MD5(`ASSET:${asset_name}::${_hashCounter}`); + _hashCounter += 1; + + // Read actual asset path + this.assetPath = soundAssetMap[asset_name]; + + // Set up the audio object + this.audio = new Audio(); + } /** @@ -14,15 +30,52 @@ class SoundSnippet{ * @param {function} callback callback to notify */ cache(callback) { - + + // Set the audio SRC + this.audio.src = this.assetPath; + + // Create a callback for loading finished + this.audio.addEventListener("loadeddata", callback, true); } - + play() { - + + // If autoplay is disabled, we notify the console + if (canPlayAudio()) { + // Play the snippet + this.audio.play(); + } else { + console.warn("[SoundSnippet] Tried to play audio with autoplay disabled. The user must press the play button before you can play audio"); + } + } stop() { - + + // Stop the snippet + this.audio.stop(); + + } + + /** + * Get the sound length in seconds + */ + getLengthSeconds() { + return 0; + } + + /** + * Get the asset name + */ + getName() { + return this.asset_name; + } + + /** + * Get this asset's hash. This can be used for comparing objects efficiently. + */ + getHash() { + return this.asset_hash; } } \ No newline at end of file diff --git a/docs/assets/sounds/debug-ding.mp3 b/docs/assets/sounds/debug-ding.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ff9ebbd10a1f02c011ed2fb2fa6f4ed196957aa7 GIT binary patch literal 42536 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 %B%a+jC1IaQ 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 + + + + @@ -47,11 +51,9 @@ - - - + From efad6d67c67a90e0a2b887a6b9183bbd6fed531d Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Sat, 18 Apr 2020 13:32:20 -0400 Subject: [PATCH 08/11] Make some soundplay tweaks --- docs/assets/js/sounds/soundcontext.js | 26 +++++++++++++++++++------- docs/assets/js/sounds/soundsnippet.js | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/assets/js/sounds/soundcontext.js b/docs/assets/js/sounds/soundcontext.js index 9804973..f9dd3da 100644 --- a/docs/assets/js/sounds/soundcontext.js +++ b/docs/assets/js/sounds/soundcontext.js @@ -12,9 +12,12 @@ * // ... * }); * - * // Play a sound + * // Play a sound using channels * globalSoundContext.playSound(globalSoundContext.channels., sounds.); * + * // Just play a sound now + * globalSoundContext.playSoundNow(sounds.); + * * // Stop a channel * globalSoundContext.mute(globalSoundContext.channels.); */ @@ -39,10 +42,12 @@ class _SoundChannel { */ enqueue(snippet) { + console.log(this.sound_queue) + // If the queue is full, cut out the next sound in the queue to make room - if (this.sound_queue.length >= this.max_size) { - this.sound_queue.splice(1, 1); - } + // if (this.sound_queue.length > this.max_size) { + // this.sound_queue.splice(1, 1); + // } // Append the sound to the queue this.sound_queue.push(snippet); @@ -66,7 +71,7 @@ class _SoundChannel { // Spawn a clean action for that time in the future setTimeout(() => { this._cleanQueue(snippet); - }, length); + }, length * 1000); } /** @@ -92,7 +97,6 @@ class _SoundChannel { } - // Spawn a watcher for the next sound & play it if (this.sound_queue.length > 0) { this.sound_queue[0].play(); @@ -124,7 +128,7 @@ class _SoundContext { // Define all sound channels this.channels = { - bgm: new _SoundChannel(2) + bgm: new _SoundChannel(3) } } @@ -138,6 +142,14 @@ class _SoundContext { channel.enqueue(snippet); } + /** + * Play a sound right now + * @param {SoundSnippet} snippet + */ + playSoundNow(snippet) { + snippet.play(); + } + /** * Stop all sounds in a channel * @param {*} channel diff --git a/docs/assets/js/sounds/soundsnippet.js b/docs/assets/js/sounds/soundsnippet.js index af03267..3aeb74e 100644 --- a/docs/assets/js/sounds/soundsnippet.js +++ b/docs/assets/js/sounds/soundsnippet.js @@ -62,7 +62,7 @@ class SoundSnippet { * Get the sound length in seconds */ getLengthSeconds() { - return 0; + return this.audio.duration; } /** From ece0d97199b93052ffd1b5e72b38812a4791bab0 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Sat, 18 Apr 2020 13:50:38 -0400 Subject: [PATCH 09/11] Breath interval limit --- docs/assets/js/player/lifeFunctions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assets/js/player/lifeFunctions.js b/docs/assets/js/player/lifeFunctions.js index 6557e2c..1df2f12 100644 --- a/docs/assets/js/player/lifeFunctions.js +++ b/docs/assets/js/player/lifeFunctions.js @@ -18,11 +18,11 @@ let currentBreathMode = breathMode.exhale; function updateLife() { if(keyDown[k.UP]) { - currentBreathMode = breathMode.inhale; + if(breath === 0) currentBreathMode = breathMode.inhale; } if(keyDown[k.DOWN]) { - currentBreathMode = breathMode.exhale; + if(breath === constants.lifeFuncs.breath.fullBreath) currentBreathMode = breathMode.exhale; } breathe(); From 18a03a5aff79805fddb293520d073223ed3ddaa8 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Sat, 18 Apr 2020 15:38:50 -0400 Subject: [PATCH 10/11] Added blood pressure system -bloood --- docs/assets/js/UI/ui.js | 9 ++++++++- docs/assets/js/constants.js | 9 ++++++--- docs/assets/js/player/lifeFunctions.js | 21 +++++++++++++++------ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/docs/assets/js/UI/ui.js b/docs/assets/js/UI/ui.js index 5e51149..f084c82 100644 --- a/docs/assets/js/UI/ui.js +++ b/docs/assets/js/UI/ui.js @@ -80,11 +80,18 @@ function heartBeatUI(x, y, width, height){ //Backdrop rect(x+width/2,y+height/2,width,height,"black"); + //Pressure Meter + rect(x+width-8,y+height/2,16,height,"red"); + rect(x+width-8,y+height/2,16,height/2,"yellow"); + rect(x+width-8,y+height/2,16,height/6,"green"); + let pressureHeight = Math.max(Math.min(y+height-(pressure/constants.lifeFuncs.cardio.optimalPressure*height/2),y+height),y); + line(x+width-16, pressureHeight,x+width,pressureHeight, 2,"black") + //Graph for (let index = 0; index < heartBeatHistory.length; index++) { const qrsValueAtPosition = heartBeatHistory[index]; const qrsValueAtNextPosition = heartBeatHistory[index+1]; - line(x+(index*width/heartBeatHistory.length), y+(2*height/3)+(qrsValueAtPosition*width/heartBeatHistory.length), x+((index+1)*width/heartBeatHistory.length), y+(2*height/3)+(qrsValueAtNextPosition*width/heartBeatHistory.length),Math.min(3,Math.max(3/beatTimeElapsed,1)), "red"); + line(x+(index*(width-16)/heartBeatHistory.length), y+(2*height/3)+(qrsValueAtPosition*(width-16)/heartBeatHistory.length), x+((index+1)*(width-16)/heartBeatHistory.length), y+(2*height/3)+(qrsValueAtNextPosition*(width-16)/heartBeatHistory.length),Math.min(3,Math.max(3/beatTimeElapsed,1)), "red"); } } diff --git a/docs/assets/js/constants.js b/docs/assets/js/constants.js index c4ef125..0adeac7 100644 --- a/docs/assets/js/constants.js +++ b/docs/assets/js/constants.js @@ -35,9 +35,12 @@ var constants = { } }, lifeFuncs:{ - breath:{ - fullBreath: 200 - } + breath:{ + fullBreath: 200 + }, + cardio:{ + optimalPressure: 50 + } }, legs:{ size:{ diff --git a/docs/assets/js/player/lifeFunctions.js b/docs/assets/js/player/lifeFunctions.js index 1df2f12..105ab0e 100644 --- a/docs/assets/js/player/lifeFunctions.js +++ b/docs/assets/js/player/lifeFunctions.js @@ -2,7 +2,7 @@ let breath = 180; let fullBreathTimer = 0; let noBreathTimer = 0; -let heartRate = 60; +let pressure = 50; let heartBeat = false; @@ -17,11 +17,11 @@ let currentBreathMode = breathMode.exhale; function updateLife() { - if(keyDown[k.UP]) { + if(keyDown[k.w]) { if(breath === 0) currentBreathMode = breathMode.inhale; } - if(keyDown[k.DOWN]) { + if(keyDown[k.s]) { if(breath === constants.lifeFuncs.breath.fullBreath) currentBreathMode = breathMode.exhale; } @@ -30,6 +30,11 @@ function updateLife() { if(keyPress[k.x]) { heartbeat(); } + + pressure-=0.25; + if(pressure<=0){ + pressure = 0; + } }; function breathe() { @@ -39,7 +44,7 @@ function breathe() { if(breath >= constants.lifeFuncs.breath.fullBreath) { breath = constants.lifeFuncs.breath.fullBreath; fullBreathTimer++; - if(fullBreathTimer >= 180) { + if(fullBreathTimer >= 600) { //cough and lose breath or something } } else { @@ -47,11 +52,11 @@ function breathe() { } break; case breathMode.exhale: - breath -= 1; + breath -= 2; if(breath <= 0) { breath = 0; noBreathTimer++; - if(noBreathTimer >= 180) { + if(noBreathTimer >= 300) { //cough and lose breath or something } } else { @@ -62,5 +67,9 @@ function breathe() { }; function heartbeat() { + pressure+=10; + if(pressure>=100){ + pressure = 100; + } heartBeat = true; }; \ No newline at end of file From 70de5fca979a26168402c37c12200dc4477bff6a Mon Sep 17 00:00:00 2001 From: rsninja722 Date: Sat, 18 Apr 2020 15:56:53 -0400 Subject: [PATCH 11/11] stuff --- docs/assets/js/constants.js | 13 ++- docs/assets/js/index.js | 3 +- docs/assets/js/player/player.js | 176 +++++++++++++++++++++++--------- 3 files changed, 137 insertions(+), 55 deletions(-) diff --git a/docs/assets/js/constants.js b/docs/assets/js/constants.js index 4570ac8..ab3930d 100644 --- a/docs/assets/js/constants.js +++ b/docs/assets/js/constants.js @@ -33,9 +33,16 @@ var constants = { complex_width: 0.65 } }, - legs:{ - size:{ - maximumMovement: 30 + player:{ + leg_speed: 0.1, + movement_divider: 50, + max_movement_speed: 3, + width: 30, + height: 50, + select_range: 10, + hip: { + offset_x: 15, + offset_y: 25 } } diff --git a/docs/assets/js/index.js b/docs/assets/js/index.js index 51d3657..ae09bbc 100644 --- a/docs/assets/js/index.js +++ b/docs/assets/js/index.js @@ -62,7 +62,9 @@ function draw() { break; // playing case globalStates.playing: + camera.zoom = 2; drawWorldBlocks(); + player.draw(); break; // paused case globalStates.paused: @@ -92,7 +94,6 @@ function absoluteDraw() { // playing case globalStates.playing: drawPlayingUI(); - player.draw(); break; // paused case globalStates.paused: diff --git a/docs/assets/js/player/player.js b/docs/assets/js/player/player.js index f953331..9566cb3 100644 --- a/docs/assets/js/player/player.js +++ b/docs/assets/js/player/player.js @@ -1,31 +1,28 @@ class Player { - - constructor(x, y){ + constructor(x, y) { this.x = x; this.y = y; - this.w = 10; - this.h = 20; - this.hipLeft = {x:this.x-5,y:this.y+10}; - this.hipRight = {x:this.x+5,y:this.y+10}; - this.leftLeg = new Leg(this.hipLeft.x,this.hipLeft.y,50,-Math.PI/4); - this.rightLeg = new Leg(this.hipRight.x, this.hipRight.y, 50, Math.PI/2); + this.w = constants.player.width; + this.h = constants.player.height; + this.hipLeft = { x: this.x - constants.player.hip.offset_x, y: this.y + constants.player.hip.offset_y }; + this.hipRight = { x: this.x + constants.player.hip.offset_x, y: this.y + constants.player.hip.offset_y }; + this.leftLeg = new Leg(this.hipLeft.x, this.hipLeft.y, 50, -Math.PI / 4); + this.rightLeg = new Leg(this.hipRight.x, this.hipRight.y, 50, Math.PI / 2); this.legSelected = "R"; - this.shouldMoveLeg = true; + this.hover = { active: false, leg: "R" }; + this.shouldMoveLeg = false; } - - - } Player.prototype.getActiveLeg = function(){ - if(this.legSelected === "L"){ + if (this.legSelected === "L") { return this.leftLeg; } return this.rightLeg; } Player.prototype.getLockedLeg = function(){ - if(this.legSelected === "R"){ + if (this.legSelected === "R") { return this.leftLeg; } return this.rightLeg; @@ -33,62 +30,80 @@ Player.prototype.getLockedLeg = function(){ // leg has been selected, move leg towards mouse Player.prototype.moveLeg = function(){ + var targetPos = mousePosition(); // Stops if we shouldn't move leg - if(!this.shouldMoveLeg){ + if (!this.shouldMoveLeg) { + var curLeg = this.getActiveLeg(); + this.hover.active = false; + if (distanceToLineSegment(this.leftLeg.x, this.leftLeg.y, this.leftLeg.x2, this.leftLeg.y2, targetPos.x, targetPos.y) < constants.player.select_range) { + this.hover.active = true; + this.hover.leg = "L"; + if(mousePress[0]) { + this.shouldMoveLeg = true; + this.legSelected = "L"; + this.hover.active = false; + } + } else if (distanceToLineSegment(this.rightLeg.x, this.rightLeg.y, this.rightLeg.x2, this.rightLeg.y2, targetPos.x, targetPos.y) < constants.player.select_range) { + this.hover.active = true; + this.hover.leg = "R"; + if(mousePress[0]) { + this.shouldMoveLeg = true; + this.legSelected = "R"; + this.hover.active = false; + } + } return 0; } - + // gets active leg & target var curLeg = this.getActiveLeg(); - var target = mousePos; + var target = targetPos; // move selected leg towards mouse - + // console.log(curLeg.angle.toPrecision(5),pointTo(curLeg,target).toPrecision(5)); - curLeg.angle = turn( curLeg.angle,pointTo(curLeg,target),0.1); + curLeg.angle = turn(curLeg.angle, pointTo(curLeg, target), constants.player.leg_speed); // var angle = pointTo(curLeg,target); curLeg.x2 = curLeg.x + curLeg.len * Math.cos(curLeg.angle); curLeg.y2 = curLeg.y + curLeg.len * Math.sin(curLeg.angle); // curLeg.angle = pointTo(curLeg,{x:curLeg.x2,y:curLeg.y2}); - if(dist(curLeg,target) > curLeg.len) { + if (dist(curLeg, target) > curLeg.len) { // move towards mouse - this.x += Math.cos(pointTo(curLeg,target)) * clamp(dist(curLeg,target)/50,1,3); + this.x += Math.cos(pointTo(curLeg, target)) * clamp(dist(curLeg, target) / constants.player.movement_divider, 1, constants.player.max_movement_speed); - this.y += Math.sin(pointTo(curLeg,target)) * clamp(dist(curLeg,target)/50,1,3); + this.y += Math.sin(pointTo(curLeg, target)) * clamp(dist(curLeg, target) / constants.player.movement_divider, 1, constants.player.max_movement_speed); this.updateHips(); } // if leg is right update it accordingly - if(this.legSelected === "R") { + if (this.legSelected === "R") { // set angle to the locked foot to the locked hip oppLeg = this.getLockedLeg(); - oppLeg.angle = pointTo({x:oppLeg.x2,y:oppLeg.y2},this.hipRight); + oppLeg.angle = pointTo({ x: oppLeg.x2, y: oppLeg.y2 }, this.hipRight); // snap body to a position where the hip is attached to the leg - this.x = (oppLeg.x2 + oppLeg.len * Math.cos(oppLeg.angle)) - 5; - this.y = (oppLeg.y2 + oppLeg.len * Math.sin(oppLeg.angle)) - 10; + this.x = (oppLeg.x2 + oppLeg.len * Math.cos(oppLeg.angle)) - constants.player.hip.offset_x; + this.y = (oppLeg.y2 + oppLeg.len * Math.sin(oppLeg.angle)) - constants.player.hip.offset_y; this.updateHips(); - + // make sure each leg ends at the hips oppLeg.x = this.hipRight.x; oppLeg.y = this.hipRight.y; curLeg.x = this.hipLeft.x; curLeg.y = this.hipLeft.y; - console.log(oppLeg.angle) - // if leg is left update it accordingly + // if leg is left update it accordingly } else { - console.log(curLeg.angle) - // set angle to the locked foot to the locked hip + // set angle to the locked foot to the locked hip oppLeg = this.getLockedLeg(); - oppLeg.angle = pointTo({x:oppLeg.x2,y:oppLeg.y2},this.hipLeft); + oppLeg.angle = pointTo({ x: oppLeg.x2, y: oppLeg.y2 }, this.hipLeft); // snap body to a position where the hip is attached to the leg - this.x = (oppLeg.x2 + oppLeg.len * Math.cos(oppLeg.angle)) + 5; - this.y = (oppLeg.y2 + oppLeg.len * Math.sin(oppLeg.angle)) - 10; + this.x = (oppLeg.x2 + oppLeg.len * Math.cos(oppLeg.angle)) + constants.player.hip.offset_x; + this.y = (oppLeg.y2 + oppLeg.len * Math.sin(oppLeg.angle)) - constants.player.hip.offset_y; this.updateHips(); @@ -99,34 +114,93 @@ Player.prototype.moveLeg = function(){ curLeg.x = this.hipRight.x; curLeg.y = this.hipRight.y; } + + } Player.prototype.updateHips = function() { - this.hipLeft = {x:this.x-5,y:this.y+10}; - this.hipRight = {x:this.x+5,y:this.y+10}; + this.hipLeft = { x: this.x - constants.player.hip.offset_x, y: this.y + constants.player.hip.offset_y }; + this.hipRight = { x: this.x + constants.player.hip.offset_x, y: this.y + constants.player.hip.offset_y }; } Player.prototype.draw = function() { - rect(this.x, this.y, this.w, this.h,"green"); - this.leftLeg.draw(); - this.rightLeg.draw(); -} - -Player.prototype.update = function() { - this.moveLeg(); - var curLeg = this.getActiveLeg(); - if(collidingWithWorld({x:curLeg.x2,y:curLeg.y2,w:4,h:4})||mousePress[0]){ - if(this.legSelected === "R"){ - this.legSelected = "L"; - this.leftLeg.angle += pi; + rect(this.x, this.y, this.w, this.h, "green"); + if(this.hover.active) { + if(this.hover.leg === "R") { + curCtx.shadowBlur = 10; + curCtx.shadowColor = "yellow"; + curCtx.lineWidth = 3; + this.rightLeg.draw(); + curCtx.lineWidth = 1; + curCtx.shadowBlur = 0; + curCtx.shadowColor = "black"; + this.leftLeg.draw(); } else { - this.legSelected = "R"; - this.rightLeg.angle += pi; + curCtx.shadowBlur = 10; + curCtx.shadowColor = "yellow"; + curCtx.lineWidth = 3; + this.leftLeg.draw(); + curCtx.lineWidth = 1; + curCtx.shadowBlur = 0; + curCtx.shadowColor = "black"; + this.rightLeg.draw(); } + } else { + this.leftLeg.draw(); + this.rightLeg.draw(); } } +Player.prototype.update = function() { + var curLeg = this.getActiveLeg(); + if(mousePress[0]) {// if (collidingWithWorld({ x: curLeg.x2, y: curLeg.y2, w: 4, h: 4 })) { + // if (this.legSelected === "R") { + // this.legSelected = "L"; + // this.leftLeg.angle += pi; + // } else { + // this.legSelected = "R"; + // this.rightLeg.angle += pi; + // } + this.shouldMoveLeg = false; + } + this.moveLeg(); + centerCameraOn(this.x,this.y); +} + +// https://github.com/scottglz/distance-to-line-segment/blob/master/index.js +function distanceSquaredToLineSegment2(lx1, ly1, ldx, ldy, lineLengthSquared, px, py) { + var t; // t===0 at line pt 1 and t ===1 at line pt 2 + if (!lineLengthSquared) { + // 0-length line segment. Any t will return same result + t = 0; + } + else { + t = ((px - lx1) * ldx + (py - ly1) * ldy) / lineLengthSquared; + + if (t < 0) + t = 0; + else if (t > 1) + t = 1; + } + + var lx = lx1 + t * ldx, + ly = ly1 + t * ldy, + dx = px - lx, + dy = py - ly; + return dx * dx + dy * dy; +} +function distanceSquaredToLineSegment(lx1, ly1, lx2, ly2, px, py) { + var ldx = lx2 - lx1, + ldy = ly2 - ly1, + lineLengthSquared = ldx * ldx + ldy * ldy; + return distanceSquaredToLineSegment2(lx1, ly1, ldx, ldy, lineLengthSquared, px, py); +} + +function distanceToLineSegment(lx1, ly1, lx2, ly2, px, py) { + return Math.sqrt(distanceSquaredToLineSegment(lx1, ly1, lx2, ly2, px, py)); +} -var player = new Player(250,200); \ No newline at end of file + +var player = new Player(250, 150); \ No newline at end of file