xxxxxxxxxx
480
/*
More info at:
https://doi.org/10.6084/m9.figshare.12847892.v5
A simple phone/tablet online javascript game produced as an educational resource for 2020 online science fair (Fête de la Science) written in p5.js.
Scenario:
Player is in control of a device with which he has to get rid of virus particles loaded with different dyes. Controls include choosing laser wavelength, adjusting mirrors and objective in a breadboard-style microscopy setup. Laser can be used to probe particles peak absorbance wavelength, or to destroy particles if focused using the objective.
Particles are destroyed if used wavelength is +/- 50nm.
Players will learn about relation between color and wavelength. A lambda-meter mode allows to just measure the current laser wavelength.
Estimated play time: 5 minutes.
*/
let scientist;
let bugs = [];
let scoreBubbles = [];
let overallSpeed;
let score, level;
let button; // only one button for now
let lambda, lambdaColor; // selected wavlength and matching color
let sx, sy; // two size units along x and y axis
let gamestarted, gameInitialized, gameover; // game states
let mode, lambdaMeter, displayCredits; // special modes
let startTime; //
let creditsText, help;
// called before setup
function preload() {
spectrum = loadImage("images/spectrum.png");
mirror = loadImage("images/mirror.png");
laser = loadImage("images/laser.png");
glovicule = loadImage("images/glovicule.png");
objective = loadImage("images/objective.png");
microscope = loadImage("images/microscope.png");
scientist = loadImage("images/mads100tist.png");
// halloween
// virus = loadImage("images/pumpkin.png");
virus = loadImage("images/virus.png");
qrcode = loadImage("images/qrcode.png");
creditsText =
"* Written in p5*js\n" +
"* Icons from emojipedia.org\n" +
"* Scientist by @MadS100tist\n" +
"* Glove by Mathieu Erhardt\n" +
"* Game by @jmutterer";
}
// called once at startup
function setup() {
createCanvas(windowWidth, windowHeight);
gameover = false;
gamestarted = false;
gameInitialized = false;
lambdaMeter = false;
score = 0;
overallSpeed = 0;
lambda = 561;
bugs = [];
scoreBubbles = [];
m1 = new Mirror();
m2 = new Mirror();
m3 = new Mirror();
m4 = new Mirror();
m5 = new Mirror();
obj = new Obj();
sx = windowWidth / 10;
sy = windowHeight / 20;
m2.y = 17 * sy;
m4.y = 8 * sy;
level = 0;
button = "";
mode = "";
help = 255;
frameRate(30);
}
// this is called when laser is shined on the 'start' command
function initializeGame() {
for (let i = 0; i < 10; i++) {
bugs[i] = new Bug(i, level);
}
gameInitialized = true;
help = 0; //hide all help items
}
// called for each frame
function draw() {
// check that the game is run in portrait mode
if (windowWidth > windowHeight) {
background(0);
imageMode(CENTER);
image(qrcode, windowWidth / 2, windowHeight / 2);
fill(255);
stroke(0);
strokeWeight(3);
textSize(30);
textAlign(CENTER, TOP);
text("Use a phone/tablet\ntouchscreen device\nand hold device vertically", windowWidth / 2, 100);
return;
}
lambdaMeter = false;
displayCredits = false;
sx = windowWidth / 10;
sy = windowHeight / 20;
background(128);
// dashed lines visualising laser and mirrors axis
drawingContext.save();
drawingContext.setLineDash([16, 10]);
strokeWeight(0.2);
stroke(255);
line(sx * 1.5, 0, sx * 1.5, windowHeight);
line(sx * 8.5, 0, sx * 8.5, windowHeight);
line(0, sy * 19, windowWidth, sy * 19);
drawingContext.restore();
// leftside top command bar and spectrum bar and tick mark
rectMode(CORNER);
fill(64);
noStroke();
rect(0, 0, sx / 2, 10 * sy);
imageMode(CORNERS);
image(spectrum, 0, windowHeight / 2, sx * 0.5, windowHeight - sy);
h = windowHeight - ((lambda - 380) / 400) * (windowHeight / 2 - sy) - sy;
push();
translate(sx * 0.5, h);
strokeWeight(1);
stroke(0);
lambdaColor = get(1, h - 1);
fill(lambdaColor);
triangle(0, 0, 10, -10, 10, 10);
line(0, 0, -sx / 2, 0);
pop();
// place mirrors and objective
m2.x = 8.5 * sx;
// default m2 and m4 placement (lambda-meter)
if (frameCount == 1) {
m2.y = 17 * sy;
m4.y = 8 * sy;
}
m2.y = constrain(m2.y, 3 * sy, 17 * sy);
m2.display(0);
// draw a small blocking line facing m2 to avoid adding confusion
stroke(0);
strokeWeight(4);
line(sx / 2 + 4, min(m2.y - sy / 2, 10 * sy), sx / 2 + 4, min(m2.y + sy / 2, 10 * sy));
m4.x = 1.5 * sx;
m4.y = constrain(m4.y, 0, 15 * sy);
m4.display(0);
m1.displayAt(8.5 * sx, 19 * sy, PI / 2);
m3.displayAt(1.5 * sx, 17 * sy, -PI);
obj.display(true);
// handle touch events for spectrum, laser, m2 and m4 mirrors and objective.
button = "";
if (touches.length) {
if ((touches[0].x < sx) && (touches[0].y > windowHeight / 2)) {
lambda = map(touches[0].y, windowHeight - sy, windowHeight / 2, 380, 780);
lambda = constrain(lambda, 380, 780);
help = help & ~8;
} else if ((touches[0].x > 8 * sx) && (touches[0].y < 17 * sy)) {
m2.y = touches[0].y;
m2.y = constrain(m2.y, 3 * sy, 17 * sy);
help = help & ~1;
} else if ((touches[0].x > 1 * sx) && (touches[0].x < 2 * sx) && (touches[0].y < 17 * sy)) {
m4.y = touches[0].y;
m4.y = constrain(m4.y, 4 * sy, 17 * sy);
help = help & ~2;
} else if ((abs(5 * sx - touches[0].x) < laser.width * sy / laser.height) && (touches[0].y > sy * 18)) {
button = "laser"
} else if ((touches[0].x > 2 * sx) && (touches[0].x < 8 * sx) && (touches[0].y > 4 * sy) && (touches[0].y < 16 * sy)) {
obj.x = touches[0].x;
obj.y = touches[0].y;
help = help & ~4;
}
} else {
mode = "";
}
// add viruses if 'start' command selected
if (gamestarted && !gameInitialized) initializeGame();
// draw laser path according to m2 position, and select mode if applicable
if (button === "laser") {
help = help & ~32;
lambdaMeter = false;
displayCredits = false;
strokeWeight(5);
stroke(lambdaColor);
line(sx * 5, 19 * sy, m1.x, m1.y);
line(m1.x, m1.y, m2.x, m2.y);
// m2 is facing m3
if (dist(0, m2.y, 0, m3.y) < 10) {
m2.y = m3.y; // snap to m3
line(m3.x, m3.y, m2.x, m2.y);
line(m3.x, m3.y, m4.x, m4.y);
line(sx / 2, m4.y, m4.x, m4.y);
if (dist(0, m4.y, 0, 8 * sy) < 10) lambdaMeter = true;
else if (dist(0, m4.y, 0, 4 * sy) < 10) displayCredits = true;
else if (dist(0, m4.y, 0, 6 * sy) < 10) {
if (gamestarted && ((millis() - startTime) > 500)) {
setup();
} else {
gamestarted = true;
startTime = millis();
// reset m4 to lambda meter to avoid looping restart
m4.y = 8 * sy;
}
}
mode = "command";
}
// m2 is facing the objective mirror m5
else if (dist(0, m2.y, 0, m5.y) < 10) {
m2.y = m5.y; // snap to m5
line(m5.x, m5.y, m2.x, m2.y);
line(m5.x, m5.y, obj.x, obj.y);
strokeWeight(5);
fill(lambdaColor);
noStroke();
triangle(obj.x - sx / 3, obj.y, obj.x + sx / 3, obj.y, obj.x, obj.y + 2 * sy);
obj.display(false);
if (!gameover) mode = "sanitise";
}
// m2 if facing nowhere special, use laser as a probe
else {
line(m2.x, m2.y, sx / 2 + 5, m2.y);
mode = "probe";
}
}
// draw other GUI items
imageMode(CENTER);
image(scientist, 9 * sx, sy, 2 * sy, 2 * sy);
image(microscope, sx, sy, 2 * sy, 2 * sy);
push();
imageMode(CENTER);
translate(sx * 5, 19 * sy);
image(laser, 0, 0, laser.width * sy * 2 / laser.height, sy * 2);
pop();
push();
textAlign(CENTER, TOP);
fill("yellow");
noStroke();
translate(0, 8 * sy);
rotate(-PI / 2);
textSize(sy / 2);
text(lambdaMeter ? floor(lambda) + "nm" : "λ-meter", 0, 0);
translate(2 * sy, 0);
fill("greenyellow");
text(gamestarted ? "restart" : "start", 0, 0);
translate(2 * sy, 0);
fill("yellow");
text("credits", 0, 0);
pop();
// display and animated bugs
if (gameInitialized) {
for (let i = bugs.length - 1; i >= 0; i--) {
bugs[i].move();
bugs[i].display(mode);
if (bugs[i].y > 18 * sy) gameover = true;
if (mode == "sanitise") {
// bug must be in front of the objective
if ((bugs[i].x > (obj.x - sx / 3)) &&
(bugs[i].x < (obj.x + sx / 3)) &&
(bugs[i].y > (obj.y + sx / 2)) &&
(bugs[i].y < (obj.y + 2 * sy))
) {
// bugs are removed only if wavelength is +/- 50nm
if (abs(bugs[i].color - lambda) < 50) {
let points = 100 - (floor(abs(bugs[i].color - lambda)));
scoreBubbles.push(new ScoreBubble(bugs[i].x, bugs[i].y, points));
bugs.splice(i, 1);
score += points;
}
}
}
}
// display score bubbles if any.
for (let i = scoreBubbles.length - 1; i >= 0; i--) {
scoreBubbles[i].display();
if (scoreBubbles[i].y0 - scoreBubbles[i].y > 30) scoreBubbles.splice(i, 1);
}
textSize(30);
textAlign(CENTER, CENTER);
// draw score
if (!gameover) {
textSize(30);
stroke(64);
strokeWeight(4);
fill(255, 255, 0);
textAlign(CENTER, CENTER);
text("Score: " + score, 5 * sx, sy);
}
// add a new set of viruses when all have been removed
// higher level are faster
// from level 3, some viruses do not display their color
if (bugs.length == 0) {
level++;
for (let i = 0; i < 10; i++) {
bugs[i] = new Bug(i, level);
}
overallSpeed += 0.05;
}
} else {
// display arrows
t = 64 * sin((frameCount % 30) * 2 * PI / 30);
if (help & 1) arrow(m2.x, m2.y, color(100, 255, 50, 64 + t), true, sx);
if (help & 2) arrow(m4.x, m4.y, color(100, 255, 50, 64 + t), true, sx);
if (help & 4) arrow(obj.x, obj.y, color(100, 255, 50, 64 + t), true, sx);
if (help & 4) arrow(obj.x, obj.y, color(100, 255, 50, 64 + t), false, sx);
if (help & 8) arrow(sx * 0.5 + 20, h, color(100, 255, 50, 64 + t), true, sx / 2);
if (help & 32) {
imageMode(CENTER);
image(glovicule, sx * 4, (17 + 0.2 * sin((frameCount % 50) * 2 * PI / 50)) * sy, sx * 3, 3 * sx * glovicule.height / glovicule.width);
}
}
// displaying the credits box
if (displayCredits) {
rectMode(CENTER);
fill(255);
rect(5 * sx, 10 * sy, 8 * sx, 5 * sy, sy / 2);
noStroke();
fill(0);
textSize(sy / 2);
textAlign(CENTER, CENTER);
text(creditsText, 5 * sx, 10 * sy);
}
// or the game over screen
else if (gameover) {
fill(255, 128, 128);
stroke(0);
strokeWeight(2);
textAlign(CENTER, CENTER);
textSize(sy);
text("Game Over!\n" + "Score: " + score, width / 2, height / 2);
}
}
function touchStarted() {
var fs = fullscreen();
if (!fs) {
fullscreen(true);
}
}
// full screening will change the size of the canvas
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
sx = windowWidth / 10;
sy = windowHeight / 20;
m2.y = 17 * sy;
m4.y = 8 * sy;
help = 255; // display all help items
}
/* prevents the mobile browser from processing some default
* touch events, like swiping left for "back" or scrolling
* the page.
*/
document.ontouchmove = function(event) {
event.preventDefault();
};
class ScoreBubble {
constructor(x, y, value) {
this.x = x;
this.y = y;
this.y0 = y;
this.speed = -1.5;
this.value = value;
}
display() {
this.y += this.speed;
fill(255);
strokeWeight(2);
textAlign(CENTER, CENTER);
textSize(sy / 2);
text(this.value, this.x, this.y);
}
}
class Bug {
constructor(c, level) {
this.x = 2 * sx + random(6 * sx);
this.y = 3 * sy + random(sy * 4);
this.speed = 1;
this.color = 380 + c * 40 + random(10);
this.hasTint = (level == 0) ? true : (random(1) < (1 / pow(2, (level - 1))));
}
move() {
this.x += random(-this.speed, this.speed) + 0.5 * ((this.x - laser.x) < 0) * (this.y > 9 * height / 10) - 0.5 * ((this.x - laser.x) > 0) * (this.y > 9 * height / 10);
this.x = constrain(this.x, 2 * sx, 8 * sx);
this.y += random(-this.speed, this.speed) + 0.2 + overallSpeed;
this.y = min(this.y, height - 20);
}
display(m) {
imageMode(CENTER);
if (this.hasTint)
tint(spectrum.get(0, spectrum.height - 1 - spectrum.height * (this.color - 380) / (400)));
else tint(128);
// halloween
// tint(spectrum.get(0, spectrum.height - 1 - spectrum.height * (609 - 380) / (400)));
image(virus, this.x, this.y, sy, sy);
if ((m == "probe") && (dist(0, this.y, 0, m2.y) < sy / 2)) {
textSize(sy / 3);
fill(255);
strokeWeight(0.5);
stroke(0);
textAlign(CENTER, CENTER);
text(floor(this.color), this.x, this.y);
}
noTint();
}
}
class Mirror {
constructor() {
this.y = height / 2;
this.x = 9.5 * sx;
}
display(angle) {
push();
translate(this.x, this.y);
rotate(angle);
imageMode(CENTER);
image(mirror, 0, 0, sx, sx);
pop();
}
displayAt(x, y, angle) {
this.y = y;
this.x = x;
this.display(angle);
}
}
class Obj {
constructor() {
this.y = height / 2;
this.x = width / 2;
}
display(withMirror) {
imageMode(CENTER);
image(objective, this.x, this.y, sx, sx);
if (withMirror) m5.displayAt(this.x, this.y - 1.5 * sy, -PI / 2);
}
}
function arrow(x, y, c, vertical, size) {
push();
stroke(c);
strokeWeight(4);
translate(x, y);
rotate(vertical ? PI / 2 : 0);
line(-size, 0, size, 0);
translate(-size, 0);
line(0, 0, size / 3, size / 3);
line(0, 0, size / 3, -size / 3);
translate(2 * size, 0);
rotate(PI);
line(0, 0, size / 3, size / 3);
line(0, 0, size / 3, -size / 3);
pop();
}