628 lines
17 KiB
JavaScript
628 lines
17 KiB
JavaScript
const pi = 3.14159265358979323846;
|
|
let score = {
|
|
strikes: 0,
|
|
balls: 0,
|
|
outs: 0,
|
|
hits: 0
|
|
}
|
|
let midiSelectSlider;
|
|
let canPitch, canSwing;
|
|
let ball = {
|
|
startX: 250, // 球開始座標
|
|
startY: 50,
|
|
angle: 0, // ball angle
|
|
spinVel: 0.01, // ball spin velocity
|
|
velX: 0, // ball velocity
|
|
velY: 0,
|
|
x: 250, // ball position
|
|
y: 50,
|
|
size: 50, // ball size
|
|
speed: 0.05, // ball speed
|
|
desiredX: 250, // ball desired position
|
|
desiredXVel: 0,
|
|
desiredY: 50,
|
|
status: "ready",
|
|
strike: false,
|
|
hit: false,
|
|
randomX: 0
|
|
};
|
|
let bat = {
|
|
startAngle: pi * 1.5,
|
|
endAngle: pi * -0.5,
|
|
desiredAngle: pi * 1.5,
|
|
angle: pi * 1.5,
|
|
vel: 0,
|
|
speed: 0.3,
|
|
status: "ready",
|
|
swung: false // 揮棒記錄
|
|
};
|
|
let pitchInput = {
|
|
c: false,
|
|
e: false,
|
|
g: false,
|
|
cTime: 0,
|
|
eTime: 0,
|
|
gTime: 0,
|
|
cVel: 0,
|
|
eVel: 0,
|
|
gVel: 0
|
|
};
|
|
let pitcherDir = "C";
|
|
let umpireReady = true;
|
|
let messageOn = false;
|
|
let messageType = "";
|
|
|
|
|
|
function preload() {
|
|
// images
|
|
ballImg = loadImage('assets/ball.png');
|
|
batImg = loadImage('assets/bat.png');
|
|
plateImg = loadImage('assets/plate.png');
|
|
|
|
// SFX
|
|
soundFormats('mp3');
|
|
swishSFX = loadSound('assets/swish');
|
|
swingSFX = loadSound('assets/swing');
|
|
hitSFX = loadSound('assets/hit');
|
|
catchSFX = loadSound('assets/catch');
|
|
ballFourSFX = loadSound('assets/ballfour');
|
|
fairBallSFX = loadSound('assets/fairball');
|
|
outSFX = loadSound('assets/out');
|
|
gameOverSFX = loadSound('assets/gameover');
|
|
|
|
|
|
}
|
|
|
|
function setup() {
|
|
let canvas = createCanvas(500, 800);
|
|
getAudioContext().suspend();
|
|
|
|
canvas.parent('sketch-holder');
|
|
colorMode(HSB, 100)
|
|
frameRate(120);
|
|
textAlign(CENTER, CENTER);
|
|
// Init MIDI
|
|
WebMidi.enable(function (err) { //check if WebMidi.js is enabled
|
|
if (err) {
|
|
console.log("WebMidi could not be enabled.", err);
|
|
} else {
|
|
console.log("WebMidi enabled!");
|
|
}
|
|
|
|
//name our visible MIDI input and output ports
|
|
console.log("---");
|
|
console.log("Inputs Ports: ");
|
|
for (i = 0; i < WebMidi.inputs.length; i++) {
|
|
console.log(i + ": " + WebMidi.inputs[i].name);
|
|
}
|
|
|
|
console.log("---");
|
|
console.log("Output Ports: ");
|
|
for (i = 0; i < WebMidi.outputs.length; i++) {
|
|
console.log(i + ": " + WebMidi.outputs[i].name);
|
|
}
|
|
midiSelectSlider = select("#slider");
|
|
midiSelectSlider.attribute("max", WebMidi.inputs.length - 1);
|
|
midiSelectSlider.changed(inputChanged);
|
|
midiIn = WebMidi.inputs[midiSelectSlider.value()]
|
|
inputChanged();
|
|
});
|
|
resetGame();
|
|
}
|
|
|
|
function draw() { // 主 Loop
|
|
background(66, 20, 20);
|
|
drawField();
|
|
drawScore();
|
|
updateBat();
|
|
drawBat();
|
|
calculatePitch();
|
|
udpateBall()
|
|
drawBall();
|
|
checkHit();
|
|
umpire();
|
|
drawMessage();
|
|
|
|
// bat.angle -= 0.05;
|
|
}
|
|
|
|
function checkHit() { // 檢查有沒有打到球
|
|
textAlign(CENTER, CENTER)
|
|
//let debugText = floor(mouseX) + "," + floor(mouseY);
|
|
fill(255);
|
|
stroke(255);
|
|
//text(debugText, mouseX, mouseY - 10);
|
|
let dist = 25;
|
|
let len = 125;
|
|
let ang = bat.angle - (pi * 0.25);
|
|
let lineX1 = dist * cos(ang - 0.1) + 170;
|
|
let lineX2 = (dist + len) * cos(ang - 0.1) + 170;
|
|
let lineY1 = dist * sin(ang - 0.1) + 600;
|
|
let lineY2 = (dist + len) * sin(ang - 0.1) + 600;
|
|
let lineX3 = dist * cos(ang + 0.1) + 170;
|
|
let lineX4 = (dist + len) * cos(ang + 0.1) + 170;
|
|
let lineY3 = dist * sin(ang + 0.1) + 600;
|
|
let lineY4 = (dist + len) * sin(ang + 0.1) + 600;
|
|
//line(lineX1, lineY1, lineX2, lineY2); // bat visualizer
|
|
// line(lineX3, lineY3, lineX4, lineY4);
|
|
//let bottomText = "ball: " + ball.x + ", " + ball.y;
|
|
//text(bottomText, 250, 750);
|
|
// 用兩條線代表球棒(避免穿牆),檢查跟球是否重疊
|
|
|
|
if (lineCircle(lineX1, lineY1, lineX2, lineY2, ball.x, ball.y, ball.size / 2) || lineCircle(lineX3, lineY3, lineX4, lineY4, ball.x, ball.y, ball.size / 2)) {
|
|
if (bat.angle < pi && bat.status == "swinging" && !ball.hit) {
|
|
fill(100, 70, 100);
|
|
ellipse(ball.x, ball.y, ball.size);
|
|
// console.log("hit" + ball.x + ", " + ball.y + " " + bat.angle);
|
|
ball.hit = true;
|
|
|
|
hitSFX.play();
|
|
flyBall(ang, bat.vel, ball.x, ball.y, ball.speed); // ball is hit!
|
|
console.log(ang, bat.vel, ball.x, ball.y, ball.speed);
|
|
}
|
|
} else {
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
function umpire() { // 判斷好球壞球
|
|
//stroke(255);
|
|
//line(200, 600, 300, 600); // plate visualizer
|
|
//line(200, 660, 300, 660);
|
|
if (lineCircle(200, 600, 300, 600, ball.x, ball.y, ball.size / 2) || lineCircle(200, 660, 300, 660, ball.x, ball.y, ball.size / 2)) {
|
|
console.log("strike zone passed.");
|
|
ball.strike = true;
|
|
|
|
} else {
|
|
|
|
}
|
|
}
|
|
|
|
function drawScore() {
|
|
textFont('Verdana');
|
|
textSize(80);
|
|
fill(66, 20, 80, 20);
|
|
noStroke();
|
|
textAlign(RIGHT, TOP);
|
|
textStyle(BOLD);
|
|
// text(score.outs, 480, 20);
|
|
text(score.hits, 130, 600);
|
|
|
|
let scoreText = "STRIKE " + "⚾️".repeat(score.strikes) + "\nBALLS " + "⚾️".repeat(score.balls) + "\nOUTS " + "⚾️".repeat(score.outs);
|
|
textSize(18);
|
|
fill(66, 10, 80);
|
|
textAlign(LEFT, CENTER);
|
|
textStyle(BOLD);
|
|
text(scoreText, 300, 720);
|
|
|
|
|
|
}
|
|
|
|
function drawMessage() {
|
|
if (messageOn) {
|
|
let type = messageType;
|
|
let bigText, smallText = "";
|
|
if (type == "hit") {
|
|
bigText = "HIT!"
|
|
smallText = ""
|
|
}
|
|
if (type == "foul") {
|
|
bigText = "FOUL"
|
|
smallText = ""
|
|
}
|
|
if (type == "strike") {
|
|
bigText = "STRIKE"
|
|
smallText = ""
|
|
}
|
|
if (type == "strikeOut") {
|
|
bigText = "STRIKE OUT!"
|
|
smallText = ""
|
|
}
|
|
if (type == "ball") {
|
|
bigText = "BALL"
|
|
smallText = ""
|
|
}
|
|
if (type == "ballFour") {
|
|
bigText = "BALL FOUR!"
|
|
smallText = ""
|
|
}
|
|
if (score.outs == 3) {
|
|
bigText = "GAME OVER"
|
|
smallText = "Hits: " + score.hits;
|
|
|
|
}
|
|
textSize(32);
|
|
textAlign(CENTER, CENTER);
|
|
text(bigText, 250, 350);
|
|
textStyle(NORMAL);
|
|
textSize(24);
|
|
text(smallText, 250, 400);
|
|
}
|
|
}
|
|
|
|
function resetGame() {
|
|
bat.swung = false; // 揮棒記錄
|
|
messageOn = false; // 訊息關閉
|
|
returnBat();
|
|
returnBall();
|
|
if (score.outs == 3) {
|
|
resetScore();
|
|
}
|
|
}
|
|
|
|
function pitcherPos(dir) { // 移動投手位置
|
|
if (ball.status == "ready") {
|
|
if (dir == "L") {
|
|
ball.desiredX -= 4;
|
|
ball.desiredX = constrain(ball.desiredX, 150, 350);
|
|
}
|
|
if (dir == "R") {
|
|
ball.desiredX += 4;
|
|
ball.desiredX = constrain(ball.desiredX, 150, 350);
|
|
}
|
|
}
|
|
}
|
|
|
|
function udpateBall() { // 球的移動邏輯
|
|
// change pitcher position
|
|
pitcherPos(pitcherDir);
|
|
// move ball to desired x, y
|
|
ball.desiredX += constrain(ball.desiredXVel, -500, 1000);
|
|
if (frameCount % 4) {
|
|
ball.desiredX += random(-ball.randomX, ball.randomX);
|
|
}
|
|
ball.velX = (ball.desiredX - ball.x) * ball.speed;
|
|
ball.velY = (ball.desiredY - ball.y) * ball.speed;
|
|
if (abs(ball.desiredX - ball.x) < 1) {
|
|
ball.x = ball.desiredX;
|
|
}
|
|
if (abs(ball.desiredY - ball.y) < 1) {
|
|
ball.y = ball.desiredY;
|
|
}
|
|
if (ball.y > 900 && ball.status == "pitching") { // 球被捕手接到了
|
|
ball.status = "catched";
|
|
catchSFX.play();
|
|
ball.desiredXVel = 0;
|
|
if (ball.strike || bat.swung) {
|
|
score.strikes++;
|
|
messageOn = true;
|
|
messageType = "strike";
|
|
console.log("strikes: " + score.strikes);
|
|
if (score.strikes == 3) { // strike out
|
|
messageOn = true;
|
|
messageType = "strikeOut";
|
|
outSFX.play();
|
|
score.outs++;
|
|
if (score.outs == 3) {
|
|
gameOverSFX.play();
|
|
}
|
|
score.strikes = 0;
|
|
score.balls = 0;
|
|
}
|
|
ball.strike = false;
|
|
} else {
|
|
score.balls++;
|
|
messageOn = true;
|
|
messageType = "ball";
|
|
console.log("balls: " + score.balls);
|
|
if (score.balls == 4) { // ball four
|
|
messageOn = true;
|
|
messageType = "ballFour";
|
|
ballFourSFX.play();
|
|
score.hits++;
|
|
score.balls = 0;
|
|
score.strikes = 0;
|
|
}
|
|
}
|
|
}
|
|
ball.x += ball.velX;
|
|
ball.y += ball.velY;
|
|
ball.angle += ball.spinVel;
|
|
}
|
|
|
|
function ballDrop() { // 判斷球落點!
|
|
if (ball.status == "flying") {
|
|
ball.desiredXVel = 0;
|
|
let ballDrop = calAngle(250, 700, ball.x, ball.y);
|
|
if (ballDrop < -0.78 && ballDrop > -2.36) {
|
|
messageOn = true;
|
|
messageType = "hit";
|
|
console.log("Fair Ball: " + ballDrop);
|
|
fairBallSFX.play();
|
|
score.hits++;
|
|
score.balls = 0;
|
|
score.strikes = 0;
|
|
} else {
|
|
messageOn = true;
|
|
messageType = "foul";
|
|
console.log("Foul Ball: " + ballDrop);
|
|
if (score.strikes < 2) {
|
|
score.strikes++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function calculatePitch() { // 計算球路
|
|
if (ball.status == "ready" && pitchInput.c == true && pitchInput.e == true && pitchInput.g == true) { // Check C, E, G all pressed
|
|
// calculate spin
|
|
let spin = pitchInput.gVel - pitchInput.cVel // C & G control spin
|
|
spin = map(spin, -0.5, 0.5, -1, 1, true);
|
|
ball.desiredXVel = spin * -120;
|
|
ball.spinVel = spin;
|
|
// calculate speed
|
|
let speed = (pitchInput.cVel + pitchInput.eVel * 2 + pitchInput.gVel) / 4;
|
|
speed = map(speed, 0.2, 0.8, 0.01, 0.06, true);
|
|
ball.speed = speed;
|
|
ball.randomX = map(abs(spin), 0, 0.5, 220, 0, true) * map(speed, 0.01, 0.03, 1, 0, true) // the less spin & the less speed, the more random X movement
|
|
console.log(abs(spin), speed);
|
|
ball.desiredX += spin * map(speed, 0.01, 0.07, 800, -50, true);
|
|
|
|
// if input too slow
|
|
let duration = max([pitchInput.cTime, pitchInput.eTime, pitchInput.gTime]) - min([pitchInput.cTime, pitchInput.eTime, pitchInput.gTime]);
|
|
console.log(duration);
|
|
if (duration > 100) {
|
|
ball.speed = 0.007; // 輸入期間超過 100ms 的話,球變超慢
|
|
ball.desiredXVel = ball.desiredXVel * 0.05; // 球變不曲
|
|
ball.spinVel = ball.spinVel * 0.2 // 球變不曲
|
|
ball.randomX = 0;
|
|
}
|
|
|
|
// pitch!
|
|
ball.status = "pitching";
|
|
console.log("pitch!")
|
|
pitchBall();
|
|
|
|
}
|
|
}
|
|
|
|
function flyBall(batAng, batVel, ballX, ballY, ballSpeed) { // 球被打到了,計算飛行方向
|
|
ball.status = "flying";
|
|
ball.randomX = 0;
|
|
let flyDist = 1000; // fly distance
|
|
ball.desiredX = flyDist * cos(batAng - 1.4) + ballX;
|
|
ball.desiredXVel = 10 * cos(batAng - 1);
|
|
ball.desiredY = flyDist * sin(batAng - 1.4) + ballY;
|
|
ball.spinVel += cos(batAng - 1);
|
|
ball.speed = 0.03; //
|
|
setTimeout(ballDrop, 600); // 0.6 秒後判斷球落點
|
|
}
|
|
|
|
function pitchBall() {
|
|
ball.desiredY = 1300; // 投球就是將球想要去的 y 座標設到 1300
|
|
swishSFX.play();
|
|
}
|
|
|
|
function returnBall() { // return the ball to original position
|
|
pitchInput.c = false;
|
|
pitchInput.e = false;
|
|
pitchInput.g = false;
|
|
ball.speed = 0.2;
|
|
ball.spinVel = random(-0.02, 0.02);
|
|
ball.desiredX = ball.startX;
|
|
ball.desiredXVel = 0;
|
|
ball.desiredY = ball.startY;
|
|
pitcherDir = "C";
|
|
ball.hit = false;
|
|
ball.status = "ready";
|
|
ball.strike = false;
|
|
ball.randomX = 0;
|
|
}
|
|
|
|
function drawBall() {
|
|
push();
|
|
translate(ball.x, ball.y);
|
|
rotate(ball.angle);
|
|
imageMode(CENTER);
|
|
image(ballImg, 0, 0, ball.size, ball.size);
|
|
pop();
|
|
}
|
|
|
|
function updateBat() {
|
|
bat.vel = (bat.desiredAngle - bat.angle) * bat.speed;
|
|
bat.angle = bat.angle + bat.vel;
|
|
if (abs(bat.angle - bat.desiredAngle) < 0.01) {
|
|
bat.angle = bat.desiredAngle;
|
|
}
|
|
}
|
|
|
|
function drawBat() {
|
|
push();
|
|
translate(170, 600);
|
|
rotate(bat.angle);
|
|
image(batImg, 40, -40, 105, 105);
|
|
pop();
|
|
}
|
|
|
|
function swingBat(vel) {
|
|
if (bat.status == "ready") {
|
|
bat.speed = map(vel, 0.2, 0.8, 0.1, 0.4, true);
|
|
console.log(bat.speed);
|
|
bat.desiredAngle = bat.endAngle;
|
|
bat.status = "swinging";
|
|
swingSFX.play();
|
|
bat.swung = true;
|
|
setTimeout(returnBat, 1000);
|
|
}
|
|
}
|
|
|
|
function returnBat() {
|
|
bat.desiredAngle = bat.startAngle;
|
|
bat.speed = 0.1;
|
|
bat.status = "ready";
|
|
}
|
|
|
|
function drawField() {
|
|
stroke(30);
|
|
line(250, 700, 0, 450);
|
|
line(250, 700, 500, 450);
|
|
imageMode(CENTER);
|
|
image(plateImg, 250, 650, 110, 110);
|
|
|
|
}
|
|
|
|
function resetScore() {
|
|
score.strikes = 0;
|
|
score.balls = 0;
|
|
score.outs = 0;
|
|
score.hits = 0;
|
|
}
|
|
|
|
function noteOn(note, vel, ms) {
|
|
if (note == 36) { // C3
|
|
resetScore();
|
|
resetGame();
|
|
}
|
|
if (note == 48) { // C3
|
|
swingBat(vel);
|
|
}
|
|
if (note == 60) {
|
|
resetGame();
|
|
}
|
|
if (note == 72) {
|
|
pitchInput.c = true;
|
|
pitchInput.cVel = vel;
|
|
pitchInput.cTime = ms;
|
|
}
|
|
if (note == 76) {
|
|
pitchInput.e = true;
|
|
pitchInput.eVel = vel;
|
|
pitchInput.eTime = ms;
|
|
}
|
|
if (note == 79) {
|
|
pitchInput.g = true;
|
|
pitchInput.gVel = vel;
|
|
pitchInput.gTime = ms;
|
|
}
|
|
if (note == 73) {
|
|
pitcherDir = "L";
|
|
}
|
|
if (note == 75) {
|
|
pitcherDir = "R";
|
|
}
|
|
}
|
|
|
|
function noteOff(note, vel, ms) {
|
|
if (note == 73) {
|
|
pitcherDir = "C";
|
|
}
|
|
if (note == 75) {
|
|
pitcherDir = "C";
|
|
}
|
|
}
|
|
|
|
function inputChanged() {
|
|
midiIn.removeListener();
|
|
midiIn = WebMidi.inputs[midiSelectSlider.value()];
|
|
midiIn.addListener('noteon', "all", function (e) {
|
|
console.log(e.note.number, e.velocity, e.timestamp);
|
|
noteOn(e.note.number, e.velocity, e.timestamp); // number:
|
|
});
|
|
midiIn.addListener('noteoff', "all", function (e) {
|
|
noteOff(e.note.number, e.velocity, e.timestamp);
|
|
})
|
|
console.log(midiIn.name);
|
|
select("#device").html(midiIn.name);
|
|
};
|
|
|
|
function mouseClicked() {
|
|
resetGame();
|
|
userStartAudio();
|
|
}
|
|
|
|
|
|
// line-circle collision
|
|
|
|
function lineCircle(x1, y1, x2, y2, cx, cy, r) {
|
|
|
|
// is either end INSIDE the circle?
|
|
// if so, return true immediately
|
|
let inside1 = pointCircle(x1, y1, cx, cy, r);
|
|
let inside2 = pointCircle(x2, y2, cx, cy, r);
|
|
if (inside1 || inside2) return true;
|
|
|
|
// get length of the line
|
|
let distX = x1 - x2;
|
|
let distY = y1 - y2;
|
|
let len = sqrt((distX * distX) + (distY * distY));
|
|
|
|
// get dot product of the line and circle
|
|
let dot = (((cx - x1) * (x2 - x1)) + ((cy - y1) * (y2 - y1))) / pow(len, 2);
|
|
|
|
// find the closest point on the line
|
|
let closestX = x1 + (dot * (x2 - x1));
|
|
let closestY = y1 + (dot * (y2 - y1));
|
|
|
|
// is this point actually on the line segment?
|
|
// if so keep going, but if not, return false
|
|
let onSegment = linePoint(x1, y1, x2, y2, closestX, closestY);
|
|
if (!onSegment) return false;
|
|
|
|
// optionally, draw a circle at the closest
|
|
// point on the line
|
|
|
|
|
|
// get distance to closest point
|
|
distX = closestX - cx;
|
|
distY = closestY - cy;
|
|
let distance = sqrt((distX * distX) + (distY * distY));
|
|
|
|
if (distance <= r) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// POINT/CIRCLE
|
|
function pointCircle(px, py, cx, cy, r) {
|
|
|
|
// get distance between the point and circle's center
|
|
// using the Pythagorean Theorem
|
|
let distX = px - cx;
|
|
let distY = py - cy;
|
|
let distance = sqrt((distX * distX) + (distY * distY));
|
|
|
|
// if the distance is less than the circle's
|
|
// radius the point is inside!
|
|
if (distance <= r) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// LINE/POINT
|
|
function linePoint(x1, y1, x2, y2, px, py) {
|
|
|
|
// get distance from the point to the two ends of the line
|
|
let d1 = dist(px, py, x1, y1);
|
|
let d2 = dist(px, py, x2, y2);
|
|
|
|
// get the length of the line
|
|
let lineLen = dist(x1, y1, x2, y2);
|
|
|
|
// since floats are so minutely accurate, add
|
|
// a little buffer zone that will give collision
|
|
let buffer = 0.1; // higher # = less accurate
|
|
|
|
// if the two distances are equal to the line's
|
|
// length, the point is on the line!
|
|
// note we use the buffer here to give a range,
|
|
// rather than one #
|
|
if (d1 + d2 >= lineLen - buffer && d1 + d2 <= lineLen + buffer) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function calAngle(cx, cy, ex, ey) {
|
|
var dy = ey - cy;
|
|
var dx = ex - cx;
|
|
var theta = Math.atan2(dy, dx); // range (-PI, PI]
|
|
return theta;
|
|
} |