diff --git a/config.toml b/config.toml index 5b66e35a..49306d93 100644 --- a/config.toml +++ b/config.toml @@ -9,7 +9,7 @@ home = ["HTML", "RSS", "JSON"] [params] themeVariant = "mcpt" -custom_js_urls = ['/js/p5.min.js', '/js/p5.sound.min.js'] +custom_js_urls = ['/js/p5.min.js'] custom_css = ['/css/p5js.css', '/css/sidebar.css'] [markup.goldmark.renderer] diff --git a/content/game-dev/part-i/introduction.md b/content/game-dev/part-i/introduction.md index b09502eb..52a35daf 100644 --- a/content/game-dev/part-i/introduction.md +++ b/content/game-dev/part-i/introduction.md @@ -9,504 +9,7 @@ weight = 1 Welcome to the return of MCPT's Game Dev Series! Over the course of 3 workshops, we have something for everyone, whether you’re a beginner or an experienced coder. Inspired by **Bloons Tower Defense**, you will learn how to code your very own tower-defense game in Processing! ### Demo - -{{< p5js >}} - - -// Program main method -function setup() { -initializeFields(); -createCanvas(800, 500); -loadHeartIcon(); -initDragAndDrop(); -initPath(); -createFirstWave(); -} -let started; -let frames = 0; -function draw() { -frames++; -background(color(0xad, 0xd5, 0x58)); - - if(!started) { - const sz = 40 + Math.sin(frames / 15) * 1; - textSize(sz); - textAlign(CENTER, CENTER) - - rectMode(CENTER); - noStroke(); - fill(color(0xed, 0xd7, 0x60)); - rect(400, 250, sz * 9, sz * 2, 50); - fill(color(0xfd, 0xe7, 0x70)); - rect(400, 250, sz * 9 - 10, sz * 2 - 10, 50); - fill(color(0x4C, 0x67, 0x10)); - text("click to start! ", 408, 255); -stroke(color(0x4C, 0x67, 0x10)); - strokeWeight(5); - noFill(); - rectMode(CORNER); - rect(0, 0, 800, 500) - return; - } - textAlign(LEFT, BASELINE) - drawPath(); - // Draw all the towers that have been placed down before - drawAllTowers(); - drawTrash(); - drawSelectedTowers(); - dragAndDropInstructions(); - drawBalloons(); - drawHealthBar(); - stroke(color(0x4C, 0x67, 0x10)); -strokeWeight(5); -noFill(); -rectMode(CORNER); -rect(0, 0, 800, 500) -} - -// Whenever the user drags the mouse, update the x and y values of the tower -function mouseDragged() { -if (within) { -// Check to see if the user is currently dragging a tower -// Set the values while accounting for the offset -x = mouseX + difX; -y = mouseY + difY; -} -} - -// Whenever the user initially presses down on the mouse -function mousePressed() { -if (mouseX < 0 || mouseX > 800 || mouseY < 0 || mouseY > 500) return; -if(!started) started = true; -// Check to see if the pointer is within the bounds of the tower -within = withinBounds(); -if (within) { -// The tower has been "picked up" -handlePickUp(); -// Calculate the offset values (the mouse pointer may not be in the direct centre of the tower) -difX = x - mouseX; -difY = y - mouseY; -} -} - -// Whenever the user releases their mouse -function mouseReleased() { -if (within) { -// If the user was holding the tower in the previous frame, the tower has just been dropped -// Call the method to handle the drop and check for drop validity -handleDrop(); -} -// The mouse is no longer holding the tower -within = false; -} - -var balloons; - -var distanceTravelled, delay, speed; - -/* -Encompasses: Displaying Balloons, Waves & Sending Balloons, Balloon Reaching End of Path -*/ -function createFirstWave() { -// {Number of "steps" taken, frames of delay before first step, speed} -balloons.push( [ 0, 100, 3 ]); -balloons.push( [ 0, 130, 3 ]); -balloons.push( [ 0, 160, 2 ]); -balloons.push( [ 0, 220, 4 ]); -balloons.push( [ 0, 340, 2 ]); -balloons.push( [ 0, 370, 2 ]); -balloons.push( [ 0, 400, 5 ]); -balloons.push( [ 0, 430, 5 ]); -balloons.push( [ 0, 490, 3 ]); -balloons.push( [ 0, 520, 1 ]); -balloons.push( [ 0, 550, 3 ]); -} - -// Displays and moves balloons -function updatePositions(balloon) { -// Only when balloonProps[1] is 0 (the delay) will the balloons start moving. -if (balloon[delay] == 0) { -// Radius of the balloon -var RADIUS = 25; -var position = getLocation(balloon[distanceTravelled]); -// Increases the balloon's total steps by the speed -balloon[distanceTravelled] += balloon[speed]; -// Drawing of ballon -ellipseMode(CENTER); -strokeWeight(0); -stroke(0); -fill(color(0xf3, 0xcd, 0x64)); -ellipse(position.x, position.y, RADIUS, RADIUS); -} else { -balloon[delay]--; -} -} - -function drawBalloons() { -for (var i = 0; i < balloons.length; i++) { -var balloon = balloons[i]; -updatePositions(balloon); -if (atEndOfPath(balloon[distanceTravelled])) { -// Removing the balloon from the list -balloons.splice(i, 1); -// Lost a life. -health--; -// Must decrease this counter variable, since the "next" balloon would be skipped -i--; -// When you remove a balloon from the list, all the indexes of the balloons "higher-up" in the list will decrement by 1 -} -} -} - -// Similar code to distance along path -function atEndOfPath(travelDistance) { -var totalPathLength = 0; -for (var i = 0; i < points.length - 1; i++) { -var currentPoint = points[i]; -var nextPoint = points[i + 1]; -var distance = dist(currentPoint.x, currentPoint.y, nextPoint.x, nextPoint.y); -totalPathLength += distance; -} -// This means the total distance travelled is enough to reach the end -if (travelDistance >= totalPathLength) -return true; -return false; -} - -// variable to track user's health -var health; - -var heart; - -// ------- HP SYSTEM -------- -/* -Heath-related variables: -int health: The player's total health. -This number decreases if balloons pass the end of the path (offscreen), currentely 12 since there are 12 balloons. -boolean[] offscreen: this array tracks if the balloon has been subtracted from health once it is off the screen. -PImage heart: the heart icon to display with the healthbar. -*/ -function loadHeartIcon() { -// done in preLoad(); -} - -// method to draw a healthbar at the top right of the screen -function drawHealthBar() { -// draw healthbar outline -stroke(0, 0, 0); -strokeWeight(0); -fill(color(0x83, 0x00, 0x00)); -rect(715, 455, 120, 20); -// draw healthbar -noStroke(); -rectMode(CORNER); -fill(color(0xFF, 0x31, 0x31)); -// the healthbar that changes based on hp -rect(655, 445.5, health * 12, 20); -rectMode(CENTER); -noFill(); -// write text -stroke(0, 0, 0); -textSize(14); -fill(255, 255, 255); -text("Health: " + health, 670, 462); -// put the heart.png image on screen -noFill(); -} - -// The points on the path, in order. -var points; - -var PATH_RADIUS; - -/* -Encompasses: The Path for Balloons, Balloon Movement -*/ -// ------- CODE FOR THE PATH -function addPointToPath(x, y) { -points.push(new p5.Vector(x, y)); -} - -function initPath() { -addPointToPath(0, 200); -addPointToPath(50, 200); -addPointToPath(200, 150); -addPointToPath(350, 200); -addPointToPath(500, 150); -addPointToPath(650, 200); -addPointToPath(650, 300); -addPointToPath(500, 250); -addPointToPath(350, 300); -addPointToPath(200, 250); -addPointToPath(50, 300); -addPointToPath(50, 400); -addPointToPath(200, 350); -addPointToPath(350, 400); -addPointToPath(500, 350); -addPointToPath(650, 400); -addPointToPath(800, 350); -} - -function drawPath() { -stroke(color(0x4C, 0x67, 0x10)); -strokeWeight(PATH_RADIUS * 2 + 1); -for (var i = 0; i < points.length - 1; i++) { -var currentPoint = points[i]; -var nextPoint = points[i + 1]; -line(currentPoint.x, currentPoint.y, nextPoint.x, nextPoint.y); -} -stroke(color(0x7b, 0x9d, 0x32)); -strokeWeight(PATH_RADIUS * 2); -for (var i = 0; i < points.length - 1; i++) { -var currentPoint = points[i]; -var nextPoint = points[i + 1]; -line(currentPoint.x, currentPoint.y, nextPoint.x, nextPoint.y); -} -} - -// GIVEN TO PARTICIPANTS BY DEFAULT -function getLocation(travelDistance) { -for (var i = 0; i < points.length - 1; i++) { -var currentPoint = points[i]; -var nextPoint = points[i + 1]; -var distance = dist(currentPoint.x, currentPoint.y, nextPoint.x, nextPoint.y); -if (distance <= 0.00000001 || travelDistance >= distance) { -travelDistance -= distance; -} else { -// In between two points -var travelProgress = travelDistance / distance; -var xDist = nextPoint.x - currentPoint.x; -var yDist = nextPoint.y - currentPoint.y; -var x = currentPoint.x + xDist * travelProgress; -var y = currentPoint.y + yDist * travelProgress; -return new p5.Vector(x, y); -} -} -// At end of path -return points[points.length - 1]; -} - -var x, y, difX, difY, count; - -// Towers that are placed down -var towers; - -// If mouse was held down during the previous frame -var within; - -var towerSize; - -var towerColour; - -// these variables are the trash bin coordinates -var trashX1, trashY1, trashX2, trashY2; - -/* -Encompasses: Displaying Towers, Drag & Drop, Discarding Towers, Rotating Towers, Tower Validity Checking -*/ -// -------- CODE FOR DRAG & DROP ---------------------- -function initDragAndDrop() { -x = 650; -y = 50; -within = false; -difX = 0; -difY = 0; -trashX1 = 525; -trashY1 = 30; -trashX2 = 775; -trashY2 = 120; -count = 0; -towers = []; -} - -// Use point to rectangle collision detection to check for mouse being within bounds of pick-up box -function pointRectCollision(x1, y1, x2, y2, size) { -// --X Distance-- --Y Distance-- -return (abs(x2 - x1) <= size / 2) && (abs(y2 - y1) <= size / 2); -} - -// Check to see if mouse pointer is within the boundaries of the tower -function withinBounds() { -return pointRectCollision(mouseX, mouseY, x, y, towerSize); -} - -// check if you drop in trash -function trashDrop() { -if (x >= trashX1 && x <= trashX2 && y >= trashY1 && y <= trashY2) { -return true; -} -return false; -} - -// -------Methods Used for further interaction------- -function handleDrop() { -// Instructions to check for valid drop area will go here -if (trashDrop()) { -x = 650; -y = 50; -print("Dropped object in trash."); -} else if (legalDrop()) { -towers.push(new p5.Vector(x, y)); -// Add the tower to the list of placed down towers -x = 650; -y = 50; -print("Dropped for the " + (++count) + "th time."); -} -} - -// Will be called whenever a tower is picked up -function handlePickUp() { -print("Object picked up."); -} - -// -------------------------------------------------- -// Draw a simple tower at a specified location -function drawTowerIcon(xPos, yPos, colour) { -strokeWeight(0); -stroke(0); -fill(colour); -rectMode(CENTER); -// Draw a simple rectangle as the tower -rect(xPos, yPos, towerSize, towerSize); -} - -// Draws a tower that rotates to face the targetLocation -function drawTowerIcon(xPos, yPos, colour, targetLocation=null) { -if (targetLocation === null) { -strokeWeight(0); -stroke(0); -fill(colour); -rectMode(CENTER); -// Draw a simple rectangle as the tower -rect(xPos, yPos, towerSize, towerSize); -return; -} -strokeWeight(5); -stroke(color(0x4C, 0x67, 0x10)); -line(xPos, yPos, targetLocation.x, targetLocation.y); -push(); -translate(xPos, yPos); -// Angle calculation -var slope = (targetLocation.y - yPos) / (targetLocation.x - xPos); -var angle = atan(slope); -rotate(angle); -strokeWeight(0); -fill(colour); -rectMode(CENTER); -// Draw a simple rectangle as the tower -rect(0, 0, towerSize, towerSize); -pop(); -} - -function drawAllTowers() { -for (var i = 0; i < towers.length; i++) { -var xPos = towers[i].x, yPos = towers[i].y; -// Towers will track the mouse as a placeholder -drawTowerIcon(xPos, yPos, towerColour, new p5.Vector(mouseX, mouseY)); -fill(color(0x4C, 0x67, 0x10)); -strokeWeight(0); -textSize(12); -text("Tower " + (i + 1), xPos - 30, yPos - 20); -} -} - -function drawSelectedTowers() { -// Changing the color if it is an illegal drop to red -if (!legalDrop()) { -// Draw the current tower (that the user is holding) as red to indicate illegal -drawTowerIcon(x, y, color(0xFF, 0x00, 0x00)); -} else { -// Draw the current tower (that the user is holding) -drawTowerIcon(x, y, towerColour); -} -// Draw the pick-up tower on the top right -drawTowerIcon(650, 50, towerColour); -} - -function drawTrash() { -rectMode(CORNERS); -noStroke(); -fill(color(0x4C, 0x67, 0x10)); -rect(trashX1, trashY1, trashX2, trashY2); -fill(255, 255, 255); -stroke(255, 255, 255); -} - -function dragAndDropInstructions() { -fill(color(0x4C, 0x67, 0x10)); -textSize(12); -text("Pick up tower from here!", 620, 20); -text("You can't place towers on the path of the balloons!", 200, 20); -text("Place a tower into the surrounding area to put it in the trash.", 200, 40); -text("Mouse X: " + mouseX + "\nMouse Y: " + mouseY + "\nMouse held: " + mouseIsPressed + "\nWithin object bounds: " + within, 15, 20); -} - -// -------- CODE FOR PATH COLLISION DETECTION --------- -function pointDistToLine(start, end, point) { -// Code from https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment -// i.e. |w-v|^2 - avoid a sqrt -var l2 = (start.x - end.x) * (start.x - end.x) + (start.y - end.y) * (start.y - end.y); -// v == w case -if (l2 == 0.0) -return dist(end.x, end.y, point.x, point.y); -var t = max(0, min(1, p5.Vector.sub(point, start).dot(p5.Vector.sub(end, start)) / l2)); -// Projection falls on the segment -var projection = p5.Vector.add(start, p5.Vector.mult(p5.Vector.sub(end, start), t)); -return dist(point.x, point.y, projection.x, projection.y); -} - -function shortestDist(point) { -var answer = Number.MAX_VALUE; -for (var i = 0; i < points.length - 1; i++) { -var start = points[i]; -var end = points[i + 1]; -var distance = pointDistToLine(start, end, point); -answer = min(answer, distance); -} -return answer; -} - -// Will return if a drop is legal by looking at the shortance distance between the rectangle center and the path. -function legalDrop() { -// checking if this tower overlaps any of the already placed towers -for (var i = 0; i < towers.length; i++) { -var towerLocation = towers[i]; -if (pointRectCollision(x, y, towerLocation.x, towerLocation.y, towerSize)) -return false; -} -return shortestDist(new p5.Vector(x, y)) > PATH_RADIUS; -} - -function initializeFields() { -balloons = []; -distanceTravelled = 0; -delay = 1; -speed = 2; -health = 11; -points = []; -PATH_RADIUS = 20; -x = 0; -y = 0; -difX = 0; -difY = 0; -count = 0; -towers = null; -within = false; -towerSize = 25; -towerColour = color(0x7b, 0x9d, 0x32); -trashX1 = 0; -trashY1 = 0; -trashX2 = 0; -trashY2 = 0; -started = false; -} - -function preload() { -// TODO: put method calls that load from files into this method -// I found the following calls that you should move here: -// (note that line numbers are from your Processing code) -} -{{< /p5js >}} +{{< p5js-src "/content/p5-js-demo/part1.js" >}} {{% expand "What is a tower-defense game?" %}} {{% notice info %}} diff --git a/content/game-dev/part-ii/AdvancedTracking.files/Part2_AdvancedTracking.zip b/content/game-dev/part-ii/AdvancedTracking.files/Part2_AdvancedTracking.zip new file mode 100644 index 00000000..2b3b981c Binary files /dev/null and b/content/game-dev/part-ii/AdvancedTracking.files/Part2_AdvancedTracking.zip differ diff --git a/content/game-dev/part-ii/AdvancedTracking.md b/content/game-dev/part-ii/AdvancedTracking.md new file mode 100644 index 00000000..aa04fdb9 --- /dev/null +++ b/content/game-dev/part-ii/AdvancedTracking.md @@ -0,0 +1,69 @@ ++++ +title = "Advanced Tracking" +weight = 3 ++++ + +--- + +{{%attachments style="green" title="Part2_AdvancedTracking" pattern=".*zip" /%}} + +### What You'll Learn + +Last session we implemented a function that rotated a tower to face a balloon based on the slope of the line made with a balloon and tower. Today we will implement a more representative procedure, which will make a tower face a balloon that is within hit radius, and is also the farthest along the path. + +##### Key Concepts + +1. Using a function that processes inputs to create an output +2. Implementing tower range +3. Implementing a filtering mechanism that will get the right balloon + +Remember that in Processing, Y values increase as you move _down the window_. Therefore, the origin (0,0) is at the top-left corner of the window. + +### Logic for the Procedure + +Every function has an input, a process, and a resulting output. The function we are making today is no different. First, let’s consider inputs. Since this function is trying to find a balloon within a tower’s hit radius, we will need the tower’s coordinates, the tower's hit-radius, and a list (array) of all the balloons to scan. The output is pretty straightforward; just the coordinates of the balloon we want. With this in mind, our function looks something like this. + +![Interface](/img/Function.png) + +Now we need to come up with the code that will process these inputs and spit out the correct balloon location. Remember, this function must 1. Find the balloons in range, and 2. Find the one in this group that is farthest along the path. In other words, we have to come up with a filtering mechanism that can be applied to each balloon and give the one that satisfies these requirements. Knowing this now, we can start coding our function. + +```java +PVector track(PVector towerLocation, int vision, ArrayList){ + PVector location = null; + Cycle through all balloons{ + if (distance between tower coordinates & balloon coordinates <= vision){ + if (the balloon is, so far, the farthest along one we have seen){ + location = balloonLocation; + } + } + } + return location; +} +``` + +##### Implementation! + +Lets translate this into real processing code with our program’s global variables and methods, as well as a few extra variables to keep track of the furthest distance travelled across the balloons we have seen. + +```java +PVector track(PVector towerLocation, int vision, ArrayList){ + int maxDist = 0; + PVector location = null; + for (float[] balloon: balloons){ + PVector balloonLocation = getLocation(balloon[distanceTravelled]); + if (dist(balloonLocation.x, balloonLocation.y,towerLocation.x,towerLocation.y) <= vision){ + if (balloon[distanceTravelled] > maxDist){ + location = balloonLocation; + maxDist = balloon[distanceTravelled]; + } + } + } + return location; +} +``` + +The first new addition we’ll look at is the 4th line in the function that uses the getLocation() function. All balloons can be represented by an array (or list) of numeric values that describe its properties, and one index (or position in the list) stores the journey length of each balloon. You can think of it as storing the number of steps the balloon has taken on the path. However, we need a way to translate this number of steps to actual coordinates. This is done by simply using the getLocation() function. + +We also added the maxDist variable. This is a dynamic/changing value that will update once a new balloon is found to have a greater journey length than previous balloons. When a balloon like this is found, the journey length of it (which is essentially measured in the number of “steps” the balloon has made) is stored in this variable, and “location” has this balloon’s location assigned to it. Because maxDist starts at 0, the first balloon to pass the first filter will have its journey length/steps assigned to the maxDist variable. The 2nd, 3rd, 4th, ect. a balloon must then have a longer journey length to pass through the 2nd “if” to store its location data into the location variable. In other words, the next balloon must "beat" the previous balloon in terms of journey length. If any of them do, their distance travelled will then be assigned to maxDist. The result is a competition-inducing piece of code that will always end up storing the balloon with the longest path, which is exactly what we want. + +And there you go! Now that we have determined the balloon that the tower should point at, we can run it through the tower rotation function we created last session. Now, towers will always point to the balloons within their hit radius that are farthest along the path. diff --git a/content/game-dev/part-ii/Currency.files/Currency_Template.zip b/content/game-dev/part-ii/Currency.files/Currency_Template.zip new file mode 100644 index 00000000..bd088ac5 Binary files /dev/null and b/content/game-dev/part-ii/Currency.files/Currency_Template.zip differ diff --git a/content/game-dev/part-ii/Currency.md b/content/game-dev/part-ii/Currency.md new file mode 100644 index 00000000..f980f7ed --- /dev/null +++ b/content/game-dev/part-ii/Currency.md @@ -0,0 +1,148 @@ ++++ +title = "Currency System" +weight = 3 ++++ + +--- + +{{%attachments style="green" title="Currency Template Code" /%}} + +### Reward for Balloon Pop + +Since we have a working system to send balloons, the next step is to reward the user when they pop balloons. + +{{% expand "See code" "false" %}} +```java +void drawBalloons() { + for (int i = 0; i < balloons.size(); i++) { + float[] balloon = balloons.get(i); + updatePositions(balloon); + if (balloon[hp] <= 0) { + handleBalloonPop(); // the balloon has been popped by a tower + + balloons.remove(i); + i--; + continue; + } + if (atEndOfPath(balloon[distanceTravelled])) { + balloons.remove(i); // Removing the balloon from the list + health--; // Lost a life. + i--; // Must decrease this counter variable, since the "next" balloon would be skipped + // When you remove a balloon from the list, all the indexes of the balloons "higher-up" in the list will decrement by 1 + } + } +} +``` +{{% /expand %}} + +In Balloons.pde, we check if balloons have positive hp when they are drawn in drawBalloon(). Here, we have an if statement that checks if `balloon[hp] <= 0`, which means that the balloon has been popped (it has less than zero hp). We can add our method handleBalloonPop(), which will add money to the user’s balance once they pop a balloon. Here are the two methods `handleBalloonPop()` and `increaseBalance()` in Currency.pde. + +{{% expand "See code" "false" %}} +```java +void handleBalloonPop() { + // Reward the player for popping the balloon + increaseBalance(rewardPerBalloon); +} + + +void increaseBalance(int amount) { + currentBalance += amount; // Increase the current balance by the amount given +} +``` +{{% /expand %}} + +The second method, `increaseBalance()` takes a parameter which is the amount that is to be added to the user’s balance. For popping balloons, the user gets $20, which is set in the global variables section, `rewardPerBalloon.` + +{{% expand "See code" "false" %}} +```java +// Current amount of money owned by the player +int currentBalance = 500; // Give the user $500 of starting balance +final int rewardPerBalloon = 20; // Money earned by popping a balloon +final int towerPrice = 100; // Price to purchase a single tower +``` +{{% /expand %}} + +For `handleBalloonPop()`, this method is called whenever a balloon is popped, so we just call `increaseBalance()` with the `rewardPerBalloon` in order to reward the user. + +### Purchasing Towers +All the towers now have a price! Whenever the user picks up a tower, they will need to purchase it using their balance. Since the `handlePickUp()` method is called whenever the user picks up a tower, we will use it to charge the player when purchasing a tower and prevent them from picking up towers without sufficient money. When purchasing a tower, we first need to check if the player has enough money to purchase the tower. This can be done by comparing the player's current balance to the cost of the tower. The costs of the towers are stored in the towerPrice array. + +{{% expand "See code for checking funds" "false" %}} +```java +/** Checks to see if there is sufficient balance for purchasing a certain item + * Parameter "cost" is the cost of the item to be purchased + */ +boolean hasSufficientFunds(int cost) { + if (currentBalance < cost) { + return false; // Not enough money to purchase the tower + } + else { + return true; // Enough money to purchase the tower + } +} +``` +{{% /expand %}} + +If the balance is greater than or equal to the cost of the tower, then allow the user to pick up the tower. The cost of that tower will be deducted from the player's balance once they drop the tower onto a valid location (not in the trash). + +{{% expand "See code for purchasing tower" "false" %}} +```java +/** Purchases a tower + * Parameter "cost" is the cost of the tower to be purchased + */ +void purchaseTower(int cost) { + currentBalance -= cost; +} +``` +{{% /expand %}} + +If the balance is less than the cost of the tower, we will not allow the tower to be picked up. + +{{% expand "See code for pick-up handling" "false" %}} +```java +// Will be called whenever a tower is picked up +void handlePickUp(int pickedUpTowerID) { + // Only if there is sufficient money to purchase the tower... + if (withinBounds(pickedUpTowerID) && hasSufficientFunds(towerPrice[pickedUpTowerID])) { + // Pick up the tower + } +} +``` +{{% /expand %}} + +### Insufficient Funds Warnings +To make it easier for the player, we will warn them whenever they try to purchase a tower that they don't have enough money for. To check if this is happening, we will first see if the user is clicking within the pick-up box. We will check the `mousePressed` variable and `withinBounds()` method to do so. Then we will check to see if the user has enough money, if they wanted to purchase the tower. We will use the `hasSufficientFunds()` method that we had previously implemented to see if they do. If both of these conditions are true, then we should colour the text red to warn the user that they have insufficient funds. + +{{% expand "See code for funds warnings" "false" %}} +```java +// Checks to see if the user is attempting to purchase/pick up a tower but has insufficient funds +boolean attemptingToPurchaseTowerWithoutFunds(int towerID) { + if (mousePressed && withinBounds(towerID) && !hasSufficientFunds(towerPrice[towerID])) { + return true; + } + else { + return false; + } +} +``` +{{% /expand %}} + +### Displaying Funds + +{{% expand "See code" "false" %}} +```java +// Displays the user's current balance on the screen +void drawBalanceDisplay() { + // If the user is attempting to purchase a tower without funds, warn them with red display text + if (attemptingToPurchaseTowerWithoutFunds()) { + fill(towerErrorColour); // Red text + } + else { + fill(0); // Black text + } + + text("Current Balance: $" + currentBalance, 336, 65); +} +``` +{{% /expand %}} +First we check if they are trying to buy a tower without funds, since then we make the text turn red to warn the user that they do not have enough funds. We call the method that was written before, `attemptingToPurchaseTowerWithoutFunds()`, to check if the user doesn’t have enough money. This will return a boolean value (either `True` or `False`) to us, so we can put this in an if statement. If you check back above, it returns true when the user doesn’t have funds, so we change the fill colour to towerErrorColour (global variable in `Towers.pde` when towers are placed illegally) in the if block. Otherwise, we change the text back to the default black text. We can then display the balance, using the `text()` method. diff --git a/content/game-dev/part-ii/_index.md b/content/game-dev/part-ii/_index.md index 2ac281c2..6c1f3f22 100644 --- a/content/game-dev/part-ii/_index.md +++ b/content/game-dev/part-ii/_index.md @@ -1,5 +1,6 @@ +++ chapter = true +alwaysopen = false title = "Part II" pre = "2. " weight = 2 diff --git a/content/game-dev/part-ii/introduction.files/AllTemplates.zip b/content/game-dev/part-ii/introduction.files/AllTemplates.zip new file mode 100644 index 00000000..7a589f62 Binary files /dev/null and b/content/game-dev/part-ii/introduction.files/AllTemplates.zip differ diff --git a/content/game-dev/part-ii/introduction.md b/content/game-dev/part-ii/introduction.md new file mode 100644 index 00000000..d7d73d44 --- /dev/null +++ b/content/game-dev/part-ii/introduction.md @@ -0,0 +1,79 @@ ++++ +title = "Introduction" +weight = 1 ++++ + +--- + +## Game Dev - Season IV + +Welcome to the return of MCPT's Game Dev Series! Over the course of 3 workshops, we have something for everyone, whether you’re a beginner or an experienced coder. Inspired by **Bloons Tower Defense**, you will learn how to code your very own tower-defense game in Processing! + +{{% notice tip "Challenges" %}} +Earn up to **300** bonus points with our tower-creation challenges! Stay tuned for more info! +{{% /notice %}} + +### Demo + +{{< p5js-src "/content/p5-js-demo/part2.js" >}} + +{{% expand "What is a tower-defense game?" %}} +{{% notice info %}} + +A **tower-defense game** is a type of strategy game where players will place down "towers", usually with a form of in-game currency that will defend against a set of enemies. If you fail to defeat the enemies, you will typically lose health - when you reach zero, it's **Game Over.** + +In **Bloons Tower Defense**, the towers are monkeys, and the enemies are balloons, which spawn in rounds and travel along a pre-determined path. Your goal as a player is to strategically spend your currency while placing towers in effective locations, using their special abilities to win the game! + +In this workshop, we will cover the fundamental aspects of a tower-defense game, and in future workshops, we will cover special abilities and ways that you can customize your game to make it your own! + +{{% /notice %}} +{{% /expand %}} + +Everything will be made step-by-step allowing you to learn and see the progress of the game. The speed of the workshop is not set and will be altered at a moment's notice in order, so don’t worry if you don't get everything immediately! + +This website is interactive and holds all the content that we will be going over. Flip along and test code just as we do! This site will also be accessible at any time for you to look back and review content. + +### Additional Info + +{{% expand "What you will learn" %}} +{{% notice info %}} +In the second part of this workshop, you will learn how to create multiple types of towers, unique abilities, projectiles, and more! + +This includes: +* How to use IDs to add multiple types of towers and projectiles +* How to implement a full currency system and adjust the cost of different towers +* How to create a custom tower tracking system +* How to use pre-built template code to create projectiles + {{% /notice %}} + {{% /expand %}} + + +{{% expand "What you will need" %}} +{{% notice info %}} + +You will need to download [Processing 4.0](https://processing.org/download) from https://processing.org/download, or have Processing 3 or newer installed. + +In our workshop, we will be adding code to several **templates**, which you can download below. + +{{% /notice %}} + + +{{%attachments style="blue" title="All Templates" pattern=".*zip" /%}} + + +{{% /expand %}} + + + +{{% expand "The Game Jam" %}} +{{% notice tip %}} + +Ultimately, the Game Dev Series will build up to a week-long Game Jam during the Winter Break. During the Game Jam, you'll develop and create the best game you can! + +There will be many prizes and awards so make sure to participate! + +In addition, by participating in the Game Dev Series and completing challenges, you will earn points which will help you win awards during the Game Jam. + +{{% /notice %}} +{{% /expand %}} + diff --git a/content/game-dev/part-ii/newtower.files/NewTowerTemplate.zip b/content/game-dev/part-ii/newtower.files/NewTowerTemplate.zip new file mode 100644 index 00000000..b38191c8 Binary files /dev/null and b/content/game-dev/part-ii/newtower.files/NewTowerTemplate.zip differ diff --git a/content/game-dev/part-ii/newtower.md b/content/game-dev/part-ii/newtower.md new file mode 100644 index 00000000..6246dec9 --- /dev/null +++ b/content/game-dev/part-ii/newtower.md @@ -0,0 +1,87 @@ ++++ +title = "New Towers" +weight = 5 ++++ + +--- + +{{%attachments style="red" title="Part2_NewTowerTemplate" pattern=".*zip" /%}} + +### Support for New Towers +Part I used only one type of tower, and it was hard-coded into our system. Our program had no support for updating or changing the type of tower, since it assumed there was only one type. Usually, we would use **[Object Oriented Programming](https://en.wikipedia.org/wiki/Object-oriented_programming)**, but this topic isn’t taught until ICS3U. Instead, we’ll try to simplify it into something that is less organized but doesn’t require a whole new topic of programming. + +We want to support multiple types of towers in our improved program. To do this, we decided to map every type of tower to an integer. The three types of towers that we decided to create are the **default** tower (the default tower we used before), the **eight-shot** tower (similar to the tack shooter in BTD), and the **slow** tower, which slows targets (similar to the glue gunner). We mapped default to `0`, eight-shot to `1`, and slow to `2`. + +```java +final int def = 0, eight = 1, slow = 2; +``` +#### Updating Drag and Drop + +In Part I, we taught how to implement drag and drop on a singular tower. To change this to support multiple towers, let’s create some new things. First, let’s create a boolean array of size 3, with indices 0, 1 and 2 representing our towers above. When making your own towers, your array is not limited to size 3 and can be any size you want. Essentially, this array will hold the values of which tower is being held right now. + +```java +//values of which tower is being held +//for example, if index 1 is true, then we are holding the eight-shot tower +boolean[] held = {false, false, false}; +``` + +Let’s also create a variable called `currentlyDragging`. This will hold the index of the boolean array above that is true. In other words, it will hold the value of the tower we are currently draggin (0 for default, 1 for eight-shot, 2 for slow). Alongside this, let’s create two arrays of PVectors, named `originalLocations` and `dragAndDropLocations`. The first one represents the locations where you are supposed to drag the towers from, whereas the second represents where our current tower is right now. Let’s also create `towerPrice` and `towerColours`, which are the prices of the towers and the colours of the towers, respectively. + +```java + +int currentlyDragging = -1; // -1 = no tower, 0 = within default, 1 = within eight, 2 = within slow +int[] towerPrice = {100, 200, 200}; +color[] towerColours = {#7b9d32, #F098D7, #82E5F7}; +PVector[] originalLocations = {new PVector(650, 50), new PVector(700, 50), new PVector(750, 50)}; +PVector[] dragAndDropLocations = {new PVector(650, 50), new PVector(700, 50), new PVector(750, 50)}; +//notice indices 0, 1 and 2 correspond to the default, eight-shot and slow towers respectively +``` + +Using these, we can change how our drag and drop functions. Instead of checking just one location, we can check for all the locations in our `originalLocations` array. Then, we can edit the values in `dragAndDropLocations` according to the tower we picked up. For example, if we pick up the freeze tower and bring it to coordinates `(400, 39)`, then our new dragAndDropLocations would be as follows. + +```java +/*values for dragAndDropLocations: + index: 0 1 2 + value: (650, 60) (700, 50) (400, 39) +*/ +dragAndDropLocations = {new PVector(650, 50), new PVector(700, 50), new PVector(400, 39)}; +``` + +Note that we did not have an array for size as all of our towers are the same size. We’ll leave this implementation as an exercise to the reader. + +#### New Tower Data + +Previously, we had just used a PVector ArrayList holding all the values of the `(x, y)` coordinates of our towers. Now, we also want to create an ArrayList storing the different values of our towers. Notice how we had previously used indices to represent our towers, using indices `0`, `1,` and `2` to represent our default, eight-shot and slow towers, respectively. Let's use the same concept here on our ArrayLists. Since we want to store multiple values, let's make our ArrayList of type integer array. That is, let's create an ArrayList of integer arrays. + +In our program, integers in the array represent the cooldown between the next projectile, the maximum cooldown, the range, and the projectile ID. We can then loop through these arrays similarily to what we did before, and apply the changes to the towers currently on our map. Here is how our tower data is made. Remember that you can play around with these values to create different types of towers. + +```java +int[] makeTowerData(int towerID) { + if (towerID == def) { + return new int[] { + 10, // Cooldown between next projectile + 10, // Max cooldown + towerVisions[def], // Tower Vision + 0 // Projectile ID + }; + } else if (towerID == eight) { + return new int[] { + 25, // Cooldown between next projectile + 25, // Max cooldown + towerVisions[eight], // Tower Vision + 1 // Projectile ID + }; + } else if (towerID == slow) { + return new int[] { + 35, + 35, + towerVisions[slow], // Tower Vision + 2 + }; + } + return new int[] {}; //filler since we need to return something +} +``` + +### TL;DR +To create new towers, use arrays with each index representing values for that type of tower. All of the original drag and drop methods now ahve a parameter **towerID** passed into them corresponding to which of the three is currently being dragged/dropped. Alternatively, you can use **[Object Oriented Programming](https://en.wikipedia.org/wiki/Object-oriented_programming)**. \ No newline at end of file diff --git a/content/game-dev/part-ii/projectiles.md b/content/game-dev/part-ii/projectiles.md new file mode 100644 index 00000000..80e05cd6 --- /dev/null +++ b/content/game-dev/part-ii/projectiles.md @@ -0,0 +1,105 @@ ++++ +title = "Projectiles" +weight = 6 ++++ + +--- +### Projectile API +For projectiles, we decided to provide an API for you guys. We’ve coded three types of projectiles already, but this part of the workshop will teach you how we made them and how to make more. + +The API contains all the code needed for a projectile to function. Essentially, it creates, stores and updates projectiles and their attributes. + +In short, every projectile has seven values: damage, pierce, angle, current distance travelled, max distance travelled, thickness, and damage type. Damage represents the damage that each projectile does. Pierce is the number of balloons that a projectile can hit before it disappears. Angle is the angle that the projectile is being shot at. The current distance travelled is the distance our current projectile has travelled, whereas the maximum distance travelled hits a range for where the projectile can travel. + +We also used a method called `createProjectile`, which takes in values for the centre, velocity, damage, pierce, maximum distance travelled, thickness and damage type, and it will add it to an ArrayList called `projectileData`. Here is what the code looks like. + +{{% expand "See code" "false" %}} +```java +final int damage = 0, pierce = 1, angle = 2, currDistTravelled = 3, maxDistTravelled = 4, thickness = 5, dmgType = 6; // Constants to make accessing the projectileData array more convenient + +void createProjectile(PVector centre, PVector vel, float damage, int pierce, float maxDistTravelled, float thickness, int dmgType) { + balloonsHit.add(new HashSet()); // Adds an empty set to the balloonsHit structure - this represents the current projectile, not having hit any balloons yet. + center.add(centre); // Adds the starting location of the projectile as the current location + velocity.add(vel); // Adds the velocity of the projectile to the list + float angle = atan2(vel.y, vel.x); + projectileData.add(new float[]{damage, pierce, angle, 0, maxDistTravelled, thickness, dmgType}); +} +``` +{{% /expand %}} + +### Handling Projectiles + +###### Projectile Cooldown + +Cooldown time is a term that is familiar to people who play games. It is applicable to things that do something regularly (eg. shoot, heal, ect.), and it tells us in how much time the thing has to wait to perform its next task. Take the tower that shoots 8 projectiles in our bloons tower defense game. Rather than shooting continuously, it shoots every few moments. In other words, there is a time delay between each shot. + +How would you control this delay in code? In our game, each tower has a cooldown time. It starts at 0 when the game starts (meaning the tower can start shooting once a balloon is in range), but after it resets to its designated cooldown time. As moments pass, the cooldown time (or time it has left to wait) decreases until it gets to zero again. Then it can shoot once more. + +In the handleProjectiles() function, all towers are handled one at a time using a loop that iterates through the list of towers. The data each tower has associated with it is transferred to the data array, and you can see that right after this data transfer, the index/position in the data array that stores the remaining cooldown time decreases by one. After that is the if-statement that checks if the cooldown time remaining is 0, as well as if a balloon is in range. These are the 2 requirements that are needed to actually shoot a projectile. If these conditions are met, then a projectile will be drawn + +###### Projectile Type + +Once a tower’s cooldown is 0 and there is a balloon in range, the next thing to determine is which projectile should be drawn. This is determined by the tower’s associated projectile type, stored also in the data array. In the code, this determining is done in the if-else if statements inside the one we just looked at. Let's look at the first one. Essentially what it says is: if the projectile type of this tower is equal to the default projectile type, create all the necessary information needed for a projectile, like its speed, damage, piercing ability, visual thickness, and its maximum travel distance. Then it simply puts all this information into the create projectile function, which then goes on to draw the projectile. + +If the tower's projectile type didn’t match with the default type, the program would simply move to the next else-if statement to check if it is the type that is shot 8 at a time. If not, it would move to the next else-if, and so on. + +###### Updating Projectiles + +{{% expand "See code" "false" %}} +```java +// Displays projectiles and removes those which need to be removed +for (int projectileID = 0; projectileID < projectileData.size(); projectileID++) { + drawProjectile(projectileID); + if (dead(projectileID)) { + projectileData.remove(projectileID); + center.remove(projectileID); + velocity.remove(projectileID); + balloonsHit.remove(projectileID); + projectileID--; + } +} +``` +{{% /expand %}} +In the next part of this method, we loop through all the projectiles made, by looping through the ArrayList. We update each projectile with the drawProjectile method (part of template code) which will draw each projectile with all of its attributes such as colour, thickness, etc., then calls two more methods. One that updates the projectile’s movement, and then a final method that checks for collisions with balloons. + +Another part of handling projectiles is removing the projectiles that are ‘dead’. To determine if a projectile is dead, it must either be: +- off the screen +- it cannot pierce anymore balloons +- travelled past the max distance for that type of projectile + +Inside the if block, we remove the projectile from each of the ArrayLists. These ArrayLists store aspects of the projectiles, such as the centre, velocity, and other data. We must remember to decrement the loop counter `projectileID` when we remove elements from the ArrayLists we are iterating through so that we do not skip any elements. + +```java +// Checks if a projectile is ready to be removed (is it off screen? has it already reached its maximum pierce? has it exceeded the maximum distance it needs to travel?) +public boolean dead(int projectileID) { + float[] data = projectileData.get(projectileID); + return offScreen(projectileID) || data[pierce] == 0 || data[currDistTravelled] > data[maxDistTravelled]; +} +``` + +This is the dead() method from the template code that identifies projectiles as ‘dead’. The `||` operator is the OR operator, which returns true if at least 1 condition is true. This means that if any of these three conditions are true, the method will return true, resulting in this projectile being removed. + + +### Balloon Hitting Special Effects +In order to incorporate special effects like the glue gunner slowing down a balloon, we will slightly modify the `hitBalloon()` method. The method will edit features of the balloon (e.g. health, speed) depending on the type of projectile it was hit by. First, retrieve the projectile info from the `projectileData` ArrayList. After decreasing the balloon's hp by the amount of damage that the projectile inflicts, check to see if the projectile is supposed to slow the balloon down (such as a projectile from the glue gunner). Also make sure the balloon has not already been slowed down by another projectile. + +{{% expand "See code" "false" %}} +```java +if (data[dmgType] == slow && balloonData[slowed] == 0) +``` +{{% /expand %}} + +To slow down the balloon, multiply its speed by a decimal (we chose 0.7) and mark the balloon as "slowed" by setting the `slowed` parameter in `balloonData` to 1. This ensures that the balloon does not get slowed again by a different projectile. + +{{% expand "See code" "false" %}} +```java +if (data[dmgType] == slow && balloonData[slowed] == 0) { // Slows down the balloon + balloonData[speed] *= 0.7; + balloonData[slowed] = 1; +} +``` +{{% /expand %}} + +{{% notice info "Challenge: Create Your Own Tower!" %}} +Try implementing your own new tower to earn some bonus points! You will earn 100 points for successfully creating a new tower. The participant who submits the "coolest" tower will earn 300 points. Second place will earn 280 points, third place will earn 260 points, etc. +{{% /notice %}} diff --git a/content/p5-js-demo/index.html b/content/p5-js-demo/index.html deleted file mode 100644 index be7478ae..00000000 --- a/content/p5-js-demo/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Demo - - - - - - - \ No newline at end of file diff --git a/content/p5-js-demo/sketch.js b/content/p5-js-demo/part1.js similarity index 98% rename from content/p5-js-demo/sketch.js rename to content/p5-js-demo/part1.js index 28c33145..061c4b3e 100644 --- a/content/p5-js-demo/sketch.js +++ b/content/p5-js-demo/part1.js @@ -15,15 +15,18 @@ function draw() { background(color(0xad, 0xd5, 0x58)); if(!started) { - const sz = 80 + Math.sin(frames / 10) * 2; + const sz = 40 + Math.sin(frames / 15); textSize(sz); textAlign(CENTER, CENTER) - fill(color(0x7b, 0x9d, 0x32)); + rectMode(CENTER); noStroke(); - rect(400, 250, sz * 9, sz * 2); + fill(color(0xed, 0xd7, 0x60)); + rect(400, 250, sz * 9, sz * 2, 50); + fill(color(0xfd, 0xe7, 0x70)); + rect(400, 250, sz * 9 - 10, sz * 2 - 10, 50); fill(color(0x4C, 0x67, 0x10)); - text("Click To Start! ", 408, 255); + text("click to start! ", 408, 255); return; } textAlign(LEFT, BASELINE) @@ -479,4 +482,3 @@ function preload() { // I found the following calls that you should move here: // (note that line numbers are from your Processing code) } - diff --git a/content/p5-js-demo/part2.js b/content/p5-js-demo/part2.js new file mode 100644 index 00000000..b5fd95ac --- /dev/null +++ b/content/p5-js-demo/part2.js @@ -0,0 +1,912 @@ +// Program main method +function setup() { + initializeFields(); + createCanvas(800, 500); + loadHeartIcon(); + initDragAndDrop(); + initPath(); + createFirstWave(); +} + +let started; +let frames = 0; + +function draw() { + frames++; + background(color(0xad, 0xd5, 0x58)); + + if(!started) { + const sz = 40 + Math.sin(frames / 15); + textSize(sz); + textAlign(CENTER, CENTER) + + rectMode(CENTER); + noStroke(); + fill(color(0xed, 0xd7, 0x60)); + rect(400, 250, sz * 9, sz * 2, 50); + fill(color(0xfd, 0xe7, 0x70)); + rect(400, 250, sz * 9 - 10, sz * 2 - 10, 50); + fill(color(0x4C, 0x67, 0x10)); + text("click to start! ", 408, 255); + return; + } + textAlign(LEFT, BASELINE) + drawPath(); + // Draw all the towers that have been placed down before + drawAllTowers(); + handleProjectiles(); + drawTrash(); + drawSelectedTowers(); + dragAndDropInstructions(); + drawBalloons(); + drawHealthBar(); + drawBalanceDisplay(); + if (health <= 0) { + drawLostAnimation(); + } +} + +// Whenever the user drags the mouse, update the x and y values of the tower +function mouseDragged() { + if (currentlyDragging !== notDragging) { + dragAndDropLocations[currentlyDragging] = new p5.Vector(mouseX + difX, mouseY + difY); + } +} + +// Whenever the user initially presses down on the mouse +function mousePressed() { + started = true; + for (var i = 0; i < towerCount; i++) { + handlePickUp(i); + } +} + +// Whenever the user releases their mouse +function mouseReleased() { + if (currentlyDragging !== notDragging) { + handleDrop(currentlyDragging); + } + currentlyDragging = notDragging; +} + +var balloons; + +var distanceTravelled, delay, speed, hp, slowed, ID; + +// Radius of the balloon +var balloonRadius; + +var maxBalloonHP; + +/* +Encompasses: Displaying Balloons, Waves & Sending Balloons, Balloon Reaching End of Path +*/ +function createFirstWave() { + // {Number of "steps" taken, frames of delay before first step, speed, hp, slowed (0=no, 1=yes)} + for (var i = 0; i <= 20; i++) { + balloons.push([0, i * 10 + 100, 3, maxBalloonHP, 0, i]); + } + for (var i = 0; i <= 30; i++) { + balloons.push([0, i * 5 + 360, 2, maxBalloonHP, 0, i]); + } + for (var i = 0; i <= 30; i++) { + balloons.push([0, i * 30 + 600, 5, maxBalloonHP, 0, i]); + } + for (var i = 0; i <= 200; i++) { + balloons.push([0, i * 5 + 2000, 5, maxBalloonHP, 0, i]); + } + balloons.sort(function (a, b) { + return a[delay] - b[delay] + }); +} + +// Displays and moves balloons +function updatePositions(balloon) { + // Only when balloonProps[1] is 0 (the delay) will the balloons start moving. + if (balloon[delay] === 0) { + var position = getLocation(balloon[distanceTravelled]); + var travelSpeed = balloon[speed]; + // Increases the balloon's total steps by the speed + balloon[distanceTravelled] += travelSpeed; + // Drawing of ballon + ellipseMode(CENTER); + strokeWeight(0); + stroke(0); + fill(0); + if (balloon[hp] < maxBalloonHP) { + // draw healthbar outline + stroke(0, 0, 0); + strokeWeight(0); + rectMode(CORNER); + fill(color(0x83, 0x00, 0x00)); + var hbLength = 35, hbWidth = 6; + rect(position.x - hbLength / 2, position.y - (balloonRadius), hbLength, hbWidth); + // draw mini healthbar + noStroke(); + fill(color(0xFF, 0x31, 0x31)); + // the healthbar that changes based on hp + rect(position.x - hbLength / 2, position.y - (balloonRadius), hbLength * (balloon[hp] / maxBalloonHP), hbWidth); + noFill(); + // write text + stroke(0, 0, 0); + textSize(14); + fill(255, 255, 255); + text("Health: " + health, 670, 462); + } + fill(color(0xf3, 0xcd, 0x64)); + if (balloon[slowed] === 1) { + fill(color(0xC1, 0x9D, 0x40)); + } + ellipse(position.x, position.y, balloonRadius, balloonRadius); + } else { + balloon[delay]--; + } +} + +function drawBalloons() { + for (var i = 0; i < balloons.length; i++) { + var balloon = balloons[i]; + updatePositions(balloon); + if (balloon[hp] <= 0) { + handleBalloonPop(); + balloons.splice(i, 1); + i--; + continue; + } + if (atEndOfPath(balloon[distanceTravelled])) { + // Removing the balloon from the list + balloons.splice(i, 1); + // Lost a life. + health--; + // Must decrease this counter variable, since the "next" balloon would be skipped + i--; + // When you remove a balloon from the list, all the indexes of the balloons "higher-up" in the list will decrement by 1 + } + } +} + +// Similar code to distance along path +function atEndOfPath(travelDistance) { + var totalPathLength = 0; + for (var i = 0; i < points.length - 1; i++) { + var currentPoint = points[i]; + var nextPoint = points[i + 1]; + var distance = dist(currentPoint.x, currentPoint.y, nextPoint.x, nextPoint.y); + totalPathLength += distance; + } + // This means the total distance travelled is enough to reach the end + if (travelDistance >= totalPathLength) + return true; + return false; +} + +// variable to track user's health +var health; + +var heart; + +// ------- HP SYSTEM -------- +/* + Heath-related variables: + int health: The player's total health. + This number decreases if balloons pass the end of the path (offscreen), currentely 11 since there are 11 balloons. + PImage heart: the heart icon to display with the healthbar. + */ +function loadHeartIcon() { + //heart = loadImage("heart.png"); +} + +// method to draw a healthbar at the bottom right of the screen +function drawHealthBar() { + // draw healthbar outline + stroke(0, 0, 0); + strokeWeight(0); + fill(color(0x83, 0x00, 0x00)); + rectMode(CENTER); + rect(721, 455, 132, 20); + // draw healthbar + noStroke(); + rectMode(CORNER); + fill(color(0xFF, 0x31, 0x31)); + // the healthbar that changes based on hp + rect(655, 445.5, health * 12, 20); + rectMode(CENTER); + noFill(); + // write text + stroke(0, 0, 0); + textSize(14); + fill(255, 255, 255); + text("Health: " + health, 670, 462); + // put the heart.png image on screen + imageMode(CENTER); + image(heart, 650, 456); + noFill(); +} + +// Give the user $750 of starting balance +var currentBalance; + +// Money earned by popping a balloon +var rewardPerBalloon; + +/** + * Currency system for tower defense + * - Rewards player for popping balloon + * - Keeps track of balance + * - Checks for sufficient funds when purchasing tower + */ +// Current amount of money owned by the player +function handleBalloonPop() { + // Reward the player for popping the balloon + increaseBalance(rewardPerBalloon); +} + +function increaseBalance(amount) { + // Increase the current balance by the amount given + currentBalance += amount; +} + +/** + * Checks to see if there is sufficient balance for purchasing a certain item + * Parameter "cost" is the cost of the tower to be purchased + */ +function hasSufficientFunds(cost) { + if (currentBalance < cost) { + // Not enough money to purchase the tower + return false; + } else { + // Enough money to purchase the tower + return true; + } +} + +/** + * Purchases a tower + * Parameter "cost" is the cost of the tower to be purchased + */ +function purchaseTower(cost) { + currentBalance -= cost; +} + +// Checks to see if the user is attempting to purchase/pick up a tower but has insufficient funds +function attemptingToPurchaseTowerWithoutFunds(towerID) { + if (mouseIsPressed && withinBounds(towerID) && !hasSufficientFunds(towerPrice[towerID])) { + return true; + } else { + return false; + } +} + +// Displays the user's current balance on the screen +function drawBalanceDisplay() { + // If the user is attempting to purchase a tower without funds, warn them with red display text + var error = false; + for (var i = 0; i < towerCount; i++) { + if (attemptingToPurchaseTowerWithoutFunds(i)) { + error = true; + } + } + if (error) { + fill(towerErrorColour); + } else { + // Black text + fill(0); + } + text("Current Balance: $" + currentBalance, 336, 65); +} + +// -1 = not holding any tower, 0 = within default, 1 = within eight, 2 = within slow +var currentlyDragging; + +var notDragging; + +var difX, difY, count; + +var held; + +var towerPrice; + +var towerColours; + +// Constant, "copy" array to store where the towers are supposed to be +var originalLocations; + +// Where the currently dragged towers are +var dragAndDropLocations; + +// Towers that are placed down +var towers; + +var towerSize; + +// Colour to display when user purchases tower without sufficient funds +var towerErrorColour; + +// these variables are the trash bin coordinates +var trashX1, trashY1, trashX2, trashY2; + +/* +Encompasses: Displaying Towers, Drag & Drop, Discarding Towers, Rotating Towers, Tower Validity Checking + */ +// -------- CODE FOR DRAG & DROP ---------------------- +// final color +function initDragAndDrop() { + difX = 0; + difY = 0; + trashX1 = 525; + trashY1 = 30; + trashX2 = 775; + trashY2 = 120; + count = 0; + towers = []; + towerData = []; +} + +// Use point to rectangle collision detection to check for mouse being within bounds of pick-up box +function pointRectCollision(x1, y1, x2, y2, size) { + // --X Distance-- --Y Distance-- + return (Math.abs(x2 - x1) <= size / 2) && (Math.abs(y2 - y1) <= size / 2); +} + +function withinBounds(towerID) { + var towerLocation = dragAndDropLocations[towerID]; + return pointRectCollision(mouseX, mouseY, towerLocation.x, towerLocation.y, towerSize); +} + +// check if you drop in trash +function trashDrop(towerID) { + var location = dragAndDropLocations[towerID]; + if (location.x < 0 || location.x > 800 || location.y < 0 || location.y > 500) + return true; + if (location.x >= trashX1 && location.x <= trashX2 && location.y >= trashY1 && location.y <= trashY2) + return true; + return false; +} + +// -------Methods Used for further interaction------- +function handleDrop(towerID) { + // Instructions to check for valid drop area will go here + if (trashDrop(towerID)) { + dragAndDropLocations[towerID] = originalLocations[towerID]; + held[towerID] = false; + print("Dropped object in trash."); + } else if (legalDrop(towerID)) { + towers.push(dragAndDropLocations[towerID].copy()); + towerData.push(makeTowerData(towerID)); + dragAndDropLocations[towerID] = originalLocations[towerID]; + held[towerID] = false; + purchaseTower(towerPrice[towerID]); + print("Dropped for the " + (++count) + "th time."); + } +} + +// Will be called whenever a tower is picked up +function handlePickUp(pickedUpTowerID) { + if (withinBounds(pickedUpTowerID) && hasSufficientFunds(towerPrice[pickedUpTowerID])) { + currentlyDragging = pickedUpTowerID; + held[currentlyDragging] = true; + var location = dragAndDropLocations[pickedUpTowerID]; + // Calculate the offset values (the mouse pointer may not be in the direct centre of the tower) + difX = parseInt(location.x) - mouseX; + difY = parseInt(location.y) - mouseY; + } + print("Object picked up."); +} + +function drawTrash() { + rectMode(CORNERS); + noStroke(); + fill(color(0x4C, 0x67, 0x10)); + rect(trashX1, trashY1, trashX2, trashY2); + fill(255, 255, 255); + stroke(255, 255, 255); +} + +function dragAndDropInstructions() { + fill(color(0x4C, 0x67, 0x10)); + textSize(12); + text("Pick up tower from here!", 620, 20); + text("You can't place towers on the path of the balloons!", 200, 20); + text("Place a tower into the surrounding area to put it in the trash.", 200, 40); + text("Mouse X: " + mouseX + "\nMouse Y: " + mouseY + "\nMouse held: " + mouseIsPressed + "\nTower Held: " + currentlyDragging, 15, 20); +} + +// -------- CODE FOR PATH COLLISION DETECTION --------- +function pointDistToLine(start, end, point) { + // Code from https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment + // i.e. |w-v|^2 - avoid a sqrt + var l2 = (start.x - end.x) * (start.x - end.x) + (start.y - end.y) * (start.y - end.y); + // v === w case + if (l2 === 0.0) + return dist(end.x, end.y, point.x, point.y); + var t = Math.max(0, Math.min(1, p5.Vector.sub(point, start).dot(p5.Vector.sub(end, start)) / l2)); + // Projection falls on the segment + var projection = p5.Vector.add(start, p5.Vector.mult(p5.Vector + .sub(end, start), t)); + return dist(point.x, point.y, projection.x, projection.y); +} + +function shortestDist(point) { + var answer = Number.MAX_VALUE; + for (var i = 0; i < points.length - 1; i++) { + var start = points[i]; + var end = points[i + 1]; + var distance = pointDistToLine(start, end, point); + answer = Math.min(answer, distance); + } + return answer; +} + +// Will return if a drop is legal by looking at the shortest distance between the rectangle center and the path. +function legalDrop(towerID) { + var heldLocation = dragAndDropLocations[towerID]; + // checking if this tower overlaps any of the already placed towers + for (var i = 0; i < towers.length; i++) { + var towerLocation = towers[i]; + if (pointRectCollision(heldLocation.x, heldLocation.y, towerLocation.x, towerLocation.y, towerSize)) + return false; + } + return shortestDist(heldLocation) > PATH_RADIUS; +} + +var framesSinceLost; + +function drawLostAnimation() { + framesSinceLost++; + var alpha = 166 * framesSinceLost / 80; + if (alpha > 166) + alpha = 166; + fill(127, alpha); + rectMode(CORNER); + noStroke(); + rect(0, 0, 800, 500); + var textAlpha = 255 * (framesSinceLost - 80) / 80; + if (textAlpha > 255) + ; + textAlpha = 255; + fill(255, textAlpha); + textSize(70); + text("YOU LOST...", 265, 260); +} + +// The points on the path, in order. +var points; + +var PATH_RADIUS; + +/* +Encompasses: The Path for Balloons, Balloon Movement + */ + +// ------- CODE FOR THE PATH +function addPointToPath(x, y) { + points.push(new p5.Vector(x, y)); +} + +function initPath() { + addPointToPath(0, 200); + addPointToPath(50, 200); + addPointToPath(200, 150); + addPointToPath(350, 200); + addPointToPath(500, 150); + addPointToPath(650, 200); + addPointToPath(650, 300); + addPointToPath(500, 250); + addPointToPath(350, 300); + addPointToPath(200, 250); + addPointToPath(50, 300); + addPointToPath(50, 400); + addPointToPath(200, 350); + addPointToPath(350, 400); + addPointToPath(500, 350); + addPointToPath(650, 400); + addPointToPath(800, 350); +} + +function drawPath() { + stroke(color(0x4C, 0x67, 0x10)); + strokeWeight(PATH_RADIUS * 2 + 1); + for (var i = 0; i < points.length - 1; i++) { + var currentPoint = points[i]; + var nextPoint = points[i + 1]; + line(currentPoint.x, currentPoint.y, nextPoint.x, nextPoint.y); + } + stroke(color(0x7b, 0x9d, 0x32)); + strokeWeight(PATH_RADIUS * 2); + for (var i = 0; i < points.length - 1; i++) { + var currentPoint = points[i]; + var nextPoint = points[i + 1]; + line(currentPoint.x, currentPoint.y, nextPoint.x, nextPoint.y); + } +} + +var dp; + +// GIVEN TO PARTICIPANTS BY DEFAULT +function getLocation(travelDistance) { + var memoized = dp[travelDistance]; + if (memoized !== undefined) { + return memoized; + } + var originalDist = travelDistance; + for (var i = 0; i < points.length - 1; i++) { + var currentPoint = points[i]; + var nextPoint = points[i + 1]; + var distance = dist(currentPoint.x, currentPoint.y, nextPoint.x, nextPoint.y); + if (distance <= 0.000000001 || travelDistance >= distance) { + travelDistance -= distance; + } else { + // In between two points + var travelProgress = travelDistance / distance; + var xDist = nextPoint.x - currentPoint.x; + var yDist = nextPoint.y - currentPoint.y; + var x = currentPoint.x + xDist * travelProgress; + var y = currentPoint.y + yDist * travelProgress; + dp[originalDist] = new p5.Vector(x, y); + return new p5.Vector(x, y); + } + } + // At end of path + dp[originalDist] = points[points.length - 1]; + return points[points.length - 1]; +} + +var center, velocity; + +var projectileData; + +var balloonsHit; + +var damage, pierce, angle, currDistTravelled, maxDistTravelled, thickness, dmgType; + +var projectileRadius; + +function createProjectile(centre, vel, damage, pierce, maxDistTravelled, thickness, dmgType) { + balloonsHit.push([]); + center.push(centre); + velocity.push(vel); + var angle = atan2(vel.y, vel.x); + projectileData.push([damage, pierce, angle, 0, maxDistTravelled, thickness, dmgType]); +} + +function distToProjectile(projectileID, point) { + var data = projectileData[projectileID]; + var width = cos(data[angle]), height = sin(data[angle]); + var displacement = new p5.Vector(width, height).mult(projectileRadius); + if (data[dmgType] === laser) + displacement.mult(1000); + var start = p5.Vector.add(center[projectileID], displacement), + end = p5.Vector.sub(center[projectileID], displacement); + if (data[dmgType] === laser) + end = center[projectileID]; + return pointDistToLine(start, end, point); +} + +function dead(projectileID) { + var data = projectileData[projectileID]; + return offScreen(projectileID) || data[pierce] === 0 || data[currDistTravelled] > data[maxDistTravelled]; +} + +function offScreen(projectileID) { + return center[projectileID].x < 0 || center[projectileID].x > 800 || center[projectileID].y < 0 || center[projectileID].y > 500; +} + +function drawProjectile(projectileID) { + var data = projectileData[projectileID]; + stroke(255); + strokeWeight(data[thickness]); + var width = cos(data[angle]), height = sin(data[angle]); + var displacement = new p5.Vector(width, height).mult(projectileRadius); + if (data[dmgType] === laser) + displacement.mult(1000); + var start = p5.Vector.add(center[projectileID], displacement), + end = p5.Vector.sub(center[projectileID], displacement); + if (data[dmgType] === laser) + end = center[projectileID]; + line(start.x, start.y, end.x, end.y); + handleCollision(projectileID); + if (data[dmgType] !== laser) + center[projectileID] = p5.Vector.add(center[projectileID], velocity[projectileID]); + if (data[dmgType] !== laser) + data[currDistTravelled] += velocity[projectileID].mag(); + else + data[currDistTravelled]++; +} + +function hitBalloon(projectileID, balloonData) { + var data = projectileData[projectileID]; + if (data[pierce] === 0 || balloonsHit[projectileID].includes(parseInt(balloonData[ID]))) + return; + data[pierce]--; + balloonData[hp] -= data[damage]; + if (data[dmgType] === slow && balloonData[slowed] === 0) { + balloonData[speed] *= 0.7; + balloonData[slowed] = 1; + } + balloonsHit[projectileID].push(parseInt(balloonData[ID])); +} + +function handleCollision(projectileID) { + var data = projectileData[projectileID]; + if (data[pierce] === 0) return; + for (var b in balloons) { + var balloon = balloons[b]; + // If the balloon hasn't entered yet, don't count it + if (balloon[delay] !== 0) + break; + var position = getLocation(balloon[distanceTravelled]); + if (distToProjectile(projectileID, position) <= balloonRadius / 2 + data[thickness] / 2) { + hitBalloon(projectileID, balloon); + } + } +} + +// -------------------------------- PROJECTILE CREATION (Participants will NOT be required to code the stuff above this line) ----------------------------------- +function track(towerLocation, vision) { + var maxDist = 0; + var location = undefined; + for (var b in balloons) { + var balloon = balloons[b] + var balloonLocation = getLocation(balloon[distanceTravelled]); + // Checks if the tower can see the balloon + if (dist(balloonLocation.x, balloonLocation.y, towerLocation.x, towerLocation.y) <= vision) { + // If the balloon has travelled further than the previously stored one, it is now the new fastest + if (balloon[distanceTravelled] > maxDist) { + location = balloonLocation; + maxDist = balloon[distanceTravelled]; + } + } + } + return location; +} + +function handleProjectiles() { + for (var i = 0; i < towers.length; i++) { + var location = towers[i]; + var data = towerData[i]; + data[cooldownRemaining]--; + var balloon = track(location, data[towerVision]); + if (data[projectileType] === laser) + balloon = new p5.Vector(mouseX, mouseY); + // Cooldown is 0 and there is a balloon that the tower tracks shoots a projectile + if (data[cooldownRemaining] <= 0 && balloon !== undefined) { + // Resets the cooldown + data[cooldownRemaining] = data[maxCooldown]; + var toMouse = new p5.Vector(balloon.x - location.x, balloon.y - location.y); + if (data[projectileType] === 0) { + var speed = 24, damage = 4, pierce = 1, maxTravelDist = 500, thickness = 4; + var unitVector = p5.Vector.div(toMouse, toMouse.mag()); + var ve = p5.Vector.mult(unitVector, speed); + createProjectile(location, ve, damage, pierce, maxTravelDist, thickness, def); + // Default type + } else if (data[projectileType] === 1) { + // Spread in 8 + for (var j = 0; j < 8; j++) { + var speed = 12, damage = 3, pierce = 2, maxTravelDist = 150, thickness = 4; + var angle = (PI * 2) * j / 8; + var unitVector = p5.Vector.div(toMouse, toMouse.mag()); + var ve = p5.Vector.mult(unitVector, speed).rotate(angle); + createProjectile(location, ve, damage, pierce, maxTravelDist, thickness, eight); + } + } else if (data[projectileType] === 2) { + // glue gunner - slows balloons + // slow-ish speed, low damage, high pierce, low range + var speed = 15, damage = 1, pierce = 7, maxTravelDist = 220, thickness = 4; + var unitVector = p5.Vector.div(toMouse, toMouse.mag()); + var ve = p5.Vector.mult(unitVector, speed); + createProjectile(location, ve, damage, pierce, maxTravelDist, thickness, slow); + } else if (data[projectileType] === 3) { + // speed & travel dist are custom, maxTravelDist basically acts like a counter + var speed = 1, pierce = 50, maxTravelDist = data[maxCooldown], thickness = 32; + var damage = 0.10; + var unitVector = p5.Vector.div(toMouse, toMouse.mag()); + var ve = p5.Vector.mult(unitVector, speed); + createProjectile(location, ve, damage, pierce, maxTravelDist, thickness, laser); + } + } + } + var deadBalloons = []; + for (var i = 0; i < projectileData.length; i++) { + drawProjectile(i); + if (dead(i)) { + deadBalloons.push(i); + } + } + const indexSet = new Set(deadBalloons); + projectileData = projectileData.filter((value, i) => !indexSet.has(i)) + center = center.filter((value, i) => !indexSet.has(i)) + velocity = velocity.filter((value, i) => !indexSet.has(i)) + balloonsHit = balloonsHit.filter((value, i) => !indexSet.has(i)) +} + +var cooldownRemaining, maxCooldown, towerVision, projectileType; + +var def, eight, slow, laser; + +var towerCount; + +var towerData; + +var towerVisions; + +/* +Encompasses: Displaying Towers & Tower Data (for projectiles) +*/ +function makeTowerData(towerID) { + if (towerID === def) { + return [ // Cooldown between next projectile + 10, // Max cooldown + 10, // Tower Vision + towerVisions[def], // Projectile ID + 0]; + } else if (towerID === eight) { + return [ // Cooldown between next projectile + 25, // Max cooldown + 25, // Tower Vision + towerVisions[eight], // Projectile ID + 1]; + } else if (towerID === slow) { + return [35, 35, // Tower Vision + towerVisions[slow], 2]; + } else if (towerID === laser) { + return [1, 1, towerVisions[laser], 3]; + } + // filler since we need to return something + return []; +} + +// -------------------------------------------------- +// Draw a simple tower at a specified location +function drawTowerIcon(xPos, yPos, colour) { + strokeWeight(0); + stroke(0); + fill(colour); + rectMode(CENTER); + // Draw a simple rectangle as the tower + rect(xPos, yPos, towerSize, towerSize); +} + +// Draws a tower that rotates to face the targetLocation +function drawTowerWithRotation(xPos, yPos, colour, targetLocation) { + push(); + translate(xPos, yPos); + // Angle calculation + var slope = (targetLocation.y - yPos) / (targetLocation.x - xPos); + var angle = atan(slope); + rotate(angle); + strokeWeight(0); + fill(colour); + rectMode(CENTER); + // Draw a simple rectangle as the tower + rect(0, 0, towerSize, towerSize); + pop(); +} + +function drawAllTowers() { + for (var i = 0; i < towers.length; i++) { + var xPos = towers[i].x, yPos = towers[i].y; + var data = towerData[i]; + var towerType = data[projectileType]; + var track1 = track(towers[i], data[towerVision]); + if (data[projectileType] === laser) + track1 = new p5.Vector(mouseX, mouseY); + if (track1 === undefined) { + drawTowerIcon(xPos, yPos, towerColours[towerType]); + } else { + drawTowerWithRotation(xPos, yPos, towerColours[towerType], new p5.Vector(track1.x, track1.y)); + } + if (pointRectCollision(mouseX, mouseY, xPos, yPos, towerSize)) { + // Drawing the tower range visually + fill(127, 80); + stroke(127); + strokeWeight(4); + ellipseMode(RADIUS); + ellipse(xPos, yPos, data[towerVision], data[towerVision]); + } + fill(color(0x4C, 0x67, 0x10)); + strokeWeight(0); + textSize(12); + text("Tower " + (i + 1), xPos - 30, yPos - 20); + } +} + +function drawSelectedTowers() { + // Note that more than one tower can be dragged at a time + for (var towerID = 0; towerID < towerCount; towerID++) { + if (held[towerID]) { + var location = dragAndDropLocations[towerID]; + if (!legalDrop(towerID)) { + drawTowerIcon(location.x, location.y, color(0xFF, 0x00, 0x00)); + } else { + drawTowerIcon(location.x, location.y, towerColours[towerID]); + } + // Drawing the tower range of the selected towers + fill(127, 80); + stroke(127); + strokeWeight(4); + ellipseMode(RADIUS); + ellipse(location.x, location.y, towerVisions[towerID], towerVisions[towerID]); + } + } + // Draws the default towers + for (var towerType = 0; towerType < towerCount; towerType++) { + var location = originalLocations[towerType]; + if (attemptingToPurchaseTowerWithoutFunds(towerType)) { + drawTowerIcon(location.x, location.y, towerErrorColour); + } else + drawTowerIcon(location.x, location.y, towerColours[towerType]); + fill(255); + textSize(14); + var textOffsetX = -15, textOffsetY = 26; + // displays the prices of towers + text("$" + towerPrice[towerType], location.x + textOffsetX, location.y + textOffsetY); + } +} + +function initializeFields() { + balloons = []; + distanceTravelled = 0; + delay = 1; + speed = 2; + hp = 3; + slowed = 4; + ID = 5; + balloonRadius = 25; + maxBalloonHP = 50; + health = 11; + currentBalance = 750; + rewardPerBalloon = 15; + currentlyDragging = -1; + notDragging = -1; + difX = 0; + difY = 0; + count = 0; + held = [false, false, false, false]; + towerPrice = [100, 200, 200, 400]; + towerColours = [color(0x7b, 0x9d, 0x32), color(0xF0, 0x98, 0xD7), color(0x82, 0xE5, 0xF7), color(0xEA, 0x0C, 0x0C)]; + originalLocations = [new p5.Vector(600, 50), new p5.Vector(650, 50), new p5.Vector(700, 50), new p5.Vector(750, 50)]; + dragAndDropLocations = [new p5.Vector(600, 50), new p5.Vector(650, 50), new p5.Vector(700, 50), new p5.Vector(750, 50)]; + towers = null; + towerSize = 25; + towerErrorColour = color(0xE3, 0x07, 0x07); + trashX1 = 0; + trashY1 = 0; + trashX2 = 0; + trashY2 = 0; + framesSinceLost = 0; + points = []; + PATH_RADIUS = 20; + dp = []; + center = []; + velocity = []; + projectileData = []; + balloonsHit = []; + damage = 0; + pierce = 1; + angle = 2; + currDistTravelled = 3; + maxDistTravelled = 4; + thickness = 5; + dmgType = 6; + projectileRadius = 11; + cooldownRemaining = 0; + maxCooldown = 1; + towerVision = 2; + projectileType = 3; + def = 0; + eight = 1; + slow = 2; + laser = 3; + towerCount = 4; + towerData = null; + towerVisions = [200, 100, 100, 300]; +} + +function preload() { +// TODO: put method calls that load from files into this method + heart = loadImage("https://raw.githubusercontent.com/mcpt/game-dev/main/PartOne/data/heart.png"); +// I found the following calls that you should move here: +// - on line 161: heart = loadImage("heart.png") +// (note that line numbers are from your Processing code) +} + diff --git a/layouts/shortcodes/p5js-src.html b/layouts/shortcodes/p5js-src.html new file mode 100644 index 00000000..01699cdd --- /dev/null +++ b/layouts/shortcodes/p5js-src.html @@ -0,0 +1,20 @@ +{{$id := "unset"}} +{{with .Get "id"}} +{{$id = .}} +{{else}} +{{$id = printf "%s%d" (delimit (shuffle (seq 1 9)) "") now.UnixNano}} +{{end}} + +
+ diff --git a/layouts/shortcodes/p5js-src.md b/layouts/shortcodes/p5js-src.md new file mode 100644 index 00000000..e69de29b diff --git a/layouts/shortcodes/p5js.md b/layouts/shortcodes/p5js.md new file mode 100644 index 00000000..e69de29b diff --git a/static/img/Function.png b/static/img/Function.png new file mode 100644 index 00000000..989fbc58 Binary files /dev/null and b/static/img/Function.png differ diff --git a/static/js/p5.sound.min.js b/static/js/p5.sound.min.js deleted file mode 100644 index 44f25231..00000000 --- a/static/js/p5.sound.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/** [p5.sound] Version: 1.0.1 - 2021-05-25 */ - !function(n){var i={};function r(t){if(i[t])return i[t].exports;var e=i[t]={i:t,l:!1,exports:{}};return n[t].call(e.exports,e,e.exports,r),e.l=!0,e.exports}r.m=n,r.c=i,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)r.d(n,i,function(t){return e[t]}.bind(null,i));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=40)}([function(t,e,n){var i;void 0===(i=function(){"use strict";function l(t,e){this.isUndef(t)||1===t?this.input=this.context.createGain():1t)this.cancelScheduledValues(t),this.linearRampToValueAtTime(e,t);else{var i=this._searchAfter(t);i&&(this.cancelScheduledValues(t),i.type===u.TimelineSignal.Type.Linear?this.linearRampToValueAtTime(e,t):i.type===u.TimelineSignal.Type.Exponential&&this.exponentialRampToValueAtTime(e,t)),this.setValueAtTime(e,t)}return this},u.TimelineSignal.prototype.linearRampToValueBetween=function(t,e,n){return this.setRampPoint(e),this.linearRampToValueAtTime(t,n),this},u.TimelineSignal.prototype.exponentialRampToValueBetween=function(t,e,n){return this.setRampPoint(e),this.exponentialRampToValueAtTime(t,n),this},u.TimelineSignal.prototype._searchBefore=function(t){return this._events.get(t)},u.TimelineSignal.prototype._searchAfter=function(t){return this._events.getAfter(t)},u.TimelineSignal.prototype.getValueAtTime=function(t){t=this.toSeconds(t);var e=this._searchAfter(t),n=this._searchBefore(t),i=this._initial;if(null===n)i=this._initial;else if(n.type===u.TimelineSignal.Type.Target){var r,o=this._events.getBefore(n.time);r=null===o?this._initial:o.value,i=this._exponentialApproach(n.time,r,n.value,n.constant,t)}else i=n.type===u.TimelineSignal.Type.Curve?this._curveInterpolate(n.time,n.value,n.duration,t):null===e?n.value:e.type===u.TimelineSignal.Type.Linear?this._linearInterpolate(n.time,n.value,e.time,e.value,t):e.type===u.TimelineSignal.Type.Exponential?this._exponentialInterpolate(n.time,n.value,e.time,e.value,t):n.value;return i},u.TimelineSignal.prototype.connect=u.SignalBase.prototype.connect,u.TimelineSignal.prototype._exponentialApproach=function(t,e,n,i,r){return n+(e-n)*Math.exp(-(r-t)/i)},u.TimelineSignal.prototype._linearInterpolate=function(t,e,n,i,r){return e+(r-t)/(n-t)*(i-e)},u.TimelineSignal.prototype._exponentialInterpolate=function(t,e,n,i,r){return(e=Math.max(this._minOutput,e))*Math.pow(i/e,(r-t)/(n-t))},u.TimelineSignal.prototype._curveInterpolate=function(t,e,n,i){var r=e.length;if(t+n<=i)return e[r-1];if(i<=t)return e[0];var o=(i-t)/n,s=Math.floor((r-1)*o),a=Math.ceil((r-1)*o),u=e[s],c=e[a];return a===s?u:this._linearInterpolate(s,u,a,c,o*(r-1))},u.TimelineSignal.prototype.dispose=function(){u.Signal.prototype.dispose.call(this),u.Param.prototype.dispose.call(this),this._events.dispose(),this._events=null},u.TimelineSignal}.apply(e,i))||(t.exports=r)},function(t,e,n){var i,r;i=[n(0),n(4),n(1),n(2)],void 0===(r=function(n){"use strict";return n.Scale=function(t,e){this._outputMin=this.defaultArg(t,0),this._outputMax=this.defaultArg(e,1),this._scale=this.input=new n.Multiply(1),this._add=this.output=new n.Add(0),this._scale.connect(this._add),this._setRange()},n.extend(n.Scale,n.SignalBase),Object.defineProperty(n.Scale.prototype,"min",{get:function(){return this._outputMin},set:function(t){this._outputMin=t,this._setRange()}}),Object.defineProperty(n.Scale.prototype,"max",{get:function(){return this._outputMax},set:function(t){this._outputMax=t,this._setRange()}}),n.Scale.prototype._setRange=function(){this._add.value=this._outputMin,this._scale.value=this._outputMax-this._outputMin},n.Scale.prototype.dispose=function(){return n.prototype.dispose.call(this),this._add.dispose(),this._add=null,this._scale.dispose(),this._scale=null,this},n.Scale}.apply(e,i))||(t.exports=r)},function(t,e,n){var i,r;i=[n(0),n(16),n(30),n(31),n(12)],void 0===(r=function(e){return e.Type={Default:"number",Time:"time",Frequency:"frequency",TransportTime:"transportTime",Ticks:"ticks",NormalRange:"normalRange",AudioRange:"audioRange",Decibels:"db",Interval:"interval",BPM:"bpm",Positive:"positive",Cents:"cents",Degrees:"degrees",MIDI:"midi",BarsBeatsSixteenths:"barsBeatsSixteenths",Samples:"samples",Hertz:"hertz",Note:"note",Milliseconds:"milliseconds",Seconds:"seconds",Notation:"notation"},e.prototype.toSeconds=function(t){return this.isNumber(t)?t:this.isUndef(t)?this.now():this.isString(t)?new e.Time(t).toSeconds():t instanceof e.TimeBase?t.toSeconds():void 0},e.prototype.toFrequency=function(t){return this.isNumber(t)?t:this.isString(t)||this.isUndef(t)?new e.Frequency(t).valueOf():t instanceof e.TimeBase?t.toFrequency():void 0},e.prototype.toTicks=function(t){return this.isNumber(t)||this.isString(t)?new e.TransportTime(t).toTicks():this.isUndef(t)?e.Transport.ticks:t instanceof e.TimeBase?t.toTicks():void 0},e}.apply(e,i))||(t.exports=r)},function(t,e,n){var i,r;i=[n(0),n(18),n(9)],void 0===(r=function(n){"use strict";return window.GainNode&&!AudioContext.prototype.createGain&&(AudioContext.prototype.createGain=AudioContext.prototype.createGainNode),n.Gain=function(){var t=this.optionsObject(arguments,["gain","units"],n.Gain.defaults);this.input=this.output=this._gainNode=this.context.createGain(),this.gain=new n.Param({param:this._gainNode.gain,units:t.units,value:t.gain,convert:t.convert}),this._readOnly("gain")},n.extend(n.Gain),n.Gain.defaults={gain:1,convert:!0},n.Gain.prototype.dispose=function(){n.Param.prototype.dispose.call(this),this._gainNode.disconnect(),this._gainNode=null,this._writable("gain"),this.gain.dispose(),this.gain=null},n.prototype.createInsOuts=function(t,e){1===t?this.input=new n.Gain:1this._nextTick&&this._state;){var e=this._state.getValueAtTime(this._nextTick);if(e!==this._lastState){this._lastState=e;var n=this._state.get(this._nextTick);e===r.State.Started?(this._nextTick=n.time,this.isUndef(n.offset)||(this.ticks=n.offset),this.emit("start",n.time,this.ticks)):e===r.State.Stopped?(this.ticks=0,this.emit("stop",n.time)):e===r.State.Paused&&this.emit("pause",n.time)}var i=this._nextTick;this.frequency&&(this._nextTick+=1/this.frequency.getValueAtTime(this._nextTick),e===r.State.Started&&(this.callback(i),this.ticks++))}},r.Clock.prototype.getStateAtTime=function(t){return t=this.toSeconds(t),this._state.getValueAtTime(t)},r.Clock.prototype.dispose=function(){r.Emitter.prototype.dispose.call(this),this.context.off("tick",this._boundLoop),this._writable("frequency"),this.frequency.dispose(),this.frequency=null,this._boundLoop=null,this._nextTick=1/0,this.callback=null,this._state.dispose(),this._state=null},r.Clock}.apply(e,i))||(t.exports=r)},function(t,e,n){var i,r;i=[n(0),n(14)],void 0===(r=function(i){function t(t,e,n){if(t.input)Array.isArray(t.input)?(i.prototype.isUndef(n)&&(n=0),this.connect(t.input[n])):this.connect(t.input,e,n);else try{t instanceof AudioNode?r.call(this,t,e,n):r.call(this,t,e)}catch(e){throw new Error("error connecting to node: "+t+"\n"+e)}}var r,o;return!window.hasOwnProperty("AudioContext")&&window.hasOwnProperty("webkitAudioContext")&&(window.AudioContext=window.webkitAudioContext),i.Context=function(t){for(var e in i.Emitter.call(this),t=t||new window.AudioContext,this._context=t,this._context)this._defineProperty(this._context,e);this._latencyHint="interactive",this._lookAhead=.1,this._updateInterval=this._lookAhead/3,this._computedUpdateInterval=0,this._worker=this._createWorker(),this._constants={}},i.extend(i.Context,i.Emitter),i.Emitter.mixin(i.Context),i.Context.prototype._defineProperty=function(e,n){this.isUndef(this[n])&&Object.defineProperty(this,n,{get:function(){return"function"==typeof e[n]?e[n].bind(e):e[n]},set:function(t){e[n]=t}})},i.Context.prototype.now=function(){return this._context.currentTime},i.Context.prototype._createWorker=function(){window.URL=window.URL||window.webkitURL;var t=new Blob(["var timeoutTime = "+(1e3*this._updateInterval).toFixed(1)+";self.onmessage = function(msg){\ttimeoutTime = parseInt(msg.data);};function tick(){\tsetTimeout(tick, timeoutTime);\tself.postMessage('tick');}tick();"]),e=URL.createObjectURL(t),n=new Worker(e);return n.addEventListener("message",function(){this.emit("tick")}.bind(this)),n.addEventListener("message",function(){var t=this.now();if(this.isNumber(this._lastUpdate)){var e=t-this._lastUpdate;this._computedUpdateInterval=Math.max(e,.97*this._computedUpdateInterval)}this._lastUpdate=t}.bind(this)),n},i.Context.prototype.getConstant=function(t){if(this._constants[t])return this._constants[t];for(var e=this._context.createBuffer(1,128,this._context.sampleRate),n=e.getChannelData(0),i=0;ithis.memory){var n=this.length-this.memory;this._timeline.splice(0,n)}return this},e.Timeline.prototype.remove=function(t){if(this._iterating)this._toRemove.push(t);else{var e=this._timeline.indexOf(t);-1!==e&&this._timeline.splice(e,1)}return this},e.Timeline.prototype.get=function(t){var e=this._search(t);return-1!==e?this._timeline[e]:null},e.Timeline.prototype.peek=function(){return this._timeline[0]},e.Timeline.prototype.shift=function(){return this._timeline.shift()},e.Timeline.prototype.getAfter=function(t){var e=this._search(t);return e+1=t&&(this._timeline=[]);return this},e.Timeline.prototype.cancelBefore=function(t){if(this._timeline.length){var e=this._search(t);0<=e&&(this._timeline=this._timeline.slice(e+1))}return this},e.Timeline.prototype._search=function(t){var e=0,n=this._timeline.length,i=n;if(0t)return r;o.time>t?i=r:o.time=t;)n--;return this._iterate(e,n+1),this},e.Timeline.prototype.forEachAtTime=function(e,n){var t=this._search(e);return-1!==t&&this._iterate(function(t){t.time===e&&n(t)},0,t),this},e.Timeline.prototype.dispose=function(){e.prototype.dispose.call(this),this._timeline=null,this._toRemove=null},e.Timeline}.apply(e,i))||(t.exports=r)},function(t,e,n){var i,r;i=[n(0),n(1),n(2)],void 0===(r=function(t){"use strict";return t.Negate=function(){this._multiply=this.input=this.output=new t.Multiply(-1)},t.extend(t.Negate,t.SignalBase),t.Negate.prototype.dispose=function(){return t.prototype.dispose.call(this),this._multiply.dispose(),this._multiply=null,this},t.Negate}.apply(e,i))||(t.exports=r)},function(t,e,n){var i,r;i=[n(0),n(2),n(1),n(6)],void 0===(r=function(t){"use strict";return t.GreaterThanZero=function(){this._thresh=this.output=new t.WaveShaper(function(t){return t<=0?0:1},127),this._scale=this.input=new t.Multiply(1e4),this._scale.connect(this._thresh)},t.extend(t.GreaterThanZero,t.SignalBase),t.GreaterThanZero.prototype.dispose=function(){return t.prototype.dispose.call(this),this._scale.dispose(),this._scale=null,this._thresh.dispose(),this._thresh=null,this},t.GreaterThanZero}.apply(e,i))||(t.exports=r)},function(t,e,n){var i,r,o;r=[],void 0===(o="function"==typeof(i=function(){var s=function(t,e){this._dragged=!1,this._element=t,this._bindedMove=this._moved.bind(this),this._bindedEnd=this._ended.bind(this,e),t.addEventListener("touchstart",this._bindedEnd),t.addEventListener("touchmove",this._bindedMove),t.addEventListener("touchend",this._bindedEnd),t.addEventListener("mouseup",this._bindedEnd)};function o(t){return"running"===t.state}return s.prototype._moved=function(t){this._dragged=!0},s.prototype._ended=function(t){this._dragged||function(t){var e=t.createBuffer(1,1,t.sampleRate),n=t.createBufferSource();n.buffer=e,n.connect(t.destination),n.start(0),t.resume&&t.resume()}(t),this._dragged=!1},s.prototype.dispose=function(){this._element.removeEventListener("touchstart",this._bindedEnd),this._element.removeEventListener("touchmove",this._bindedMove),this._element.removeEventListener("touchend",this._bindedEnd),this._element.removeEventListener("mouseup",this._bindedEnd),this._bindedMove=null,this._bindedEnd=null,this._element=null},function(e,t,n){var i=new Promise(function(t){!function(e,n){o(e)?n():function t(){o(e)?n():(requestAnimationFrame(t),e.resume&&e.resume())}()}(e,t)}),r=[];return function t(e,n,i){if(Array.isArray(e)||NodeList&&e instanceof NodeList)for(var r=0;r= this._length) {\n this._writeIndex = 0;\n } // For excessive frames, the buffer will be overwritten.\n\n\n this._framesAvailable += sourceLength;\n\n if (this._framesAvailable > this._length) {\n this._framesAvailable = this._length;\n }\n }\n /**\n * Pull data out of buffer and fill a given sequence of Float32Arrays.\n *\n * @param {array} arraySequence An array of Float32Arrays.\n */\n\n }, {\n key: "pull",\n value: function pull(arraySequence) {\n // The channel count of arraySequence and the length of each channel must\n // match with this buffer obejct.\n // If the FIFO is completely empty, do nothing.\n if (this._framesAvailable === 0) {\n return;\n }\n\n var destinationLength = arraySequence[0].length; // Transfer data from the internal buffer to the |arraySequence| storage.\n\n for (var i = 0; i < destinationLength; ++i) {\n var readIndex = (this._readIndex + i) % this._length;\n\n for (var channel = 0; channel < this._channelCount; ++channel) {\n arraySequence[channel][i] = this._channelData[channel][readIndex];\n }\n }\n\n this._readIndex += destinationLength;\n\n if (this._readIndex >= this._length) {\n this._readIndex = 0;\n }\n\n this._framesAvailable -= destinationLength;\n\n if (this._framesAvailable < 0) {\n this._framesAvailable = 0;\n }\n }\n }, {\n key: "framesAvailable",\n get: function get() {\n return this._framesAvailable;\n }\n }]);\n\n return RingBuffer;\n }()\n}["default"];\n\nvar RecorderProcessor =\n/*#__PURE__*/\nfunction (_AudioWorkletProcesso) {\n _inherits(RecorderProcessor, _AudioWorkletProcesso);\n\n function RecorderProcessor(options) {\n var _this;\n\n _classCallCheck(this, RecorderProcessor);\n\n _this = _possibleConstructorReturn(this, _getPrototypeOf(RecorderProcessor).call(this));\n var processorOptions = options.processorOptions || {};\n _this.numOutputChannels = options.outputChannelCount || 2;\n _this.numInputChannels = processorOptions.numInputChannels || 2;\n _this.bufferSize = processorOptions.bufferSize || 1024;\n _this.recording = false;\n\n _this.clear();\n\n _this.port.onmessage = function (event) {\n var data = event.data;\n\n if (data.name === \'start\') {\n _this.record(data.duration);\n } else if (data.name === \'stop\') {\n _this.stop();\n }\n };\n\n return _this;\n }\n\n _createClass(RecorderProcessor, [{\n key: "process",\n value: function process(inputs) {\n if (!this.recording) {\n return true;\n } else if (this.sampleLimit && this.recordedSamples >= this.sampleLimit) {\n this.stop();\n return true;\n }\n\n var input = inputs[0];\n this.inputRingBuffer.push(input);\n\n if (this.inputRingBuffer.framesAvailable >= this.bufferSize) {\n this.inputRingBuffer.pull(this.inputRingBufferArraySequence);\n\n for (var channel = 0; channel < this.numOutputChannels; ++channel) {\n var inputChannelCopy = this.inputRingBufferArraySequence[channel].slice();\n\n if (channel === 0) {\n this.leftBuffers.push(inputChannelCopy);\n\n if (this.numInputChannels === 1) {\n this.rightBuffers.push(inputChannelCopy);\n }\n } else if (channel === 1 && this.numInputChannels > 1) {\n this.rightBuffers.push(inputChannelCopy);\n }\n }\n\n this.recordedSamples += this.bufferSize;\n }\n\n return true;\n }\n }, {\n key: "record",\n value: function record(duration) {\n if (duration) {\n this.sampleLimit = Math.round(duration * sampleRate);\n }\n\n this.recording = true;\n }\n }, {\n key: "stop",\n value: function stop() {\n this.recording = false;\n var buffers = this.getBuffers();\n var leftBuffer = buffers[0].buffer;\n var rightBuffer = buffers[1].buffer;\n this.port.postMessage({\n name: \'buffers\',\n leftBuffer: leftBuffer,\n rightBuffer: rightBuffer\n }, [leftBuffer, rightBuffer]);\n this.clear();\n }\n }, {\n key: "getBuffers",\n value: function getBuffers() {\n var buffers = [];\n buffers.push(this.mergeBuffers(this.leftBuffers));\n buffers.push(this.mergeBuffers(this.rightBuffers));\n return buffers;\n }\n }, {\n key: "mergeBuffers",\n value: function mergeBuffers(channelBuffer) {\n var result = new Float32Array(this.recordedSamples);\n var offset = 0;\n var lng = channelBuffer.length;\n\n for (var i = 0; i < lng; i++) {\n var buffer = channelBuffer[i];\n result.set(buffer, offset);\n offset += buffer.length;\n }\n\n return result;\n }\n }, {\n key: "clear",\n value: function clear() {\n var _this2 = this;\n\n this.leftBuffers = [];\n this.rightBuffers = [];\n this.inputRingBuffer = new RingBuffer(this.bufferSize, this.numInputChannels);\n this.inputRingBufferArraySequence = new Array(this.numInputChannels).fill(null).map(function () {\n return new Float32Array(_this2.bufferSize);\n });\n this.recordedSamples = 0;\n this.sampleLimit = null;\n }\n }]);\n\n return RecorderProcessor;\n}(_wrapNativeSuper(AudioWorkletProcessor));\n\nregisterProcessor(processorNames.recorderProcessor, RecorderProcessor);'},function(t,e,n){"use strict";n.r(e),e.default='function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn\'t been initialised - super() hasn\'t been called"); } return self; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); }\n\nfunction isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _construct(Parent, args, Class) { if (isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); }\n\nfunction _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n// import dependencies via preval.require so that they\'re available as values at compile time\nvar processorNames = {\n "recorderProcessor": "recorder-processor",\n "soundFileProcessor": "sound-file-processor",\n "amplitudeProcessor": "amplitude-processor"\n};\nvar RingBuffer = {\n "default":\n /*#__PURE__*/\n function () {\n /**\n * @constructor\n * @param {number} length Buffer length in frames.\n * @param {number} channelCount Buffer channel count.\n */\n function RingBuffer(length, channelCount) {\n _classCallCheck(this, RingBuffer);\n\n this._readIndex = 0;\n this._writeIndex = 0;\n this._framesAvailable = 0;\n this._channelCount = channelCount;\n this._length = length;\n this._channelData = [];\n\n for (var i = 0; i < this._channelCount; ++i) {\n this._channelData[i] = new Float32Array(length);\n }\n }\n /**\n * Getter for Available frames in buffer.\n *\n * @return {number} Available frames in buffer.\n */\n\n\n _createClass(RingBuffer, [{\n key: "push",\n\n /**\n * Push a sequence of Float32Arrays to buffer.\n *\n * @param {array} arraySequence A sequence of Float32Arrays.\n */\n value: function push(arraySequence) {\n // The channel count of arraySequence and the length of each channel must\n // match with this buffer obejct.\n // Transfer data from the |arraySequence| storage to the internal buffer.\n var sourceLength = arraySequence[0] ? arraySequence[0].length : 0;\n\n for (var i = 0; i < sourceLength; ++i) {\n var writeIndex = (this._writeIndex + i) % this._length;\n\n for (var channel = 0; channel < this._channelCount; ++channel) {\n this._channelData[channel][writeIndex] = arraySequence[channel][i];\n }\n }\n\n this._writeIndex += sourceLength;\n\n if (this._writeIndex >= this._length) {\n this._writeIndex = 0;\n } // For excessive frames, the buffer will be overwritten.\n\n\n this._framesAvailable += sourceLength;\n\n if (this._framesAvailable > this._length) {\n this._framesAvailable = this._length;\n }\n }\n /**\n * Pull data out of buffer and fill a given sequence of Float32Arrays.\n *\n * @param {array} arraySequence An array of Float32Arrays.\n */\n\n }, {\n key: "pull",\n value: function pull(arraySequence) {\n // The channel count of arraySequence and the length of each channel must\n // match with this buffer obejct.\n // If the FIFO is completely empty, do nothing.\n if (this._framesAvailable === 0) {\n return;\n }\n\n var destinationLength = arraySequence[0].length; // Transfer data from the internal buffer to the |arraySequence| storage.\n\n for (var i = 0; i < destinationLength; ++i) {\n var readIndex = (this._readIndex + i) % this._length;\n\n for (var channel = 0; channel < this._channelCount; ++channel) {\n arraySequence[channel][i] = this._channelData[channel][readIndex];\n }\n }\n\n this._readIndex += destinationLength;\n\n if (this._readIndex >= this._length) {\n this._readIndex = 0;\n }\n\n this._framesAvailable -= destinationLength;\n\n if (this._framesAvailable < 0) {\n this._framesAvailable = 0;\n }\n }\n }, {\n key: "framesAvailable",\n get: function get() {\n return this._framesAvailable;\n }\n }]);\n\n return RingBuffer;\n }()\n}["default"];\n\nvar SoundFileProcessor =\n/*#__PURE__*/\nfunction (_AudioWorkletProcesso) {\n _inherits(SoundFileProcessor, _AudioWorkletProcesso);\n\n function SoundFileProcessor(options) {\n var _this;\n\n _classCallCheck(this, SoundFileProcessor);\n\n _this = _possibleConstructorReturn(this, _getPrototypeOf(SoundFileProcessor).call(this));\n var processorOptions = options.processorOptions || {};\n _this.bufferSize = processorOptions.bufferSize || 256;\n _this.inputRingBuffer = new RingBuffer(_this.bufferSize, 1);\n _this.inputRingBufferArraySequence = [new Float32Array(_this.bufferSize)];\n return _this;\n }\n\n _createClass(SoundFileProcessor, [{\n key: "process",\n value: function process(inputs) {\n var input = inputs[0]; // we only care about the first input channel, because that contains the position data\n\n this.inputRingBuffer.push([input[0]]);\n\n if (this.inputRingBuffer.framesAvailable >= this.bufferSize) {\n this.inputRingBuffer.pull(this.inputRingBufferArraySequence);\n var inputChannel = this.inputRingBufferArraySequence[0];\n var position = inputChannel[inputChannel.length - 1] || 0;\n this.port.postMessage({\n name: \'position\',\n position: position\n });\n }\n\n return true;\n }\n }]);\n\n return SoundFileProcessor;\n}(_wrapNativeSuper(AudioWorkletProcessor));\n\nregisterProcessor(processorNames.soundFileProcessor, SoundFileProcessor);'},function(t,e,n){"use strict";n.r(e),e.default='function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn\'t been initialised - super() hasn\'t been called"); } return self; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _wrapNativeSuper(Class) { var _cache = typeof Map === "function" ? new Map() : undefined; _wrapNativeSuper = function _wrapNativeSuper(Class) { if (Class === null || !_isNativeFunction(Class)) return Class; if (typeof Class !== "function") { throw new TypeError("Super expression must either be null or a function"); } if (typeof _cache !== "undefined") { if (_cache.has(Class)) return _cache.get(Class); _cache.set(Class, Wrapper); } function Wrapper() { return _construct(Class, arguments, _getPrototypeOf(this).constructor); } Wrapper.prototype = Object.create(Class.prototype, { constructor: { value: Wrapper, enumerable: false, writable: true, configurable: true } }); return _setPrototypeOf(Wrapper, Class); }; return _wrapNativeSuper(Class); }\n\nfunction isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _construct(Parent, args, Class) { if (isNativeReflectConstruct()) { _construct = Reflect.construct; } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); }\n\nfunction _isNativeFunction(fn) { return Function.toString.call(fn).indexOf("[native code]") !== -1; }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\n// import dependencies via preval.require so that they\'re available as values at compile time\nvar processorNames = {\n "recorderProcessor": "recorder-processor",\n "soundFileProcessor": "sound-file-processor",\n "amplitudeProcessor": "amplitude-processor"\n};\nvar RingBuffer = {\n "default":\n /*#__PURE__*/\n function () {\n /**\n * @constructor\n * @param {number} length Buffer length in frames.\n * @param {number} channelCount Buffer channel count.\n */\n function RingBuffer(length, channelCount) {\n _classCallCheck(this, RingBuffer);\n\n this._readIndex = 0;\n this._writeIndex = 0;\n this._framesAvailable = 0;\n this._channelCount = channelCount;\n this._length = length;\n this._channelData = [];\n\n for (var i = 0; i < this._channelCount; ++i) {\n this._channelData[i] = new Float32Array(length);\n }\n }\n /**\n * Getter for Available frames in buffer.\n *\n * @return {number} Available frames in buffer.\n */\n\n\n _createClass(RingBuffer, [{\n key: "push",\n\n /**\n * Push a sequence of Float32Arrays to buffer.\n *\n * @param {array} arraySequence A sequence of Float32Arrays.\n */\n value: function push(arraySequence) {\n // The channel count of arraySequence and the length of each channel must\n // match with this buffer obejct.\n // Transfer data from the |arraySequence| storage to the internal buffer.\n var sourceLength = arraySequence[0] ? arraySequence[0].length : 0;\n\n for (var i = 0; i < sourceLength; ++i) {\n var writeIndex = (this._writeIndex + i) % this._length;\n\n for (var channel = 0; channel < this._channelCount; ++channel) {\n this._channelData[channel][writeIndex] = arraySequence[channel][i];\n }\n }\n\n this._writeIndex += sourceLength;\n\n if (this._writeIndex >= this._length) {\n this._writeIndex = 0;\n } // For excessive frames, the buffer will be overwritten.\n\n\n this._framesAvailable += sourceLength;\n\n if (this._framesAvailable > this._length) {\n this._framesAvailable = this._length;\n }\n }\n /**\n * Pull data out of buffer and fill a given sequence of Float32Arrays.\n *\n * @param {array} arraySequence An array of Float32Arrays.\n */\n\n }, {\n key: "pull",\n value: function pull(arraySequence) {\n // The channel count of arraySequence and the length of each channel must\n // match with this buffer obejct.\n // If the FIFO is completely empty, do nothing.\n if (this._framesAvailable === 0) {\n return;\n }\n\n var destinationLength = arraySequence[0].length; // Transfer data from the internal buffer to the |arraySequence| storage.\n\n for (var i = 0; i < destinationLength; ++i) {\n var readIndex = (this._readIndex + i) % this._length;\n\n for (var channel = 0; channel < this._channelCount; ++channel) {\n arraySequence[channel][i] = this._channelData[channel][readIndex];\n }\n }\n\n this._readIndex += destinationLength;\n\n if (this._readIndex >= this._length) {\n this._readIndex = 0;\n }\n\n this._framesAvailable -= destinationLength;\n\n if (this._framesAvailable < 0) {\n this._framesAvailable = 0;\n }\n }\n }, {\n key: "framesAvailable",\n get: function get() {\n return this._framesAvailable;\n }\n }]);\n\n return RingBuffer;\n }()\n}["default"];\n\nvar AmplitudeProcessor =\n/*#__PURE__*/\nfunction (_AudioWorkletProcesso) {\n _inherits(AmplitudeProcessor, _AudioWorkletProcesso);\n\n function AmplitudeProcessor(options) {\n var _this;\n\n _classCallCheck(this, AmplitudeProcessor);\n\n _this = _possibleConstructorReturn(this, _getPrototypeOf(AmplitudeProcessor).call(this));\n var processorOptions = options.processorOptions || {};\n _this.numOutputChannels = options.outputChannelCount || 1;\n _this.numInputChannels = processorOptions.numInputChannels || 2;\n _this.normalize = processorOptions.normalize || false;\n _this.smoothing = processorOptions.smoothing || 0;\n _this.bufferSize = processorOptions.bufferSize || 2048;\n _this.inputRingBuffer = new RingBuffer(_this.bufferSize, _this.numInputChannels);\n _this.outputRingBuffer = new RingBuffer(_this.bufferSize, _this.numOutputChannels);\n _this.inputRingBufferArraySequence = new Array(_this.numInputChannels).fill(null).map(function () {\n return new Float32Array(_this.bufferSize);\n });\n _this.stereoVol = [0, 0];\n _this.stereoVolNorm = [0, 0];\n _this.volMax = 0.001;\n\n _this.port.onmessage = function (event) {\n var data = event.data;\n\n if (data.name === \'toggleNormalize\') {\n _this.normalize = data.normalize;\n } else if (data.name === \'smoothing\') {\n _this.smoothing = Math.max(0, Math.min(1, data.smoothing));\n }\n };\n\n return _this;\n } // TO DO make this stereo / dependent on # of audio channels\n\n\n _createClass(AmplitudeProcessor, [{\n key: "process",\n value: function process(inputs, outputs) {\n var input = inputs[0];\n var output = outputs[0];\n var smoothing = this.smoothing;\n this.inputRingBuffer.push(input);\n\n if (this.inputRingBuffer.framesAvailable >= this.bufferSize) {\n this.inputRingBuffer.pull(this.inputRingBufferArraySequence);\n\n for (var channel = 0; channel < this.numInputChannels; ++channel) {\n var inputBuffer = this.inputRingBufferArraySequence[channel];\n var bufLength = inputBuffer.length;\n var sum = 0;\n\n for (var i = 0; i < bufLength; i++) {\n var x = inputBuffer[i];\n\n if (this.normalize) {\n sum += Math.max(Math.min(x / this.volMax, 1), -1) * Math.max(Math.min(x / this.volMax, 1), -1);\n } else {\n sum += x * x;\n }\n } // ... then take the square root of the sum.\n\n\n var rms = Math.sqrt(sum / bufLength);\n this.stereoVol[channel] = Math.max(rms, this.stereoVol[channel] * smoothing);\n this.volMax = Math.max(this.stereoVol[channel], this.volMax);\n } // calculate stero normalized volume and add volume from all channels together\n\n\n var volSum = 0;\n\n for (var index = 0; index < this.stereoVol.length; index++) {\n this.stereoVolNorm[index] = Math.max(Math.min(this.stereoVol[index] / this.volMax, 1), 0);\n volSum += this.stereoVol[index];\n } // volume is average of channels\n\n\n var volume = volSum / this.stereoVol.length; // normalized value\n\n var volNorm = Math.max(Math.min(volume / this.volMax, 1), 0);\n this.port.postMessage({\n name: \'amplitude\',\n volume: volume,\n volNorm: volNorm,\n stereoVol: this.stereoVol,\n stereoVolNorm: this.stereoVolNorm\n }); // pass input through to output\n\n this.outputRingBuffer.push(this.inputRingBufferArraySequence);\n } // pull 128 frames out of the ring buffer\n // if the ring buffer does not have enough frames, the output will be silent\n\n\n this.outputRingBuffer.pull(output);\n return true;\n }\n }]);\n\n return AmplitudeProcessor;\n}(_wrapNativeSuper(AudioWorkletProcessor));\n\nregisterProcessor(processorNames.amplitudeProcessor, AmplitudeProcessor);'},function(t,e,n){var i,r;i=[n(0),n(17)],void 0===(r=function(r){r.Frequency=function(t,e){if(!(this instanceof r.Frequency))return new r.Frequency(t,e);r.TimeBase.call(this,t,e)},r.extend(r.Frequency,r.TimeBase),r.Frequency.prototype._primaryExpressions=Object.create(r.TimeBase.prototype._primaryExpressions),r.Frequency.prototype._primaryExpressions.midi={regexp:/^(\d+(?:\.\d+)?midi)/,method:function(t){return this.midiToFrequency(t)}},r.Frequency.prototype._primaryExpressions.note={regexp:/^([a-g]{1}(?:b|#|x|bb)?)(-?[0-9]+)/i,method:function(t,e){var n=i[t.toLowerCase()]+12*(parseInt(e)+1);return this.midiToFrequency(n)}},r.Frequency.prototype._primaryExpressions.tr={regexp:/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?):?(\d+(?:\.\d+)?)?/,method:function(t,e,n){var i=1;return t&&"0"!==t&&(i*=this._beatsToUnits(this._timeSignature()*parseFloat(t))),e&&"0"!==e&&(i*=this._beatsToUnits(parseFloat(e))),n&&"0"!==n&&(i*=this._beatsToUnits(parseFloat(n)/4)),i}},r.Frequency.prototype.transpose=function(t){return this._expr=function(t,e){return t()*this.intervalToFrequencyRatio(e)}.bind(this,this._expr,t),this},r.Frequency.prototype.harmonize=function(t){return this._expr=function(t,e){for(var n=t(),i=[],r=0;rthis.buffer.duration)throw"jump time out of range";if(e>this.buffer.duration-t)throw"end time out of range";var n=t||0,i=e||void 0;this.isPlaying()&&(this.stop(0),this.play(0,this.playbackRate,this.output.gain.value,n,i))}},{key:"channels",value:function(){return this.buffer.numberOfChannels}},{key:"sampleRate",value:function(){return this.buffer.sampleRate}},{key:"frames",value:function(){return this.buffer.length}},{key:"getPeaks",value:function(t){if(!this.buffer)throw"Cannot load peaks yet, buffer is not loaded";if(t=t||5*window.width,this.buffer){for(var e=this.buffer,n=e.length/t,i=~~(n/10)||1,r=e.numberOfChannels,o=new Float32Array(Math.round(t)),s=0;so[u])&&(o[u]=h)}return o}}},{key:"reverseBuffer",value:function(){if(!this.buffer)throw"SoundFile is not done loading";var t=this._lastPos/R.sampleRate,e=this.getVolume();this.setVolume(0,.001);for(var n=this.buffer.numberOfChannels,i=0;it[o].hi&&o++,r[o]=void 0!==r[o]?(r[o]+n[s])/2:n[s]}return r}},{key:"getOctaveBands",value:function(t,e){var n=t||3,i=e||15.625,r=[],o={lo:i/Math.pow(2,1/(2*n)),ctr:i,hi:i*Math.pow(2,1/(2*n))};r.push(o);for(var s=p.audiocontext.sampleRate/2;o.hi=this._maxDelay)throw new Error("Delay Time exceeds maximum delay time of "+this._maxDelay+" second.");t.connect(this.input),this.leftDelay.delayTime.setValueAtTime(o,this.ac.currentTime),this.rightDelay.delayTime.setValueAtTime(o,this.ac.currentTime),this._leftGain.gain.value=r,this._rightGain.gain.value=r,i&&(this._leftFilter.freq(i),this._rightFilter.freq(i))}},{key:"delayTime",value:function(t){"number"!=typeof t?(t.connect(this.leftDelay.delayTime),t.connect(this.rightDelay.delayTime)):(this.leftDelay.delayTime.cancelScheduledValues(this.ac.currentTime),this.rightDelay.delayTime.cancelScheduledValues(this.ac.currentTime),this.leftDelay.delayTime.linearRampToValueAtTime(t,this.ac.currentTime),this.rightDelay.delayTime.linearRampToValueAtTime(t,this.ac.currentTime))}},{key:"feedback",value:function(t){if(t&&"number"!=typeof t)t.connect(this._leftGain.gain),t.connect(this._rightGain.gain);else{if(1<=t)throw new Error("Feedback value will force a positive feedback loop.");"number"==typeof t&&(this._leftGain.gain.value=t,this._rightGain.gain.value=t)}return this._leftGain.gain.value}},{key:"filter",value:function(t,e){this._leftFilter.set(t,e),this._rightFilter.set(t,e)}},{key:"setType",value:function(t){switch(1===t&&(t="pingPong"),this._split.disconnect(),this._leftFilter.disconnect(),this._rightFilter.disconnect(),this._split.connect(this.leftDelay,0),this._split.connect(this.rightDelay,1),t){case"pingPong":this._rightFilter.setType(this._leftFilter.biquad.type),this._leftFilter.output.connect(this._merge,0,0),this._rightFilter.output.connect(this._merge,0,1),this._leftFilter.output.connect(this.rightDelay),this._rightFilter.output.connect(this.leftDelay);break;default:this._leftFilter.output.connect(this._merge,0,0),this._rightFilter.output.connect(this._merge,0,1),this._leftFilter.output.connect(this.leftDelay),this._rightFilter.output.connect(this.rightDelay)}}},{key:"dispose",value:function(){de(ye(e.prototype),"dispose",this).call(this),this._split.disconnect(),this._leftFilter.dispose(),this._rightFilter.dispose(),this._merge.disconnect(),this._leftGain.disconnect(),this._rightGain.disconnect(),this.leftDelay.disconnect(),this.rightDelay.disconnect(),this._split=void 0,this._leftFilter=void 0,this._rightFilter=void 0,this._merge=void 0,this._leftGain=void 0,this._rightGain=void 0,this.leftDelay=void 0,this.rightDelay=void 0}}]),e}();function _e(t){return(_e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function ge(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function be(t,e){for(var n=0;nthis.length&&(this.length=i.sequence.length)}},{key:"removePhrase",value:function(t){for(var e in this.phrases)this.phrases[e].name===t&&this.phrases.splice(e,1)}},{key:"getPhrase",value:function(t){for(var e in this.phrases)if(this.phrases[e].name===t)return this.phrases[e]}},{key:"replaceSequence",value:function(t,e){for(var n in this.phrases)this.phrases[n].name===t&&(this.phrases[n].sequence=e)}},{key:"incrementStep",value:function(t){this.partStep=t.parts.length?(t.scoreStep=0,t.onended()):(t.scoreStep=0,t.parts[t.currentPart-1].stop(),t.parts[t.currentPart].start())}function Ue(t,e){for(var n=0;nthis.cutoff&&e>this.threshold&&0this.treshold){this.isDetected=!0,this.callback?this.callback(this.energy):e&&e(this.energy);var n=this;setTimeout(function(){n.isDetected=!1},this.sensitivity)}this.penergy=this.energy}}]),r}();function xn(t,e){for(var n=0;n