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
For my final project, I was looking at making a top-down dungeon crawler game. There’s plenty of games like this, like some of the original Legend of Zelda Titles, but the one I saw as most relevant to my project was Edward McMillen and Florian Himsl’s 2011 Flash-based game, The Binding of Isaac. Admittedly, I’d heard of the newer version, but I never really looked at the older one, which was initially made in a week. The way the player moves through rooms makes sense mechanics-wise, and means generating levels should be simple (in theory).
My proposal for a final project would be a top-down view dungeon crawler game. I found that I was interested in using keyboard inputs as controls, and the natural development from there is a game.
Players would be a wizard, armed with a staff firing magic missiles (I think turtles would be good for a meandering projectile) going room to room vanquishing skeletons. I think rooms would all be square, but could spawn with doors. I could possibly make a system to make rooms and store their data, and start with a random numbers of doors that would decrease until rooms could no longer spawn with doors linking to new rooms. In this last room would be something that would be the goal, possibly a pile of treasure. If time becomes an issue, then the scope of the project could shrink to become a single room with waves of enemies, the goal being to last as long as you can.
The main inspiration was a Scratch project a friend and I built in our early high school days.
]]>
A user’s generated music
While looking at various computational instruments, I ended up on a small tangent that lead to the discovery of not a person, but a tool. Specifically, that tool was WolframTones, created 2005 by Wolfram Research, based on research from the 1980s. I found it interesting in that unlike what I looked at during week 4, this was a tool that was more centered around what I suppose you could call the formality of music. It allows for control over tempo, pitch mapping, and instrumentation. As an extra blessing or curse, the site had so many different options that one could really engross themselves. There are already hundreds of premade musical scales, instruments, and instrument roles. It’s crazy.
WolframTones is powered by Wolfram Automata. Basically, there’s a square that’s either black or white and it’ll gontinue to grow based on a specific rule, generating complexity. There are 256 rules, and Stephen Wolfram’s experiment went through all those rules. Hopefully this image explains it a little better.
WolframTones takes these rules and flips them sideways, and uses them as notes.
The above video is an example of someone using the website and their generated music.
// Kevin Thies
// Section C
// kthies@andrew.cmu.edu
// Project-11- Turtle Freestyle
var ttl; // turtle
function setup() {
// style
createCanvas(480, 480);
background(4, 20, 45);
// make the turtle with starting values
ttl = makeTurtle(width/2, height/2);
ttl.penDown();
ttl.setWeight(6);
ttl.setColor("yellow");
ttl.face(0);
}
function draw() {
background(4, 20, 45, 20);
// firefly
// move
ttl.forward(5);
// rotate
// random, flying-insect-like movement
ttl.right(map(noise(ttl.x, ttl.y), 0, 1, -180, 180));
// if mouse over jar, make it move towards mouse
if(mouseX < 360 & mouseX > 120 && mouseY > 90 && mouseY < 390) {
ttl.turnToward(mouseX, mouseY, 40);
}
// if near jar wall,
if (ttl.x > 350 || ttl.x < 130 || ttl.y > 380 || ttl.y < 100) {
// turn towards center
ttl.turnToward(width/2, height/2, 180);
ttl.forward(10);
// teleport back into jar
if (ttl.x > 350) {
ttl.goto(350, ttl.y);
print("turtle too far right");
} else if (ttl.x < 130) {
ttl.goto(130, ttl.y);
print("turtle too far left");
} else if (ttl.y > 380) {
ttl.goto(ttl.x, 380);
print("turtle too far down");
} else if (ttl.y < 85) {
ttl.goto(ttl.x, 100);
print("turtle too far up");
}
}
// firefly light
strokeWeight(0);
fill(255, 255, 0, 40);
ellipse(ttl.x, ttl.y, 40, 40);
// jar (dimensions are x 120 to 360 y 90 to 390)
fill(0, 0, 0, 0);
rectMode(CENTER);
stroke(255);
strokeWeight(2);
rect(width/2, height/2, 240, 300, 20);
fill(173, 151,64);
strokeWeight(0);
rect(width/2, 73, 240, 30, 20);
}
//////////////////////////////////////////////////////////////////////////
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) {
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};
return turtle;
}
I’ve done projects making “flying bugs in a jar” before, and I decided it would be a good time to try and make one with turtles. Getting the turtle to stay inside the jar was more challenging that I expected, mostly because of the way I made the turtle move. However, it responds to the cursor while it’s inside the jar. I suppose the way to expand on this project would to have multiple fireflies in the jar that have some level of collision so that they could act like a swarm.
I would add screenshots but they’d look just like the program above.
LA HYBRIDScope CITY from Filipa Valente on Vimeo.
limiLAB is run by Filipa Valente, a Portugese architect and interactive artist based in Los Angeles. She did her undergrad and Masters in Architecture at the Bartlett School of Architecture in London, and her Masters in Media Art and Architecture MEDIASCAPES at SciArc. Her work explores space, and leverages light and sound.
Of her works, I found the Hybridscope City to be the most evocative. The project was sited at LAX airport, and passersby getting off the plane could cycle through the different real-time sights and sounds of the city. The form itself is also like an organic web, or specifically filleted voronori meshes. The projection mapping and skeleton tracking were powered by a kinect and a custom max/MSP/Jitter script.
While it is interactive, it doesn’t look intuitive. In the video, people really have to reach out, and it’s said that it needs a person to calibrate first. Plus, it doesn’t look obviously interactive. Since the images are projected downwards, a person’s shadow would only get in the way of the projector, which I think would make people less likely to interact with the piece.
]]>// Kevin Thies
// Section C
// kthies@andrew.cmu.edu
// Project 10 - Generative Landscape
// initiate containers for mountains, trees, and posts
var mountains = [];
var trees = [];
var posts = [];
// keeps track of time for post spawning
var timer = 0;
// preset y values to act as "layers"
var layer = [470, 435, 400, 380, 320, 290];
function setup() {
createCanvas(480, 480);
// add starting mountains
for(var i = 0; i < 15; i++) {
var randomX = random(width);
mountains[i] = makeMountain(randomX, 5);
}
// add starting trees
for(var i = 0; i < 100; i++) {
var randomX = random(width);
trees[i] = makeTree(randomX, 5);
}
// add closer starting trees with different speed and color
for(var i = 100; i < 200; i++) {
var randomX = random(width);
var newTree = makeTree(randomX, 4);
newTree.color = color(10,0,29);
newTree.speed = 0.9;
trees[i] = newTree;
}
}
function draw() {
// background
background(12,24,68);
strokeWeight(3);
backgroundGradient();
strokeWeight(0);
// sun
strokeWeight(0);
fill("lemonchiffon");
ellipse(width / 2, layer[5], 100, 100);
// ground
fill(12,24,68);
rectMode(CORNERS);
rect(0, layer[5], width, height);
fill(10,0,29);
rect(0, layer[4], width, height);
// mountains
strokeWeight(0);
updateAndDisplayMountains();
removeOffscreenMountains();
addNewMountains();
// trees at base of mountain
updateAndDisplayTrees();
removeOffscreenTrees();
addNewTrees();
addNewCloserTrees();
// highway railing
// posts
updateAndDisplayPosts();
removeOffscreenPosts();
addNewPosts();
// solid barrier part of the rail
fill(40,37,31);
rect(0, layer[2], width, layer[0]);
stroke(98,87,93);
strokeWeight(3);
line(0, layer[2], width, layer[2]);
strokeWeight(0);
rectMode(CENTER);
fill(24,12,14);
rect(width/2, layer[1], width, 40)
}
// POSTS //////////////////////////////////////////////////////////////////
// moves the existing posts
function updateAndDisplayPosts() {
for(var i = 0; i < posts.length; i++) {
posts[i].move();
posts[i].display();
}
}
// removes the posts that are offscreen
function removeOffscreenPosts() {
// if posts are offscreen, don't re-add them to posts[]
var postsToKeep = [];
for(var i = 0; i < posts.length; i++) {
if (posts[i].x + 40 > 0) {
postsToKeep.push(posts[i]);
}
}
posts = postsToKeep;
}
// adds the new posts
function addNewPosts() {
// every 40 frames, shoot in a new post
timer ++;
if(timer == 40) {
timer = 0;
posts.push(makePost(width))
}
}
// how posts are made
function makePost(px) {
var post = {x: px,
speed: 12,
display: postDisplay,
color: color(98,87,93),
color2: color(40,37,31),
move: postMove,
}
return post;
}
// is how to move the posts
function postMove() {
this.x -= this.speed;
}
// builds the posts from the this.x coordinate
function postDisplay() {
stroke(this.color);
strokeWeight(3);
// | part of post
line(this.x, height, this.x, layer[2] - 20);
// / / part of post
line(this.x, layer[2] - 20, this.x - 10, layer[2] - 10);
line(this.x - 20, layer[2] - 20, this.x - 30, layer[2] - 10);
// - part of post
line(this.x - 5, layer[2] - 15, this.x - 25, layer[2] - 15);
strokeWeight(0);
}
// TREES //////////////////////////////////////////////////////////////////
// how trees are made
function makeTree(tx, layer) {
var tree = {x: tx,
layer: layer,
speed: 0.7,
display: treeDisplay,
color: color(12,24,68),
move: treeMove,
scale: random(0.5, 1)};
return tree;
}
// how the trees are moved
function treeMove() {
this.x -= this.speed;
}
// builds the far trees from an x coordinate
function treeDisplay() {
fill(this.color);
ellipse(this.x, layer[this.layer], 20 * this.scale, 20 * this.scale);
}
// moves existing trees
function updateAndDisplayTrees() {
for(var i = 0; i < trees.length; i++) {
trees[i].move();
trees[i].display();
}
}
// removes trees that are offscreen
function removeOffscreenTrees() {
// if tree is offscreen, don't re-add it to trees[]
var treesToKeep = [];
for(var i = 0; i < trees.length; i++) {
if (trees[i].x + 20 > 0) {
treesToKeep.push(trees[i]);
}
}
trees = treesToKeep;
}
// random chance generates a new tree on layer 5
function addNewTrees() {
var newTreeChance = 0.1;
if(random(0,1) < newTreeChance) {
trees.push(makeTree(width, 5))
}
}
// random chance generates a new tree on layer 4 with fitting color and speed
function addNewCloserTrees() {
var newTreeChance = 0.1;
if(random(0,1) < newTreeChance) {
var newTree = makeTree(width, 4);
newTree.color = color(10,0,29);
newTree.speed = 0.9;
trees.push(newTree);
}
}
// BACKGROUND //////////////////////////////////////////////////////////////
// makes the gradient seen in the background from y = 0 to layer 5
function backgroundGradient() {
for(var i = 0; i < layer[5]; i++) {
var g = map(i, 0, layer[5], 0, 140);
stroke(255, g, 0);
line(0, i, width, i);
}
}
// MOUNTAINS //////////////////////////////////////////////////////////////
// moves existing mountains (so strong)
function updateAndDisplayMountains() {
for(var i = 0; i < mountains.length; i++) {
mountains[i].move();
mountains[i].display();
}
}
// removes mountains that are offscreen
function removeOffscreenMountains() {
// if mountain is offscreen, don't re-add it to mountains[]
var mountainsToKeep = [];
for(var i = 0; i < mountains.length; i++) {
if (mountains[i].x + (200 * mountains[i].scale) > 0) {
mountainsToKeep.push(mountains[i]);
}
}
mountains = mountainsToKeep;
}
// random chance generates a new mountain
function addNewMountains() {
var newMountainChance = 0.01;
if(random(0,1) < newMountainChance) {
mountains.push(makeMountain(width, 5))
}
}
// how mountains are made
function makeMountain(mx, layer) {
var mountain = {x: mx,
layer: layer,
speed: 0.5,
display: mountainDisplay,
color: color(85,54,86),
color2: color(85, 54, 106),
move: mountainMove,
scale: random(0.5, 1)};
return mountain;
}
// how mountains are moved
function mountainMove() {
this.x -= this.speed;
}
// actually builds the moutain based off an x coordinate
function mountainDisplay() {
fill(this.color);
triangle(this.x, layer[this.layer],
this.x + (200 * this.scale), layer[this.layer],
this.x + (100 * this.scale), layer[this.layer] - (180 * this.scale));
fill(this.color2);
triangle(this.x + (170 * this.scale), layer[this.layer],
this.x + (200 * this.scale), layer[this.layer],
this.x + (100 * this.scale), layer[this.layer] - (180 * this.scale));
}
// Congradulations, you made it to the end! *confetti emoji*
At first when I was thinking about what to do, I thought about long car rides through the mountains. I sketched up an idea that involved cars going different directions, but after implementing the moving mountains, I realized I had to simplify my idea. I kept the most important part of that, the mountains, and rebuilt from there. I think having all the layers helps add some measurable scale to the mountains and trees in the background. I’m also just really with how it turned out, and although making the objects got tedious, once I had one and understood how the pieces fit together, it really sped up from there.
Kthies sketch
So originally, I was planning on doing just a greyscale image that used the white space to display grayscale, but my friend Raven (pictured) has been working on a project related to superheros, which made me think of comics and how those were printed early on, so I expanded the project scope to take an image and break it down into simulated CMYK values (it’s just RGB in the end because screens) with each of those being slightly offset from the analyzed pixel. I think it turned out really well and makes cool-looking images!
// Kevin Thies
// Section C
// kthies@andrew.cmu.edu
// Project 09 - Computational Portrait
var portrait; // the image
var pixels = []; // holds the pixels
var step = 6; // distance between "pixels"
var radiusV; // radius of value circles
var radiusR; // radius of red circles
var radiusG; // radius of green circles
var radiusB; // radius of blue circles
var rgb = []; // holds RGBA values of pixels
// preload image
function preload() {
var myImageURL = "https://i.imgur.com/sveeELP.jpg";
portrait = loadImage(myImageURL);
}
function setup() {
createCanvas(480, 520);
background(255);
portrait.loadPixels();
noLoop();
noStroke();
}
function draw() {
// get a grid of positions one step apart
for(var x = 0; x < width; x += step) {
for(var y = 0; y < height; y += step) {
// value ===============================================
// kept darker than the color values to just balance it out
// RGB is structured the same way, just not using brightness but direct
// R, G, and B values
// sets the radius of the circle, remapping the value of color/brightness
// to a circle between 0 and step * 1.3 in diameter
// with step > 1, it means the colors can overlap, hiding the white background
radiusV = map( brightness( portrait.get(x * 2, y * 2) ),
255, 0,
0, step * 1.5 );
// sets the fill color with transparency
fill(0, 0, 0, 80);
// draws the ellipse at the point
ellipse(x, y, radiusV, radiusV);
// RGB ==================================================
// rgb takes on the [R, G, B, A] values as an array
rgb = portrait.get(x * 2, y * 2);
print(rgb[0] + " " + rgb[1] + " " + rgb[2] + " " + rgb[3]);
// by filling the color with 255 of not that color, when stacked up the
// colors match - ex. red = 0,255,255 g = 255,0,255 b = 255, 255, 0
// basically I translated RGB to CMY
// red / cyan
radiusR = map(rgb[0],
255, 0,
0, step * 1.3);
fill(0, 255, 255, 80);
ellipse(x + step / 5, y, radiusR, radiusR);
// green / magenta
radiusG = map(rgb[1],
255, 0,
0, step * 1.3);
fill(255,0,255, 80);
ellipse(x - step / 5, y, radiusG, radiusG);
// blue / yellow
radiusB = map(rgb[2],
255, 0,
0, step * 1.3);
fill(255,255,0,80);
ellipse(x, y - step / 5, radiusB, radiusB);
}
}
}
]]>
Minimaforms is an experimental architectural design practice founded in 2002, the vision of two brothers, Theodore and Stephen Spyropoulos. Theodore is an architect, and Stephen is an artist and interactive designer. Their work is synergistic, and look at possible futures and designing projects that act as prototypes. They’re interested in looking at communication and the way information is deconstructed and connected.
Their work is quite refined. The more pure architecture projects not display parametricism, but specifically resemble communicating nodes, or as they say, an agent based system with an emphasis on how they communicate over time. One of their projects that I particularly admire is titled Archigram on their website, and is an evolution on a 1966 Archigram project “Living Pod“, which is a proposal for a trailer home-like dwelling that can move and connect to other units to create larger structures. I think the project is impressive both on the conceptual level, but especially that they brought it all into a large physical model. Additionally, they took the idea of the pod and literrally evolved it, developing spines and tails to connect, which does fit with the idea of a time-based development.