xxxxxxxxxx
1418
/*
Final project for Introduction to Interactive Media.
*/
const WIDTH = 576;
const HEIGHT = 576;
const ROOM_W = 9;
const ROOM_H = 9;
const TILE_W = 64;
const TILE_H = 64;
const FRAME_RATE = 60;
// The assets file is described at the bottom.
let assetsFile;
const levels = [
{ number: 1, strings: null },
{ number: 2, strings: null },
{ number: 3, strings: null },
{ number: 4, strings: null },
];
let gameEngine;
function preload() {
for (const level of levels) {
level.strings = loadStrings(`levels/level${level.number}.txt`);
}
gameEngine = new GameEngine();
}
function setup() {
frameRate(FRAME_RATE);
pixelDensity(1);
createCanvas(WIDTH, HEIGHT);
gameEngine.run();
}
function draw() {
gameEngine.update();
}
function keyPressed() {
gameEngine.sUserInput(key.toLowerCase(), keyCode, "START");
}
function keyReleased() {
gameEngine.sUserInput(key.toLowerCase(), keyCode, "END");
}
function readSerial(data) {
if (data != null) {
gameEngine.readArduino(data);
}
}
class GameEngine {
constructor() {
this.assets = new Assets(assetsFile);
this.sceneMap = new Map();
this.currentScene = null;
this.running = false;
this.isConnectedArduino = false;
}
run() {
this.assets.loadQueuedAnimations();
this.running = true;
this.changeScene("MENU", new Scene_Menu(this));
}
changeScene(sceneName, scene, endCurrentScene) {
if (endCurrentScene) {
if (this.currentSceneName) {
this.sceneMap.delete(this.currentSceneName);
}
}
if (scene != null) {
this.sceneMap.set(sceneName, scene);
} else {
const foundScene = this.sceneMap.get(sceneName);
if (foundScene == null) {
print(`Warning: Scene does not exist: ${sceneName}`);
return;
}
}
this.currentSceneName = sceneName;
}
update() {
if (!this.running || this.sceneMap.size == 0) return;
// Update Arduino connection state.
this.isConnectedArduino = serialActive;
// this.isConnectedArduino = true;
// Update current scene.
const currentScene = this.sceneMap.get(this.currentSceneName);
currentScene.update();
currentScene.sRender();
}
quit() {
this.running = false;
}
sUserInput(key, keyCode, type) {
if (!this.running) return;
this.sceneMap.get(this.currentSceneName).sDoAction(key, keyCode, type);
}
connectArduino() {
setUpSerial();
}
readArduino(data) {
if (this.running) {
if (this.isConnectedArduino) {
this.sceneMap.get(this.currentSceneName).sReadArduino(data);
} else {
}
} else {
}
}
sendArduino(data) {
writeSerial(data);
}
}
class Scene_Menu {
constructor(game) {
this.game = game;
}
update() {}
sRender() {
rectMode(CORNERS);
background(0);
noStroke();
fill(255);
textAlign(LEFT);
textStyle(BOLD);
textSize(36);
text("Struggling Adventurer", 60, 100, WIDTH - 100, 200);
textStyle(NORMAL);
textSize(22);
if (!this.game.isConnectedArduino) {
text("Press SPACE to connect to Arduno.", 60, 300, WIDTH - 60, 400);
} else {
const instructions =
"Instructions:\n" +
"- Move with controller.\n\n" +
"Press SPACE to begin!";
text(instructions, 60, 300, WIDTH - 60, 400);
}
}
sDoAction(key, keyCode, type) {
if (type == "END") return;
if (key == " ") {
if (!this.game.isConnectedArduino) {
this.game.connectArduino();
} else {
const level = levels.find((it) => it.number == 1);
if (level == undefined) {
print("Couldn't load first level.");
} else {
this.game.changeScene("PLAY", new Scene_Play(this.game, level));
}
}
}
}
sReadArduino(data) {
// print("Got data", data);
this.game.sendArduino("1\n");
}
}
class Scene_End {
constructor(game) {
this.game = game;
}
update() {}
sRender() {
rectMode(CORNERS);
background(0);
noStroke();
fill(255);
textAlign(LEFT);
textStyle(BOLD);
textSize(36);
text("end", 60, 100, WIDTH - 100, 200);
textStyle(NORMAL);
textSize(22);
text("end", 60, 300, WIDTH - 60, 400);
}
sDoAction(key, keyCode, type) {
if (type == "END") return;
if (key == " " || keyCode == ESCAPE || keyCode == ENTER) {
this.game.changeScene("MENU", null, true);
}
}
sReadArduino(data) {
print("Got data", data);
this.game.sendArduino("1\n");
}
}
class Scene_Play {
constructor(game, level) {
this.game = game;
this.level = level;
this.currentFrame = 0;
this.drawShapes = true;
this.drawTextures = true;
this.drawCollision = false;
this.drawTextBoxes = true;
this.roomSize = createVector(ROOM_W, ROOM_H);
this.tileSize = createVector(TILE_W, TILE_H);
// Small optimization.
this.halfTileSize = this.tileSize.copy().div(2);
this.roomTileSizeMult = this.roomSize.copy().mult(this.tileSize);
this.playerConfig = {
RX: 0,
RY: 0,
TX: 0,
TY: 0,
CW: 0,
CH: 0,
SPEED: 0,
displayType: null,
displayName: null,
};
this.init();
}
init() {
this.loadLevel();
}
update() {
this.entityManager.update();
this.sAI();
this.sMovement();
this.sCollision();
this.sTriggers();
this.sScriptedEvents();
this.sAnimation();
this.sCamera();
this.currentFrame++;
}
onEnd(type) {
type = type === undefined ? "exit" : type;
if (type == "exit") {
this.game.changeScene("MENU", null, true);
} else if (type == "finish") {
const nextLevel = levels.find((it) => it.number == this.level.number + 1);
if (nextLevel == undefined) {
// This was the last level.
this.game.changeScene("END", new Scene_End(this.game), true);
} else {
this.game.changeScene(
"PLAY",
new Scene_Play(this.game, nextLevel),
true
);
}
}
}
loadLevel() {
this.entityManager = new EntityManager();
this.triggerStatus = new Map();
this.scriptedEvents = new EventScheduler();
this.osc = new p5.TriOsc();
this.osc.amp(0.5);
this.npcsToCollect = 0;
this.npcsCollected = 0;
this.numRotations = 0;
this.screenDilated = false;
// Main text box.
// {
// const e = this.entityManager.addEntity("textbox");
// e.c.shape = new CShape(
// "rect",
// this.tileSize.copy().mult(2),
// color(50, 50, 50)
// );
// e.c.transform = new CTransform(createVector(width / 2, height / 2));
// e.c.text = new CText("Starting...");
// }
for (
let lineIndex = 0;
lineIndex < this.level.strings.length;
lineIndex++
) {
const line = this.level.strings[lineIndex];
const words = line.split(/\s+/);
if (words.length == 0 || words[0] == "") continue;
const type = words[0];
if (type == "Player") {
// Store the player configuration.
this.playerConfig.displayType = words[1];
this.playerConfig.displayName = words[2];
this.playerConfig.RX = float(words[3]);
this.playerConfig.RY = float(words[4]);
this.playerConfig.TX = float(words[5]);
this.playerConfig.TY = float(words[6]);
this.playerConfig.CW = float(words[7]);
this.playerConfig.CH = float(words[8]);
this.playerConfig.SPEED = float(words[9]);
} else if (type == "Tile") {
// Get the tile's animation and position, and create it.
const displayType = words[1];
const displayName = words[2];
const room = createVector(float(words[3]), float(words[4]));
const tile = createVector(float(words[5]), float(words[6]));
const blockMove = int(words[7]);
const entity = this.entityManager.addEntity("tile");
this.setShapeOrAnimation(entity, displayType, displayName);
entity.c.bbox = new CBoundingBox(
this.getEntitySize(entity),
blockMove == 1
);
entity.c.transform = new CTransform(
this.getPosition(room, tile, entity)
);
} else if (type == "Trigger") {
const displayType = words[1];
const displayName = words[2];
const room = createVector(float(words[3]), float(words[4]));
const tile = createVector(float(words[5]), float(words[6]));
const triggerId = int(words[7]);
const entity = this.entityManager.addEntity("trigger");
this.setShapeOrAnimation(entity, displayType, displayName);
entity.c.bbox = new CBoundingBox(this.getEntitySize(entity), false);
entity.c.transform = new CTransform(
this.getPosition(room, tile, entity)
);
entity.c.trigger = new CTrigger(triggerId);
this.triggerStatus.set(triggerId, { start: null, end: null });
} else if (type == "NPC") {
const displayType = words[1];
const displayName = words[2];
const room = createVector(float(words[3]), float(words[4]));
const tile = createVector(float(words[5]), float(words[6]));
const blockMove = int(words[7]);
const collectible = int(words[8]);
const dangerous = int(words[9]);
const entity = this.entityManager.addEntity("npc");
this.setShapeOrAnimation(entity, displayType, displayName);
entity.c.bbox = new CBoundingBox(
this.getEntitySize(entity),
blockMove == 1
);
entity.c.transform = new CTransform(
this.getPosition(room, tile, entity)
);
if (collectible) {
entity.c.collectible = new CCollectible(true);
this.npcsToCollect += 1;
}
if (dangerous) {
entity.c.dangerous = new CDangerous(true);
}
} else if (type == "Room") {
// const rx = int(words[1]);
// const ry = int(words[2]);
// const color = words[3];
// if (color != "None") {
// if (!this.roomColors.has(rx)) {
// this.roomColors.set(rx, new Map());
// }
// this.roomColors.get(rx).set(ry, color);
// }
} else if (type == "DefaultBG") {
const color = words[1];
this.defaultRoomColor = color;
} else {
print(`Unknown Entity Type or Room config: ${type}`);
}
}
this.spawnPlayer();
}
sAI() {
let npcIndex = 0;
for (const npc of this.entityManager.get("npc")) {
const name = this.getDisplayName(npc);
if (name.includes("Car")) {
const velX = map(
noise(this.currentFrame * 0.01, npcIndex * 10, 0),
0,
1,
-this.playerConfig.SPEED,
this.playerConfig.SPEED
);
const velY = map(
noise(this.currentFrame * 0.01, npcIndex * 10, 10),
0,
1,
-this.playerConfig.SPEED,
this.playerConfig.SPEED
);
npc.c.transform.velocity.set(velX, velY);
}
npcIndex++;
}
}
sMovement() {
// Player movement.
for (const player of this.entityManager.get("player")) {
const transform = player.c.transform;
const input = player.c.input;
transform.prevPos.set(transform.pos);
const vel = transform.velocity;
vel.set(0, 0);
// Arrow key input. For debugging.
if (input.left) vel.x -= this.playerConfig.SPEED;
if (input.right) vel.x += this.playerConfig.SPEED;
if (input.up) vel.y -= this.playerConfig.SPEED;
if (input.down) vel.y += this.playerConfig.SPEED;
// Sensor input.
vel.x += input.sensorX;
vel.y += input.sensorY;
// Inverted controls.
if (input.inverted) {
vel.mult(-1);
}
transform.pos.add(vel);
}
// NPC Movement.
// Velocities are calculated in the AI system.
for (const npc of this.entityManager.get("npc")) {
npc.c.transform.pos.add(npc.c.transform.velocity);
}
}
sCollision() {
// Player-trigger collisions.
// Should be before player-tile collisions so that touching triggers inside
// non-movable areas is detected.
for (const player of this.entityManager.get("player")) {
for (const trigger of this.entityManager.get("trigger")) {
if (!trigger.active) continue;
const overlap = Physics.GetOverlap(trigger, player);
if (!(overlap.x > 0 && overlap.y > 0)) continue;
const triggerId = trigger.c.trigger.id;
// Update activation times for this trigger.
const status = this.triggerStatus.get(triggerId);
if (status.end == null || status.end != this.currentFrame - 1) {
// Either never activated before, or activated but not currently.
status.start = this.currentFrame;
}
status.end = this.currentFrame;
}
}
// Player-NPC collisions.
for (const player of this.entityManager.get("player")) {
if (!player.c.bbox.blockMove) continue;
for (const npc of this.entityManager.get("npc")) {
if (!npc.c.bbox.blockMove) continue;
const overlap = Physics.GetOverlap(player, npc);
if (!(overlap.x > 0 && overlap.y > 0)) continue;
if (npc.c.collectible != null && npc.c.collectible.collectible) {
this.npcsCollected += 1;
npc.destroy();
if (this.npcsCollected == this.npcsToCollect) {
this.scriptedEvents.add(this.currentFrame, { name: "OpenDoors" });
}
}
if (npc.c.dangerous != null && npc.c.dangerous.dangerous) {
this.scriptedEvents.add(this.currentFrame, {
name: "KillPlayer",
player: player,
});
}
this.scriptedEvents.add(this.currentFrame, { name: "InvertControls" });
this.scriptedEvents.add(this.currentFrame, { name: "FlipDilate" });
// npc.c.transform.prevPos.set(npc.c.transform.pos);
// npc.c.transform.pos.add(overlap);
}
}
// Entity-tile collisions.
for (const entity of this.entityManager.entities) {
if (entity.tag != "player" && entity.tag != "npc") continue;
if (entity.c.bbox == null || !entity.c.bbox.blockMove) continue;
const transform = entity.c.transform;
for (const tile of this.entityManager.get("tile")) {
if (!tile.c.bbox.blockMove) continue;
const overlap = Physics.GetOverlap(entity, tile);
if (!(overlap.x > 0 && overlap.y > 0)) {
// No overlap.
continue;
}
const prevOverlap = Physics.GetPreviousOverlap(entity, tile);
let shouldPushBackX = false;
let shouldPushBackY = false;
if (prevOverlap.x > 0 && prevOverlap.y > 0) {
shouldPushBackX = true;
shouldPushBackY = true;
} else if (prevOverlap.y > 0) {
shouldPushBackX = true;
} else if (prevOverlap.x > 0) {
shouldPushBackY = true;
} else {
if (overlap.x > overlap.y) {
shouldPushBackY = true;
} else {
shouldPushBackX = true;
}
}
if (shouldPushBackX) {
// If entity is to the left of the tile, subtract overlap; add it otherwise.
const isToLeft = transform.pos.x < tile.c.transform.pos.x;
transform.pos.x += isToLeft ? -overlap.x : overlap.x;
}
if (shouldPushBackY) {
// If entity is below the tile, subtract overlap; add it otherwise.
const isAbove = transform.pos.y < tile.c.transform.pos.y;
transform.pos.y += isAbove ? -overlap.y : overlap.y;
}
}
}
}
sTriggers() {
const cutoff = floor(FRAME_RATE / 4);
for (const [id, status] of this.triggerStatus.entries()) {
if (status.start == this.currentFrame) {
// print("Triggering", id);
if (id == 1) {
// Open door.
// for (const e of this.entityManager.get("tile")) {
// const name = this.getDisplayName(e);
// if (
// name != null &&
// (name.includes("GreyRock") || name.includes("Door"))
// ) {
// e.destroy();
// }
// }
} else if (id == 2) {
// End level.
// this.onEnd("finish");
} else {
// Play sound.
// const freq = map(id, 1, 8, 80, 2000);
// this.osc.freq(freq);
// this.osc.start();
}
} else if (status.start == this.currentFrame - cutoff) {
// this.osc.stop();
}
}
}
sScriptedEvents() {
for (const event of this.scriptedEvents.get(this.currentFrame)) {
if (event.name == "KillPlayer") {
event.player.destroy();
this.spawnPlayer();
if (this.level.number == 3) {
this.numRotations++;
}
} else if (event.name == "OpenDoors") {
// Open doors.
for (const tile of this.entityManager.get("tile")) {
const name = this.getDisplayName(tile);
if (
name == "GreyRock" ||
name == "RedTreeEyes" ||
name == "BlueBlock"
) {
tile.destroy();
}
}
} else if (event.name == "InvertControls") {
if (this.level.number == 4) {
for (const player of this.entityManager.get("player")) {
player.c.input.inverted = !player.c.input.inverted;
}
}
} else if (event.name == "FlipDilate") {
this.screenDilated = !this.screenDilated;
}
}
this.scriptedEvents.delete(this.currentFrame);
}
sAnimation() {
// Update all animations and destroy entities with finished nonrepeating animations.
for (const entity of this.entityManager.entities) {
const animation = entity.c.animation;
if (animation == null) continue;
animation.animation.update();
if (!animation.repeat && animation.animation.hasEnded()) {
entity.destroy();
}
}
}
sCamera() {
// Room-based camera.
// Get the player's room.
const room = this.getRoom(this.entityManager.get("player")[0]);
// Set room background color.
// const mp = this.roomColors.get(room.x);
// let foundColor = false;
// if (mp != null) {
// const clr = mp.get(room.y);
// if (clr != null) {
// this.currentRoomColor = clr;
// foundColor = true;
// }
// }
// if (!foundColor) {
// this.currentRoomColor = this.defaultRoomColor;
// }
// Get the pixel coordinates of the top left corner of this room.
room.mult(this.roomTileSizeMult);
// Translate so that the entities in this room will be drawn.
translate(-room.x, -room.y);
if (room.x != 0 || room.y != 0) {
// Player exited the room.
this.onEnd("finish");
}
}
sRender() {
angleMode(DEGREES);
imageMode(CENTER);
const renderShape = (e) => {
const transform = e.c.transform;
if (transform == null) return;
if (e.c.animation != null && this.drawTextures) {
const animation = e.c.animation.animation;
push();
translate(transform.pos.x, transform.pos.y);
// scale(transform.scale.x, transform.scale.y);
// rotate(transform.angle);
if (this.numRotations > 0) {
rotate(this.numRotations * 45);
}
rectMode(CORNER);
image(
animation.sprite,
0,
0,
animation.size.x,
animation.size.y,
animation.drawX,
0,
animation.size.x
);
pop();
} else if (e.c.shape != null && this.drawShapes) {
const shape = e.c.shape.shape;
if (shape.fillColor == null) {
noFill();
} else {
fill(ashape.fillColor);
}
if (shape.strokeWidth == null) {
noStroke();
} else {
strokeWeight(shape.strokeWidth);
stroke(shape.strokeColor);
}
if (shape.type == "rect") {
rectMode(CENTER);
rect(transform.pos.x, transform.pos.y, shape.size.x, shape.size.y);
} else if (shape.type == "ellipse") {
ellipse(transform.pos.x, transform.pos.y, shape.size.x, shape.size.y);
}
}
};
background(this.defaultRoomColor);
if (this.drawTextBoxes) {
// Draw text boxes.
rectMode(CENTER);
textAlign(CENTER);
noFill();
noStroke();
for (const e of this.entityManager.get("textbox")) {
renderShape(e);
const pos = e.c.transform.pos;
const size = e.c.shape.shape.size;
fill(255);
text(e.c.text.text, pos.x, pos.y);
}
}
if (this.drawShapes || this.drawTextures) {
// Draw everything with a shape or animation.
for (const e of this.entityManager.get("tile")) {
renderShape(e);
}
for (const e of this.entityManager.get("trigger")) {
renderShape(e);
}
for (const e of this.entityManager.get("npc")) {
renderShape(e);
}
for (const e of this.entityManager.get("player")) {
renderShape(e);
}
}
if (this.drawCollision) {
// Draw collision boxes.
rectMode(CENTER);
noFill();
strokeWeight(1);
for (const e of this.entityManager.entities) {
const bbox = e.c.bbox;
if (bbox == null) continue;
const transform = e.c.transform;
stroke(255);
if (bbox.blockMove) stroke(255, 0, 0);
rect(transform.pos.x, transform.pos.y, bbox.size.x, bbox.size.y);
}
}
if (this.level.number == 4) {
if (this.screenDilated) {
filter(DILATE);
}
}
}
sDoAction(key, keyCode, type) {
const p = this.entityManager.get("player")[0];
if (type == "START") {
if (keyCode == ESCAPE) this.onEnd("exit");
else if (key == "s") this.drawShapes = !this.drawShapes;
else if (key == "t") this.drawTextures = !this.drawTextures;
else if (key == "c") this.drawCollision = !this.drawCollision;
else if (keyCode == UP_ARROW) p.c.input.up = true;
else if (keyCode == LEFT_ARROW) p.c.input.left = true;
else if (keyCode == DOWN_ARROW) p.c.input.down = true;
else if (keyCode == RIGHT_ARROW) p.c.input.right = true;
else if (!isNaN(parseInt(key))) {
const number = parseInt(key);
const level = levels.find((it) => it.number == number);
print("pressed", key, number, level);
if (level) {
this.game.changeScene("PLAY", new Scene_Play(this.game, level), true);
}
}
} else if (type == "END") {
if (keyCode == UP_ARROW) p.c.input.up = false;
else if (keyCode == LEFT_ARROW) p.c.input.left = false;
else if (keyCode == DOWN_ARROW) p.c.input.down = false;
else if (keyCode == RIGHT_ARROW) p.c.input.right = false;
}
}
sReadArduino(data) {
// print("Got data", data);
// Turn x and y from [0, 1] to [-3, 3].
const [x, y] = data.trim().split(",");
const V = this.playerConfig.SPEED;
let sensorX = map(x, 0, 1, V, -V);
let sensorY = map(y, 0, 1, -V, V);
// If absolute value is this much, it probably means that the
// ball is not in the box. So set values to 0.
if (abs(sensorX) == V) {
sensorX = 0;
}
if (abs(sensorY) == V) {
sensorY = 0;
}
// Update input components of all players.
for (const player of this.entityManager.get("player")) {
const input = player.c.input;
input.sensorX = sensorX;
input.sensorY = sensorY;
}
// Debug printing.
// if (this.currentFrame % 10 == 0) {
// print("sensor", sensorX, sensorY);
// }
// Send answer to arduino.
this.game.sendArduino("2\n");
}
getPosition(room, tile, entity) {
// Return the midpoint pixel coordinates of some tile in some room.
const halfSize = this.getEntityHalfSize(entity);
return createVector(
room.x * this.roomTileSizeMult.x + tile.x * this.tileSize.x + halfSize.x,
room.y * this.roomTileSizeMult.y + tile.y * this.tileSize.y + halfSize.y
);
}
getEntityHalfSize(entity) {
return entity.c.animation != null
? entity.c.animation.animation.halfSize
: entity.c.shape != null
? entity.c.shape.shape.halfSize
: entity.c.bbox != null
? entity.c.bbox.halfSize
: this.halfTileSize;
}
getEntitySize(entity) {
return entity.c.animation != null
? entity.c.animation.animation.size
: entity.c.shape != null
? entity.c.shape.shape.size
: entity.c.bbox != null
? entity.c.bbox.size
: this.tileSize;
}
getRoom(entity) {
// Get top left corner of the entity, and use it to find the room column and row.
// They will be decimal numbers, e.g. (0.36, 1.3);
const room = entity.c.transform.pos
.copy()
.sub(this.getEntityHalfSize(entity))
.div(this.roomTileSizeMult);
// These numbers correspond to the room (0, 1);
room.x = floor(room.x);
room.y = floor(room.y);
return room;
}
setShapeOrAnimation(entity, displayType, displayName) {
if (displayType == "Shape") {
const shape = this.game.assets.shapeMap.get(displayName);
if (shape == null) {
print(`Could not find shape: ${displayName}`);
} else {
entity.c.shape = new CShape(shape);
}
} else if (displayType == "Animation") {
const animation = this.game.assets.animationMap.get(displayName);
if (animation == null) {
print(`Could not find animation: ${displayName}`);
} else {
entity.c.animation = new CAnimation(animation, true);
}
} else if (displayType != "None") {
print(`Unknown display type: ${displayType}`);
}
}
getDisplayName(entity) {
if (entity.c.shape != null) {
return entity.c.shape.shape.name;
} else if (entity.c.animation != null) {
return entity.c.animation.animation.name;
}
return null;
}
spawnPlayer() {
// Spawn the player.
const player = this.entityManager.addEntity("player");
this.setShapeOrAnimation(
player,
this.playerConfig.displayType,
this.playerConfig.displayName
);
player.c.transform = new CTransform(
this.getPosition(
createVector(this.playerConfig.RX, this.playerConfig.RY),
createVector(this.playerConfig.TX, this.playerConfig.TY),
player
)
);
player.c.bbox = new CBoundingBox(
createVector(this.playerConfig.CW, this.playerConfig.CH),
true
);
player.c.input = new CInput();
}
}
class Assets {
// The Assets class handles loading all the assets used in the game.
constructor(file) {
// Initialize texture maps and other things.
this.textureMap = new Map();
this.animationMap = new Map();
this.soundMap = new Map();
this.shapeMap = new Map();
this.animationsToLoad = [];
this.loadFile(file);
}
loadFile(file) {
// Load the assets from the file.
// All textures must be loaded before any animation, because textures
// are loaded from images from files.
// Therefore, we add animations to a queue and actually add them in
// the function loadQueuedAnimations().
for (const line of file) {
const words = line.split(/\s+/);
const type = words[0];
if (type == "Texture") {
const name = words[1];
const path = words[2];
this.addTexture(name, path);
} else if (type == "Animation") {
const name = words[1];
const texture = words[2];
const frames = int(words[3]);
const speed = float(words[4]);
this.animationsToLoad.push([name, texture, frames, speed]);
} else if (type == "Sound") {
const name = words[1];
const path = words[2];
this.addSound(name, path);
} else if (type == "Shape") {
const name = words[1];
const type = words[2];
const sizeX = int(words[3]);
const sizeY = int(words[4]);
let fillColor = words[5];
if (fillColor == "None") {
fillColor = null;
}
let strokeWidth = words[6];
let strokeColor = words[7];
if (strokeWidth == "None") {
strokeWidth = null;
strokeColor = null;
} else {
strokeWidth = int(strokeWidth);
}
this.addShape(
name,
new Shape(
name,
type,
createVector(sizeX, sizeY),
fillColor,
strokeWidth,
strokeColor
)
);
} else {
print(`Unknown Asset Type: ${type}`);
}
}
}
loadQueuedAnimations() {
// After all textures have been loaded, load all animations.
for (const [name, texture, frames, speed] of this.animationsToLoad) {
this.addAnimation(name, texture, frames, speed);
}
this.animationsToLoad = [];
}
addTexture(textureName, path) {
// When the image is loaded, add it to the texture map.
loadImage(path, (img) => {
// const scaleFactor = TILE_H / img.height;
// img.resize(int(img.width * scaleFactor), int(img.height * scaleFactor));
this.textureMap.set(textureName, img);
print(`Added Texture: ${textureName}`);
});
}
addAnimation(animationName, textureName, frameCount, speed) {
// The texture must be loaded already, or the textureMap will return undefined.
const texture = this.textureMap.get(textureName);
if (!texture) {
print(
`ERROR: Could not add Animation ${animationName} because Texture ${textureName} is not loaded.`
);
return;
}
this.animationMap.set(
animationName,
new Animation(animationName, texture, frameCount, speed)
);
print(`Added Animation: ${animationName}`);
}
addSound(soundName, path) {
loadSound(path, (sound) => {
// Lower volume.
sound.setVolume(0.35);
// Make it so doing sound.play() doesn't restart it.
sound.playMode("sustain");
// Add to map.
this.soundMap.set(soundName, sound);
print(`Added Sound: ${soundName}`);
});
}
addShape(shapeName, shape) {
this.shapeMap.set(shapeName, shape);
}
}
class EntityManager {
// The EntityManager class manages all entities in the game,
// i.e. their creation and destroyal every frame.
constructor() {
this.entities = [];
this.entitiesToAdd = [];
this.entityMap = new Map();
this.totalEntities = 0;
}
removeDeadEntities(lst) {
return lst.filter((it) => it.active);
}
update() {
// First, remove all dead entities.
this.entities = this.removeDeadEntities(this.entities);
for (const [k, v] of this.entityMap) {
this.entityMap.set(k, this.removeDeadEntities(v));
}
// Then, add all entities that should be added.
if (this.entitiesToAdd.length > 0) {
for (const e of this.entitiesToAdd) {
this.entities.push(e);
this.get(e.tag).push(e);
}
this.entitiesToAdd = [];
}
}
addEntity(tag) {
// When adding an entity, add it to the list, so it is
// truly added when the time is appropriate.
const e = new Entity(this.totalEntities++, tag);
this.entitiesToAdd.push(e);
return e;
}
get(tag) {
if (!this.entityMap.has(tag)) {
this.entityMap.set(tag, []);
}
return this.entityMap.get(tag);
}
}
class Entity {
constructor(id, tag) {
this.id = id;
this.tag = tag;
this.active = true;
this.c = new ComponentList();
}
destroy() {
this.active = false;
}
}
class ComponentList {
// A list of components that every entity can have, although they can be null.
constructor() {
this.transform = null;
this.bbox = null;
this.shape = null;
this.input = null;
this.trigger = null;
this.input = null;
this.text = null;
}
}
class CTransform {
/*
Has:
- The entity's current position.
- The entity's position in the previous frame.
- The entity's velocity.
*/
constructor(pos, velocity) {
this.pos = pos === undefined ? createVector(0, 0) : pos.copy();
this.prevPos = this.pos.copy();
this.velocity =
velocity === undefined ? createVector(0, 0) : velocity.copy();
}
}
class CBoundingBox {
/*
Has:
- The collision bounding box size.
- Whether it should block movement.
*/
constructor(size, blockMove) {
this.size = size === undefined ? createVector(0, 0) : size.copy();
this.halfSize = this.size.copy().div(2);
this.blockMove = blockMove;
}
}
class CShape {
constructor(shape) {
this.shape = shape;
}
}
class Shape {
/*
Contains information for displaying the entity:
- Type: square or circle.
- Size: size, a vector.
- Color: color, a p5 color object.
*/
constructor(name, type, size, fillColor, strokeWidth, strokeColor) {
this.name = name;
this.type = type;
this.size = size.copy();
this.halfSize = this.size.copy().div(2);
this.fillColor = fillColor;
this.strokeWidth = strokeWidth;
this.strokeColor = strokeColor;
}
}
class CAnimation {
// The animation component has just an animation, and a boolean value repeat
// which indicates whether the animation repeats, or happens once before stopping.
constructor(animation, repeat) {
this.animation = animation;
this.repeat = repeat;
}
}
class Animation {
/*
An animation consists of:
- Name: used for checking an entity's animation at some point in time after creation.
- Sprite/texture: the actual p5.Image that corresponds to the animation's spritesheet.
- frameCount: how many 'cells' the animation has in the sprite sheet.
- speed: A speed of 2 would make the animation change half as fast as a speed of 1.
An animation's size is the size of one cell, i.e. a height of the sprite's height
and a width of the sprite's width divided by the number of cells (frameCount).
The drawX parameter is the starting X coordinate of the current cell to draw.
*/
constructor(name, texture, frameCount, speed) {
this.name = name;
this.sprite = texture;
this.frameCount = frameCount ?? 1;
this.speed = speed ?? 1;
this.currentFrame = 0;
this.size = createVector(
this.sprite.width / this.frameCount,
this.sprite.height
);
this.halfSize = this.size.copy().div(2);
this.drawX = floor(this.currentFrame * this.size.x);
}
update() {
// Increase the current frame, and also set drawX.
this.currentFrame++;
if (this.speed > 0 && this.frameCount > 1) {
const frame = floor(this.currentFrame / this.speed) % this.frameCount;
this.drawX = frame * this.size.x;
}
}
hasEnded() {
// It has ended if there have been more changes to the cell than the frameCount
// or if its speed is 0.
return (
this.speed == 0 ||
floor(this.currentFrame / this.speed) >= this.frameCount
);
}
}
class CInput {
// Keeps track of specific properties related to user input.
constructor() {
this.up = false;
this.down = false;
this.left = false;
this.right = false;
this.sensorX = 0;
this.sensorY = 0;
}
}
class CTrigger {
// Tracks trigger id for trigger entities.
constructor(id) {
this.id = id;
}
}
class CText {
constructor(text) {
this.text = text;
}
}
class CCollectible {
constructor(collectible) {
this.collectible = true;
}
}
class CDangerous {
constructor(dangerous) {
this.dangerous = true;
}
}
class Physics {
static GetOverlap(a, b) {
// Get overlap amount between two entities.
const ta = a.c.transform;
const tb = b.c.transform;
const ca = a.c.bbox;
const cb = b.c.bbox;
return createVector(
ca.halfSize.x + cb.halfSize.x - abs(ta.pos.x - tb.pos.x),
ca.halfSize.y + cb.halfSize.y - abs(ta.pos.y - tb.pos.y)
);
}
static GetPreviousOverlap(a, b) {
// Get overlap amount between two entities, from the previous frame.
const ta = a.c.transform;
const tb = b.c.transform;
const ca = a.c.bbox;
const cb = b.c.bbox;
return createVector(
ca.halfSize.x + cb.halfSize.x - abs(ta.prevPos.x - tb.prevPos.x),
ca.halfSize.y + cb.halfSize.y - abs(ta.prevPos.y - tb.prevPos.y)
);
}
}
class EventScheduler {
// Helper class for scheduling events at certain future frames.
constructor() {
this.events = new Map();
}
get(frame) {
return this.events.get(frame) ?? [];
}
add(frame, event) {
if (!this.events.has(frame)) {
this.events.set(frame, []);
}
this.events.get(frame).push(event);
}
delete(frame) {
this.events.delete(frame);
}
}
assetsFile = [
"Shape UnderwaterWall rect 64 64 navy None None",
"Shape UnderwaterDoor rect 64 64 grey None None",
"Shape Bubble ellipse 36 36 None 3 white",
"Shape CityWall rect 64 64 maroon None None",
"Shape CityDoor rect 64 64 sienna None None",
"Shape SnowWall rect 64 64 lightsteelblue None None",
"Shape SnowDoor rect 64 64 steelblue None None",
"Shape Player1 ellipse 48 48 limegreen None None",
"Shape TriggerPurple rect 32 32 purple None None",
"Texture TexFish images/fish1.png",
"Texture TexGreenRock images/greenrock.png",
"Texture TexGreyRock images/greyrock.png",
"Texture TexCoral images/coral.png",
"Texture TexFishFood images/fishfood2.png",
"Texture TexToad images/toad.png",
"Texture TexCar images/car3.png",
"Texture TexCoin images/coin.png",
"Texture TexAsphalt images/asphalt2.png",
// "Texture TexBlackBrick images/blackbrick.png",
"Texture TexBrownBrick images/brownbrick.png",
"Texture TexIce images/ice.png",
"Texture TexGift images/gift.png",
"Texture TexSnowman images/snowman.png",
"Texture TexSanta images/santa.png",
"Texture TexBlueBlock images/blueblock.png",
"Texture TexRedTreeEyes images/redtreeeyes.png",
"Animation Fish TexFish 1 0",
"Animation GreenRock TexGreenRock 1 0",
"Animation GreyRock TexGreyRock 1 0",
"Animation Coral TexCoral 1 0",
"Animation FishFood TexFishFood 1 0",
"Animation Toad TexToad 1 0",
"Animation Car TexCar 1 0",
"Animation Coin TexCoin 4 120",
"Animation Asphalt TexAsphalt 1 0",
// "Animation BlackBrick TexBlackBrick 1 0",
"Animation BrownBrick TexBrownBrick 1 0",
"Animation Ice TexIce 1 0",
"Animation Gift TexGift 1 0",
"Animation Snowman TexSnowman 1 0",
"Animation Santa TexSanta 6 15",
"Animation BlueBlock TexBlueBlock 1 0",
"Animation RedTreeEyes TexRedTreeEyes 1 0",
];