Files
NiceBaseball/sketch.js
2022-02-15 01:35:01 +08:00

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;
}