var debugMode = false; // toggles debug mode
var char; // stores the character
var missiles = []; // stores the magic missiles
var enemies = []; // stores the enemies
var rooms = []; // stores the rooms
var roomKeys = []; // sotres the "name" of the room
var roomEnemies = []; // stores the leftover enemies of each room
var lastUsedFloorSeed = 1; // previous floor seed used
var currentRoom = "C"; // room id of current room
var directions = ["N", "E", "S", "W"]; // used in generating doors
var previousDir = "C"; // initiates with the previous value of C
var mouseCounter = 0; // how long has mouse been held for?
var spread = 30; // spread of the angle for magic missiles
var fireInterval = 5; // how often are missiles shot
var manaGain = 1.5; // how much mana regained per frame
var manaLost = 20; // mana expended to shoot a magic missile
var hitDamage = 1; // damage player takes if hit
var moveSpeed = 1.7; // how fast the player moves
var enemySpeed = 1.5; // how fast enemies move
var hitRadius = 15; // radius of enemy hitbox
var hitsToKill = 20; // how many hits an enemy needs to be killed
var doorLimit; // max amount of doors that can spawn
var floorsCleared = 0; // number of floors the player has cleared
var deathCounter = 0; // a timer after the player dies
var fadeCounter = 0; // the counter for fade out
var shouldFade = false; // should the screen fade?
var shotsFired= 0; // holds how many shots were fired
var enemiesSlain = 0; // holds how many skeletons were slain
var enemiesAlive = 0; // holds how many enemies in room are alive
function preload() { // load the textures
dungeonFloor = loadImage("https://i.imgur.com/VjkGIfD.jpg");
dungeonWalls = loadImage("https://i.imgur.com/z7qxLHe.png");
northDoor = loadImage("https://i.imgur.com/KmQ5l5A.png");
southDoor = loadImage("https://i.imgur.com/PS56sKG.png");
westDoor = loadImage("https://i.imgur.com/UvWhm3u.png");
eastDoor = loadImage("https://i.imgur.com/KNxPK3v.png");
hole = loadImage("https://i.imgur.com/2CX6TWN.png");
holeLight = loadImage("https://i.imgur.com/GR17LVV.png");
wizard = loadImage("https://i.imgur.com/3eZz0KO.png");
wizardNoMagic = loadImage("https://i.imgur.com/PI6dWjQ.png");
wizardHurt = loadImage("https://i.imgur.com/OF8kDJn.png");
staffGlow = loadImage("https://i.imgur.com/FaP3USD.png");
missileGlow = loadImage("https://i.imgur.com/kz9j30X.png");
deadSkeleton = loadImage("https://i.imgur.com/XPcZiRJ.png");
skeleton = loadImage("https://i.imgur.com/MmpBxbE.png");
skeletonHit = loadImage("https://i.imgur.com/KxqTfbM.png");
}
function setup() {
createCanvas(480, 480);
angleMode(DEGREES);
noCursor(); // hide cursor so that we can replace it with a custom one
makeCharacter(240, 240); // starts character in the center of the canvas
doorLimit = floor(random(6, 11)); // random door limit at the start
// generate the initial center room
centerRoom = {"floorSeed":1,
"doorSeed":1,
"directionEntered":"C",
"north":true, "south":false, "east":false, "west":false,
"final":false, "enemiesInRoom":[]
};
// push the room into the room array
rooms.push(centerRoom);
// push the room key into the room key array
roomKeys.push(currentRoom);
// push 0 enemies into the first room
roomEnemies.push(enemies);
// fade in
shouldFade = true;
fadeCounter = 120;
}
function draw() {
background(0); // backup in case textures don't load
drawRoom(); // draws the room
enemyUpdate(); // updates enemies in room
missileUpdate(); // updates the magic missiles
characterUpdate(); // updates the character
cursorUpdate(); // updates the cursor
doorCheck(); // checks if a player can go thorugh a door
deathCheck(); // checks if the player has died
fadeOut(); // checks if fadeout should execute
}
function fadeOut() { // fades the screen to black and back
// applies a red overlay based on character health
strokeWeight(0);
fill(255, 0, 0, map(char.health, 70, 0, 0, 100));
// if the character is dead, stop the red
if(char.health <= 0){
fill(0, 0, 0, 0);
}
rect(0, 0, width, height);
// counts the fade counter up to 128 if fade is started
if(fadeCounter < 128 & shouldFade === true) {
fadeCounter ++;
}
// after it reaches that point, fade is set back to false
if(fadeCounter >= 128) {
shouldFade = false;
}
// if fade is false, decrement the counter
if(shouldFade === false) {
fadeCounter --;
}
// add a black box of darkening opacity to fade out
fill(0, 0, 0, fadeCounter * 2);
rect(0, 0, width, height);
}
function deathCheck() { // checks to see if the character has died
// grabs the index of the current room
// needs to be a local variable or it just doesn't work
var roomIndex = roomKeys.indexOf(currentRoom);
push();
// if character is dead:
if(char.health <= 0) {
// increment death counter
deathCounter++;
// do a special death fadeout with a black box
strokeWeight(0);
fill(0, 0, 0, deathCounter * 2);
rect(0, 0, width, height);
}
// after a certain amount of time to fade:
if(deathCounter > 120) {
// fade in text that the character has died
textAlign(CENTER);
textSize(32);
strokeWeight(0);
textStyle(BOLD);
fill(255, 0, 0, (deathCounter - 120));
text("You have Died", width / 2, 150);
// format the bottom text
fill(0);
textSize(14);
strokeWeight(1.3);
stroke(255, 0, 0, (deathCounter - 240));
textStyle(ITALIC);
// fade in how many floors were cleared
if (floorsCleared === 1) {
text(floorsCleared + " floor freed from evil's grasp", width / 2, 200);
} else {
text(floorsCleared + " floors freed from evil's grasp", width / 2, 200);
}
// fade in how many magic missiles were cast
stroke(255, 0, 0, (deathCounter - 360));
text("Magic missile was cast " + shotsFired + " times", width / 2, 230);
// fade in how many enemies were defeated
stroke(255, 0, 0, (deathCounter - 480));
text(enemiesSlain - enemiesAlive + " of evil's minions were put to rest", width / 2, 260);
// after a while, offer the option to replay
textSize(18);
strokeWeight(0);
textStyle(BOLD);
fill(255, 0, 0, (deathCounter - 800));
text("press space to play again", width / 2, 300);
}
pop();
}
function doorCheck() { // checks if a player can enter a door or hole
// grabs the index of the current room
var roomIndex = roomKeys.indexOf(currentRoom);
// sets initial value if all enemies have been defeated
var allEnemiesDefeated = true;
// for all enemies in room:
for(var i = 0; i < rooms[roomIndex].enemiesInRoom.length; i++) {
// if one is alive, allEnemiesDefeated is false
if(rooms[roomIndex].enemiesInRoom[i].life < 100) {
allEnemiesDefeated = false;
}
}
// if the character is near and going in the direction of a door
// change the room in that direction
if(dist(char.x, char.y, width / 2, 0) < 60 &
char.up === true &&
rooms[roomIndex].north === true &&
allEnemiesDefeated === true) {
char.y = 440;
changeRoom("N");
}
if(dist(char.x, char.y, width / 2, height) < 60 &
char.down === true &&
rooms[roomIndex].south === true &&
allEnemiesDefeated === true) {
char.y = 40;
changeRoom("S");
}
if(dist(char.x, char.y, 0, height / 2) < 60 &
char.left === true &&
rooms[roomIndex].west === true &&
allEnemiesDefeated === true) {
char.x = 440;
changeRoom("W");
}
if(dist(char.x, char.y, width, height / 2) < 60 &
char.right === true &&
rooms[roomIndex].east === true &&
allEnemiesDefeated === true) {
char.x = 40;
changeRoom("E");
}
if(dist(char.x, char.y, width / 2, height / 2) < 30 &
rooms[roomIndex].final === true &&
allEnemiesDefeated === true) {
resetFloor();
}
}
function drawRoom() { // draws the room each update cycle
// grabs the index of the current room
var roomIndex = roomKeys.indexOf(currentRoom);
// displays floor based on floorseed
if(rooms[roomIndex].floorSeed === 1) {
image(dungeonFloor, 0, 0, width, height);
} else if (rooms[roomIndex].floorSeed === 2) {
push();
rotate(90);
image(dungeonFloor, 0, -1 * height, width, height);
pop();
} else if (rooms[roomIndex].floorSeed === 3) {
push();
rotate(180);
image(dungeonFloor, -1 * width, -1 * height, width, height);
pop();
} else if (rooms[roomIndex].floorSeed === 4) {
push();
rotate(270);
image(dungeonFloor, -1 * width, 0, width, height);
pop();
}
// displays walls
image(dungeonWalls, 0, 0, width, height);
// display doors
if(rooms[roomIndex].north === true) {
image(northDoor, 0, 0, width, height);
}
if(rooms[roomIndex].south === true) {
image(southDoor, 0, 0, width, height);
}
if(rooms[roomIndex].west === true) {
image(westDoor, 0, 0, width, height);
}
if(rooms[roomIndex].east === true) {
image(eastDoor, 0, 0, width, height);
}
// display hole if final room
if(rooms[roomIndex].final === true) {
image(hole, 0, 0, width, height);
}
// display light if first room
if(currentRoom === "C") {
image(holeLight, 0, 0, width, height);
}
// debug options
if(debugMode === true) {
strokeWeight(0);
fill(255);
stroke(255);
text("room ID = " + currentRoom, 20, 20);
text("rooms left = " + doorLimit, 20, 30);
text("north = " + rooms[roomIndex].north, 20, 40);
text("south = " + rooms[roomIndex].south, 20, 50);
text("east = " + rooms[roomIndex].east, 20, 60);
text("west = " + rooms[roomIndex].west, 20, 70);
text("final = " + rooms[roomIndex].final, 20, 80);
text("Number of enemies = " + rooms[roomIndex].enemiesInRoom.length, 20, 400);
text("floors cleared = " + floorsCleared, 20, 90);
}
}
function changeRoom(directionEntered) { // if players enter a door, change the room
// set a random floor seed to determine the floor image rotation
var floorSeed = ceil(random(0, 4));
// make sure the last floor seed is different from the new one
while(floorSeed === lastUsedFloorSeed) {
floorSeed = ceil(random(0, 4));
}
// reset the last used floor seed
lastUsedFloorSeed = floorSeed;
// Add doorseed based on how many are left
var doorSeed = ceil(random(0, 3));
// This giant block manages room ids, if you enter a new room that direction
// is added to the string for the name, and removes the last letter of the string
// if you backtrack into a previously explored room
if(directionEntered === "N" & previousDir === "S") {
currentRoom = currentRoom.substr(0, currentRoom.length - 1);
} else if(directionEntered === "S" & previousDir === "N") {
currentRoom = currentRoom.substr(0, currentRoom.length - 1);
} else if(directionEntered === "E" & previousDir === "W") {
currentRoom = currentRoom.substr(0, currentRoom.length - 1);
} else if(directionEntered === "W" & previousDir === "E") {
currentRoom = currentRoom.substr(0, currentRoom.length - 1);
// if the current room has not been entered, append the
// direction entered to the end of current room
} else {
currentRoom = currentRoom + directionEntered;
}
// sets previous direction to the direction used to enter the room
previousDir = currentRoom.substring(currentRoom.length - 1, currentRoom.length);
// sets initial door values
var doorNorth = false;
var doorSouth = false;
var doorEast = false;
var doorWest = false;
var doorFinal = false;
// if the room entered doesn't exist yet:
if(roomKeys.indexOf(currentRoom) == -1) {
// subrtact one from the remaining rooms to generate
doorLimit --;
// set the starting position of where the player entered
var initial = directions.indexOf(directionEntered);
// generates a random door based on where the character entered
// and the door seed
var doorNum = initial + 2 + doorSeed;
// make sure doorNum is between 0 and 3
while(doorNum > 3) {
doorNum -= 4;
}
// ties the door num to a direction string
var doorToDisplay = directions[doorNum];
// if it's the final room of the floor:
if(doorLimit === 0) {
// make sure the room know's it's the final one
doorFinal = true;
// generate the final door on the same side as would enter from
var finalDoorNum = initial + 2;
while(finalDoorNum > 3) {
finalDoorNum -= 4;
}
// tie the final door number to a direction string
doorToDisplay = directions[finalDoorNum];
}
// take the door to display as well as the door the character came from
// and add those properties to the new room
if(doorToDisplay === "N" || directionEntered === "S") {
doorNorth = true;
}
if(doorToDisplay === "S" || directionEntered === "N") {
doorSouth = true;
}
if(doorToDisplay === "E" || directionEntered === "W") {
doorEast = true;
}
if(doorToDisplay === "W" || directionEntered === "E") {
doorWest = true;
}
}
// add a new room to the room array
rooms.push(makeNewRoom(doorSeed, floorSeed, directionEntered,
doorNorth, doorSouth, doorEast, doorWest,
doorFinal));
// add the room ID to the list of roomKeys
roomKeys.push(currentRoom);
// sets local room index
var roomIndex = roomKeys.indexOf(currentRoom);
// resets the arrays for missiles and enemies, clearing the room
missiles = [];
enemies = [];
// generate new enemies
if(doorFinal === false & roomKeys.indexOf(currentRoom) === roomKeys.length - 1) {
for(var i = 0; i < (random(2, 4 + floorsCleared) + floorsCleared); i++) {
rooms[roomIndex].enemiesInRoom.push(makeEnemy(random(100, 380), random(100, 380), 0, 0));
// rotate random directions
rooms[roomIndex].enemiesInRoom[i].right(degrees(random(360)));
// set random speed that increases each floor
rooms[roomIndex].enemiesInRoom[i].speed = random(1.3, 1.8) + 0.05 * floorsCleared;
// set random move mode
rooms[roomIndex].enemiesInRoom[i].moveMode = floor(random(2));
}
}
}
function makeNewRoom(doorSeed, floorSeed, directionEntered,
north, south, east, west,
final) { // makes the room
room = {"floorSeed":floorSeed,
"doorSeed":doorSeed,
"directionEntered":directionEntered,
"north":north, "south":south, "east":east, "west":west,
"final":final,
"enemiesInRoom":[]
};
return room;
}
function resetFloor() { // resets the floor if cleared
// heal some of the character's health
char.health += floor(((100 - char.health) * 3 / 5));
// reset floor containers
missiles = [];
enemies = [];
rooms = [];
roomKeys = [];
previousDir = "C"; // initiates with the previous value of C
currentRoom = "C"; // current room is the center room
// make a new door limit
doorLimit = floor(random(6, 11));
// add 1 to number of floors cleared
floorsCleared ++;
// define the new center room of the floor
centerRoom = {"floorSeed":1,
"doorSeed":1,
"directionEntered":"C",
"north":true, "south":false, "east":false, "west":false,
"final":false,
"enemiesInRoom":[]
};
// push the room into the rooms array
rooms.push(centerRoom);
// push the room ID into the room keys array
roomKeys.push(currentRoom);
// push 0 enemies into the array
roomEnemies.push(enemies);
// execute fade out
shouldFade = true;
fadeCounter = 0;
// position character to the center of the room
char.x = width / 2;
char.y = height / 2;
}
function makeCharacter(x, y) { // creates the character object
char = {"x":x, "y":y,
"left":false, "right":false,
"up":false, "down":false,
"facing":0, "health":100, "mana":100,
"isHit":false
};
return char;
}
function characterUpdate() { // updates the character's movement
// if the movement key is pressed, set the appropriate character move state
if (keyIsDown(68) || keyIsDown(RIGHT_ARROW)) { // D or right arrow
char.right = true;
} else {
char.right = false;
}
if (keyIsDown(65) || keyIsDown(LEFT_ARROW)) { // A or left arrow
char.left = true;
} else {
char.left = false;
}
if (keyIsDown(83) || keyIsDown(DOWN_ARROW)) { // S or down arrow
char.down = true;
} else {
char.down = false;
}
if (keyIsDown(87) || keyIsDown(UP_ARROW)) { // W or up arrow
char.up = true;
} else {
char.up = false;
}
// Face the mouse
push();
translate(char.x, char.y);
// this gets an angle between 0 and 360
char.facing = ((atan2(mouseY - char.y, mouseX - char.x) + 360)% 360);
pop();
// the update moves and draws the character
characterMove();
characterDraw();
// regain mana
char.mana = min(100, char.mana += manaGain)
return;
}
function characterMove() { // moves the character
// if it's not fading and the character is alive:
if(deathCounter === 0 & shouldFade === false) {
// if the movement states are true, move by a set speed
// move linearly slightly faster
if(char.right === true && char.up === false && char.down === false) {
char.x += moveSpeed * sqrt(2);
}
if(char.left === true & char.up === false && char.down === false) {
char.x -= moveSpeed * sqrt(2);
}
if(char.up === true & char.left === false && char.right === false) {
char.y -= moveSpeed * sqrt(2);
}
if(char.down === true & char.left === false && char.right === false) {
char.y += moveSpeed * sqrt(2);
}
// move diagonally
if(char.right === true & char.up === true && deathCounter === 0) {
char.x += moveSpeed;
char.y -= moveSpeed;
}
if(char.right === true & char.down === true && deathCounter === 0) {
char.x += moveSpeed;
char.y += moveSpeed;
}
if(char.left === true & char.up === true && deathCounter === 0) {
char.x -= moveSpeed;
char.y -= moveSpeed;
}
if(char.left === true & char.down === true && deathCounter === 0) {
char.x -= moveSpeed;
char.y += moveSpeed;
}
}
// limit x and y coords to inside the walls
char.x = constrain(char.x, 40, 440);
char.y = constrain(char.y, 40, 440);
}
function characterDraw() { // draws the character
// style
fill(255);
stroke(5);
strokeWeight(2);
// glow under character
image(missileGlow, char.x - 120 + sin((char.facing + 247) * -1) * 40,
char.y - 120 + cos((char.facing + 247) * -1) * 40,
240, 240);
// Main Character // // // // // // // // // //
push();
// make the main character the origin
translate(char.x, char.y);
// rotate the character based on its angle
rotate(char.facing - 80);
// display ,agic hand if shooting magic missiles or normal if not
if(mouseIsPressed) {
image(wizard, -60, -45, 120, 120);
} else {
image(wizardNoMagic, -60, -45, 120, 120);
}
// if the character is hit display a red overlay
if(char.isHit === true) {
image(wizardHurt, -60, -45, 120, 120);
}
// make the staff glow based on how much mana the character has
push();
tint(255, map(char.mana, 0, 100, 0, 180))
image(staffGlow, -32, 4, 20, 20);
pop();
// reset the main character push
pop();
// Debugging Options
// if debug is true, display:
strokeWeight(2);
if(debugMode === true) {
stroke(255);
line(char.x, char.y, mouseX, mouseY);
stroke(0);
// what direction is the character facing?
text("Facing:" + floor(char.facing), char.x + 20, char.y + 10);
// what are its coordinates?
text("X:" + floor(char.x) + " Y:" + floor(char.y), char.x + 20, char.y + 20);
// if the mouse is pressed, what's the mouse counter?
if(mouseIsPressed) {
text("mouseCounter:" + mouseCounter, char.x + 20, char.y + 30);
}
// how much mana does it have?
text("Mana:" + char.mana, char.x + 20, char.y + 40);
// how much health?
text("Health:" + char.health, char.x + 20, char.y + 50);
// is the character hit?
text("isHit:" + char.isHit, char.x + 20, char.y + 60);
}
}
function makeEnemy(ex, ey, cx, cy) { // creates the enemy object
// make a turtle at the enemy coordinates with proxy target coords
enemy = makeTurtle(ex, ey, cx, cy);
enemy.angle = 1;
enemy.penUp();
enemy.setWeight(4);
enemy.setColor(255);
// add 1 to total amount of enemies slain
enemiesSlain ++;
return enemy;
}
function enemyUpdate() { // updates the enemies' movement
// sets local room index
var roomIndex = roomKeys.indexOf(currentRoom);
// set initial player collision value
var hasBeenHit = false;
// set initial enemies alive value
enemiesAlive = 0;
// go through all the enemies
for(var i = 0; i < rooms[roomIndex].enemiesInRoom.length; i++) {
// if the enemy is alive:
if(rooms[roomIndex].enemiesInRoom[i].life < 100) {
// add 1 to enemiesAlive
enemiesAlive ++;
// face the character using a more aggressive, snappy formula
if (rooms[roomIndex].enemiesInRoom[i].moveMode === 0) {
rooms[roomIndex].enemiesInRoom[i].face(degrees((atan2(
char.y - rooms[roomIndex].enemiesInRoom[i].y,
char.x - rooms[roomIndex].enemiesInRoom[i].x) + 360)% 360));
// or use the short interesting way where enemies wander
} else if(rooms[roomIndex].enemiesInRoom[i].moveMode === 1) {
rooms[roomIndex].enemiesInRoom[i].right(rooms[roomIndex].enemiesInRoom[i].angleTo(char.x, char.y));
}
// move forward
rooms[roomIndex].enemiesInRoom[i].forward(rooms[roomIndex].enemiesInRoom[i].speed);
// constrain enemy movement
rooms[roomIndex].enemiesInRoom[i].x = constrain(rooms[roomIndex].enemiesInRoom[i].x, 40, 440);
rooms[roomIndex].enemiesInRoom[i].y = constrain(rooms[roomIndex].enemiesInRoom[i].y, 40, 440);
// display as alive
push();
// make the enemy the origin
translate(rooms[roomIndex].enemiesInRoom[i].x, rooms[roomIndex].enemiesInRoom[i].y);
// rotate based on its angle
rotate(radians(rooms[roomIndex].enemiesInRoom[i].angle) - 90);
// display the monster image
image(skeleton, -60, -55, 120, 120);
pop();
} else { // if the enemy is dead
// display as dead
push();
// make the enemy the origin
translate(rooms[roomIndex].enemiesInRoom[i].x, rooms[roomIndex].enemiesInRoom[i].y);
// rotate based on angle
rotate(radians(rooms[roomIndex].enemiesInRoom[i].angle) - 90);
// display the dead monster image
image(deadSkeleton, -60, -60, 120, 120);
pop();
}
// test if missile collides
// for all missiles:
for(var j = 0; j < missiles.length; j++) {
// if the enemy is less than hitRadius from the missile:
if(dist(rooms[roomIndex].enemiesInRoom[i].x, rooms[roomIndex].enemiesInRoom[i].y,
missiles[j].x, missiles[j].y) < hitRadius) {
// add a portion of 100 based on the hitsToKill variable
rooms[roomIndex].enemiesInRoom[i].life += 100 / hitsToKill;
// if an enemy has less than 100 life (aka is alive):
if(rooms[roomIndex].enemiesInRoom[i].life < 100){
// display the hurt skeleton
push();
// make the skeleton the origin
translate(rooms[roomIndex].enemiesInRoom[i].x, rooms[roomIndex].enemiesInRoom[i].y);
// rotate based on enemy angle
rotate(radians(rooms[roomIndex].enemiesInRoom[i].angle) - 90);
// display the white overlay on the skeleton
image(skeletonHit, -60, -55, 120, 120);
pop();
}
}
}
// test if it hits player
// if the enemy x and y is withing hitRadius from the character x and y:
if(dist(rooms[roomIndex].enemiesInRoom[i].x, rooms[roomIndex].enemiesInRoom[i].y, char.x, char.y) < hitRadius &
rooms[roomIndex].enemiesInRoom[i].life < 100) {
// character health goes down by hit damage
char.health -= hitDamage;
// character hasBeenHit becomes true to display red overlay
hasBeenHit = true;
}
// if too far away, change to aggressive move mode
if(dist(rooms[roomIndex].enemiesInRoom[i].x, rooms[roomIndex].enemiesInRoom[i].y, char.x, char.y) > width * 2 / 3) {
rooms[roomIndex].enemiesInRoom[i].moveMode = 0;
}
// if too close, change to passive move mode
if(dist(rooms[roomIndex].enemiesInRoom[i].x, rooms[roomIndex].enemiesInRoom[i].y, char.x, char.y) < hitRadius) {
rooms[roomIndex].enemiesInRoom[i].moveMode = 1;
}
// Debug Options
// if debugMode is on:
if(debugMode === true) {
// display
strokeWeight(1);
stroke(255, 0, 0);
fill(0, 0, 0, 0);
// what is the enemy's angle?
text("Angle:" + floor(radians(rooms[roomIndex].enemiesInRoom[i].angle)),
rooms[roomIndex].enemiesInRoom[i].x + 20,
rooms[roomIndex].enemiesInRoom[i].y + 20);
// what is the enemy's health?
text("Health:" + floor(rooms[roomIndex].enemiesInRoom[i].life),
rooms[roomIndex].enemiesInRoom[i].x + 20,
rooms[roomIndex].enemiesInRoom[i].y + 30);
// what is the enemy's angle to player?
text("angle2Player:" + floor(rooms[roomIndex].enemiesInRoom[i].angleto(char.x, char.y)),
rooms[roomIndex].enemiesInRoom[i].x + 20,
rooms[roomIndex].enemiesInRoom[i].y + 10);
// draw an ellipse to show the hitRadius of the enemy
ellipse(rooms[roomIndex].enemiesInRoom[i].x, rooms[roomIndex].enemiesInRoom[i].y, hitRadius * 2, hitRadius * 2);
}
}
// if any enemy in the room has hit the player:
if(hasBeenHit === true){
// isHit is true
char.isHit = true;
} else {
// isHit is false
char.isHit = false;
}
}
function cursorUpdate() { // replaces the mouse cursor with a new one
// displays a custom "cursor" over the mouse position
// since the mouse cursor is hidden in setup
fill(0, 0, 0, 0);
stroke(255, 0, 0);
strokeWeight(2);
ellipse(mouseX, mouseY, 8, 8);
}
function missileUpdate() { // updates the magic missiles
// if the mouse is pressed and char alive, spawn a missile every interval
if(mouseIsPressed & deathCounter === 0) {
// increment mouseCounter, which is the number of frames the mouse has
// been held down for
mouseCounter++;
// every interval of frames after (if there's mana)
if(mouseCounter % fireInterval == 0 & char.mana > 0) {
// push the new missile into the array
missiles.push(makeMissile(char.x, char.y,
mouseX, mouseY,
char.facing));
}
} else { // if mouse isn't pressed, then reset the mouse counter
mouseCounter = 0;
}
// for each missile:
for(var i = 0; i < missiles.length; i++) {
// after a certain "age" the missile will be pushed out of the array
// increment age
missiles[i].life++;
// guide missile towards its target, turning more the closer it is
missiles[i].turnToward(missiles[i].tx, missiles[i].ty,
map(abs(missiles[i].angleTo(missiles[i].tx, missiles[i].ty)),
200, 0, 0, 320));
// draw the missile tail
fill(0, 255, 255, 99);
strokeWeight(2);
stroke(0, 255, 255, 0.9);
ellipse(missiles[i].x, missiles[i].y, 10, 10);
// move missile
missiles[i].forward(8);
// draw the missile glow
image(missileGlow, missiles[i].x - 60, missiles[i].y - 60, 120, 120);
// draw missile head
fill("White");
strokeWeight(0);
stroke(0, 255, 255, 0.9);
ellipse(missiles[i].x, missiles[i].y, 5, 5);
// cull old missiles
if(missiles[i].life > 70) {
missiles.shift();
}
}
}
function makeMissile(cx, cy, tx, ty, d) { // creates the magic missiles
// make a turtle at the character and with the target coords
// there's a bunch of math to make sure they come out of the
// magic aura around his hand
missile = makeTurtle(cx + sin((d + 247) * -1) * 40,
cy + cos((d + 247) * -1) * 40,
tx, ty);
// add a random offset to the launch trajectory
var offset = random(-1 * spread, spread);
// launch the missile
missile.right(degrees(d + offset));
missile.penUp();
// add 1 to total shots fired
shotsFired ++;
// character looses mana when a missile is shot
char.mana -= manaLost;
return missile;
}
function keyPressed() { // turns on debug mode when = is pressed
// if the "=" key is pressed, which I found was 187 or 69
if(keyCode === 187 || keyCode === 61) {
// if debug is on, turn it off
if (debugMode === true) {
debugMode = false;
// if debug is off, turn it on
} else {
debugMode = true;
}
}
// if spacebar is pressed at the end of the death sequence
if(deathCounter > 800 & keyCode === 32) {
// reset the floor
resetFloor();
// reset values that are not reset usually when a floor is
fadeCounter = 120;
deathCounter = 0;
floorsCleared = 0;
char.health = 100;
enemiesSlain = 0;
shotsFired = 0;
}
// if "e" is pressed while debugMode is on, spawn an enemy at the mouse
if(keyCode === 69 & debugMode === true) {
var roomIndex = roomKeys.indexOf(currentRoom);
rooms[roomIndex].enemiesInRoom.push(makeEnemy(mouseX, mouseY, 0, 0));
}
// if ENTER is pressed while debugMode is on, set doorLimit to 1,
// making sure the next room is the final room
if(keyCode === ENTER & debugMode === true) {
doorLimit = 1;
}
// if SHIFT is pressed while debugMode is on, increment floorsCleared
if(keyCode === SHIFT & debugMode === true) {
floorsCleared ++;
}
}
// turtle code has and added "tx" and "ty", which are coordinate points
// that are used to make the magic missiles home in on the clicked location.
// also have a "Life" that tracks how many frames it's been on screen for
function turtleLeft(d){this.angle-=d;}function turtleRight(d){this.angle+=d;}
function turtleForward(p){var rad=radians(this.angle);var newx=this.x+cos(rad)*p;
var newy=this.y+sin(rad)*p;this.goto(newx,newy);}function turtleBack(p){
this.forward(-p);}function turtlePenDown(){this.penIsDown=true;}
function turtlePenUp(){this.penIsDown = false;}function turtleGoTo(x,y){
if(this.penIsDown){stroke(this.color);strokeWeight(this.weight);
line(this.x,this.y,x,y);}this.x = x;this.y = y;}function turtleDistTo(x,y){
return sqrt(sq(this.x-x)+sq(this.y-y));}function turtleAngleTo(x,y){
var absAngle=degrees(atan2(y-this.y,x-this.x));
var angle=((absAngle-this.angle)+360)%360.0;return angle;}
function turtleTurnToward(x,y,d){var angle = this.angleTo(x,y);if(angle< 180){
this.angle+=d;}else{this.angle-=d;}}function turtleSetColor(c){this.color=c;}
function turtleSetWeight(w){this.weight=w;}function turtleFace(angle){
this.angle = angle;}function makeTurtle(tx,ty,targetx,targety){var turtle={x:tx,y:ty,
angle:0.0,penIsDown:true,color:color(128),weight:1,left:turtleLeft,
right:turtleRight,forward:turtleForward, back:turtleBack,penDown:turtlePenDown,
penUp:turtlePenUp,goto:turtleGoTo, angleto:turtleAngleTo,
turnToward:turtleTurnToward,distanceTo:turtleDistTo, angleTo:turtleAngleTo,
setColor:turtleSetColor, setWeight:turtleSetWeight,face:turtleFace,
tx:targetx, ty:targety, life:0, speed:1, moveMode:0};
return turtle;}
You can move with W,A,S,D or the arrow keys. Click to cast Magic Missile!
Tips and things to note:
The glow on your staff indicates how much mana you have. If you’re shooting slowly, let up on the firing and just let it recharge
The magic missiles bank towards where your mouse is when you fire them. Aim slightly ahead of enemies for best results
I might have gone a bit overkill.
When I first saw turtle graphics I was like “Huh, you could make a cool magic missile effect”
This game is built completely around that.
To briefly go over the game itself, it’s a dungeon crawler. The player goes into randomly generated rooms with randomly generated enemies which make up floors of random length. As the player progresses through the floors, the average enemy speed and number in a room will gradually increase. Paired with the player’s health that only partially returns when a floor is completed, it’s certain that the player will die. The question then is how far can they make it?
To help me troubleshoot the project, I added in a ‘debug mode’ of sorts. By pressing ‘=’, the player can see a lot of cheaty data, spawn enemies, generate the final room of each floor, and increase how many floors the game thinks the player has cleared.
There were a lot of challenges. I wasn’t sure how to do level generation (the system can easily do non-linear levels by generating more doors but linear is easy to play through) and for a while, enemy AI was giving me some problems.
In the end, I’m more than happy with what I made. I enjoy playing it, I enjoyed making it, and I enjoy changing some of the settings to make the game truly ridiculous.
Also the visual assets are either made by me or heavily changed free assets