I'm a beginner using p5js and I'm trying to work with classes. I'm making a game where you have to find and click a 'wanted man', from a crowd.
So basically, a randomizer picks between 7 different types of 'civilians', and it's supposed to remove one of the types from the 'civilians' that have been spawned. After removing the 'wanted man', I want to add one wanted man so that there is only one 'wanted man'.
So the code spawns a bunch of random 'civilians', then it will delete all 'wanted man' types in the array, and add only one of them. I think there is a better way to do this though.
My basic desire is to have a crowd of 'civilians' that run around, - one of which is a 'wanted man' - and you would have to find and click that 'wanted man' (kind of like a hunting/assassination game).
This is the code for the sketch.js file:
var civilians = [];
var page = 0;
var man1img;
var man2img;
var man3img;
var man4img;
var man5img;
var man6img;
var aliemanimg;
var w;
var h;
var spawnCount = 14;
var wantedMan;
var randCiv;
function preload() {
man1img = loadImage("man1.png");
man2img = loadImage("man2.png");
man3img = loadImage("man3.png");
man4img = loadImage("man4.png");
man5img = loadImage("man5.png");
man6img = loadImage("man6.png");
aliemanimg = loadImage("alieman.png");
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
function setup() {
createCanvas(windowWidth, windowHeight);
imageMode(CENTER);
// wantedMan = round(random(0, 6));
wantedMan = 0;
for (var i = 0; i < spawnCount; i++) {
randCiv = round(random(0, 6));
w = random(windowWidth);
h = random(windowHeight);
civilians.push(new Civilian(w, h, wantedMan, randCiv));
console.log(wantedMan);
if (civilians[i].isWantedMan()) {
//OVER HERE \/
civilians.splice(i, 1);
}
}
civilians.push(new Civilian(w, h, wantedMan, wantedMan));
}
// page setup
// page 1 : main screen (play, settings, and those stuff)
// page 2 : show chosen civilian
// page 3 : playing
// page 4 : lose
// page 5 : options
function draw() {
background(220, 80, 80);
for (var i = civilians.length - 1; i >= 0; i--) {
civilians[i].update();
civilians[i].show(mouseX, mouseY);
if (civilians[i].clickedOn(mouseX, mouseY)) {
// detect if is right person
console.log("clicked on boi");
if (civilians[i].isWantedMan()) {
console.log("HES WANTED");
} else {
console.log("HES NOT WANTED");
}
}
}
text(round(frameRate()), 20, 20);
//show wanted man
var tempImg = man1img;
if (wantedMan == 1) {
tempImg = man2img;
} else if (wantedMan == 2) {
tempImg = man3img;
} else if (wantedMan == 3) {
tempImg = man4img;
}
if (wantedMan == 4) {
tempImg = man5img;
} else if (wantedMan == 5) {
tempImg = man6img;
} else if (wantedMan == 6) {
tempImg = aliemanimg;
}
image(tempImg, 50, 70, 70, 90);
}
This is the code for the class:
class Civilian {
constructor(x, y, wantedMan, type) {
this.x = x;
this.y = y;
this.w = 47;
this.h = 60;
this.t = {
x: x,
y: y,
};
this.size = 47;
this.moveSpeed = 0.01;
this.moveDist = 20;
this.wantedMan = wantedMan;
this.civilian = type
this.civilianImg = man1img
this.wantedMan = wantedMan
}
update() {
//move target to random position
this.t.x = random(this.t.x - this.moveDist, this.t.x + this.moveDist);
this.t.y = random(this.t.y - this.moveDist, this.t.y + this.moveDist);
//edge detect
if (this.t.x < 0) {
this.t.x += 5;
}
if (this.t.x > width) {
this.t.x -= 5;
}
if (this.t.y < 0) {
this.t.y += 5;
}
if (this.t.y > height) {
this.t.y -= 5;
}
//images position follows target but with easing
this.x += (this.t.x - this.x) * this.moveSpeed;
this.y += (this.t.y - this.y) * this.moveSpeed;
}
show(ex, ey) {
var d = dist(ex, ey, this.x, this.y);
if (d > this.size / 2) {
tint(255, 255, 255);
} else {
tint(0, 255, 0);
}
if(this.civilian == 1) {
this.civilianImg = man2img
} else if(this.civilian == 2) {
this.civilianImg = man3img
} else if(this.civilian ==3) {
this.civilianImg = man4img
} if(this.civilian == 4) {
this.civilianImg = man5img
} else if(this.civilian == 5) {
this.civilianImg = man6img
} else if(this.civilian == 6) {
this.civilianImg = aliemanimg
}
image(this.civilianImg, this.x, this.y, 47, 60);
}
clickedOn(ex, ey) {
var d = dist(ex, ey, this.x, this.y);
return d < this.size / 2 && mouseIsPressed;
}
isWantedMan() {
return this.civilian == this.wantedMan;
}
}
However, whenever I add a .splice(i,1) under the 'for' loop in setup function - to remove the 'wanted man', it shows this error:
"TypeError: Cannot read properties of undefined (reading
'isWantedMan') at /sketch.js:41:22".
isWantedMan() is a function in the Civilian Class, that returns true if the current 'civilian' is wanted. The .splice is supposed to remove a object from the array, when it is a 'wanted man'.
I don't know why this happens. When I replace the .splice code with a console.log() code, then there is no error.
Also there were probably a lot of things that I could have done better in the code.
I'm attempting to make a game, and I've got some sprites that will overlap each other. When clearing the sprite's area with clearRect, any sprite behind it will disappear as if the foreground sprite wasn't transparent. However, if I try to save and restore the area behind the sprite using get/putImageData, weird things start to happen. Part of the sprites in various places do not get "undrawn", other parts seem to be broken up and yards away, and other sprites get smeared. Here's a code chunk:
var anim = function()
{
if(gtiming < Date.now() % 1000)
timing = (Date.now() % 1000) - gtiming;
if(stage == 1)
{
ugcnt = ugcnt + timing;
if(mleft == true)
{
acc = acc - 0.25;
}
else if(mright == true)
{
acc = acc + 0.25;
}
else
{
if(acc < 0 || acc > 0) acc = acc / 1.1;
}
if(kyx < 0)
{
kyx = 0;
acc = -acc;
}
else if(kyx > 432)
{
kyx = 432;
acc = -acc;
}
if(kyblk != null)
xbios.putImageData(kyblk, kyx, 155);
kyblk = null;
kyx = kyx + acc;
kyblk = xbios.getImageData(kyx, 155, 208, 245);
xbios.drawImage(kyk[Math.floor(kyf)], 0, 0, 416, 490, kyx, 155, 208, 245);
if(ugcnt > mus[r][1] * 1000)
{
ugcnt = 0;
ugobj.push(new ugnaut(Math.floor(Math.random() * 640), -208, "L"));
}
ugobj.forEach(testug);
kyf = kyf + ((timing / 1000) * (mus[r][1] * 240));
if(kyf > 119)
kyf = kyf - 119;
}
gtiming = Date.now() % 1000;
if(stage > 0)
requestAnimationFrame(anim);
}
function ugnaut(x, y, f)
{
this.x = x;
this.y = y;
this.f = f;
this.fr = 0;
this.blk = null;
this.set=function()
{
if(this.blk != null)
xbios.putImageData(this.blk, this.x, this.y);
this.blk = null;
if(f == "L")
{
this.y++;
this.blk = xbios.getImageData(this.x, this.y, 179, 208);
xbios.drawImage(ugf[this.fr], 0, 0, 179, 208, this.x, this.y, 179, 208);
this.fr++;
if(this.fr > 44) this.fr = 0;
}
}
this.getx = function()
{
return this.x;
}
this.gety = function()
{
return this.y;
}
this.getf = function()
{
return this.f;
}
}
function testug(item, index)
{
if(item.getx() > -180 && item.getx() < 640 && item.gety() > -224 && item.gety() < 400)
{
item.set();
}
else
{
item = null;
ugobj.splice(index, 1);
}
}
For those wondering, yes, I did call the Canvas 2D Context xbios. Just felt like a fun name at the time. Anyways, from my understanding having a this inside the "object" ugnaut the value it holds will be local to that object's instance, and so I assume each ugnaut will hold its own background information in this.blk, but am I wrong? What other methods should I use?
I have a selection menu in my HTML canvas that I would like to trigger corresponding audio files. I have tried implementing this by declaring the images inside the if (this.hovered) & (this.clicked) part of the makeSelection function within the selectionForMenu prototype, such that on each new selection the selected audio file is redefined, but this causes problems like slow loading and overlapping audio. It is also problematic as I am trying to get the speaker button at the bottom of the screen to play the audio corresponding to the current selection too, so if it is only defined within that function it is not accessible to the makeButton function.
You can see the selection menu and speaker button in the snippet below. Each new selection in the menu should play once an audio file that corresponds to it (which I have not been able to add to this demonstration). It can be replayed by re-clicking the selection or clicking the speaker button, but each click should only provoke one play of the audio and of course overlapping is undesired. Any help will be appreciated.
var c=document.getElementById('game'),
canvasX=c.offsetLeft,
canvasY=c.offsetTop,
ctx=c.getContext('2d');
var button = function(id, x, strokeColor) {
this.id = id;
this.x = x;
this.strokeColor = strokeColor;
this.hovered = false;
this.clicked = false;
}
button.prototype.makeInteractiveButton = function() {
if (this.hovered) {
if (this.clicked) {
this.fillColor = '#DFBCDE';
} else {
this.fillColor = '#CA92C8'
}
} else {
this.fillColor = '#BC77BA'
}
ctx.strokeStyle=this.strokeColor;
ctx.fillStyle=this.fillColor;
ctx.beginPath();
ctx.lineWidth='5';
ctx.arc(this.x, 475, 20, 0, 2*Math.PI);
ctx.closePath();
ctx.stroke();
ctx.fill();
}
button.prototype.hitTest = function(x, y) {
return (Math.pow(x-this.x, 2) + Math.pow(y-475, 2) < Math.pow(20, 2));
}
var selectionForMenu = function(id, text, y) {
this.id = id;
this.text = text;
this.y = y;
this.hovered = false;
this.clicked = false;
this.lastClicked = false;
}
selectionForMenu.prototype.makeSelection = function() {
var fillColor='#A84FA5';
if (this.hovered) {
if (this.clicked) {
if (this.lastClicked) {
fillColor='#E4C7E2';
} else {
fillColor='#D5A9D3';
}
} else if (this.lastClicked) {
fillColor='#D3A4D0';
} else {
fillColor='#BA74B7';
}
} else if (this.lastClicked) {
fillColor='#C78DC5';
} else {
fillColor='#A84FA5';
}
ctx.beginPath();
ctx.fillStyle=fillColor;
ctx.fillRect(0, this.y, 350, 30)
ctx.stroke();
ctx.font='10px Noto Sans';
ctx.fillStyle='white';
ctx.textAlign='left';
ctx.fillText(this.text, 10, this.y+19);
}
selectionForMenu.prototype.hitTest = function(x, y) {
return (x >= 0) && (x <= (350)) && (y >= this.y) && (y <= (this.y+30)) && !((x >= 0) && (y > 450));
}
var Paint = function(element) {
this.element = element;
this.shapes = [];
}
Paint.prototype.addShape = function(shape) {
this.shapes.push(shape);
}
Paint.prototype.render = function() {
ctx.clearRect(0, 0, this.element.width, this.element.height);
for (var i=0; i<this.shapes.length; i++) {
try {
this.shapes[i].makeSelection();
}
catch(err) {}
}
ctx.beginPath();
ctx.fillStyle='#BC77BA';
ctx.fillRect(0, 450, 750, 50);
ctx.stroke();
for (var i=0; i<this.shapes.length; i++) {
try {
this.shapes[i].makeInteractiveButton();
}
catch(err) {}
}
var speaker = new Image(25, 25);
speaker.src='https://i.stack.imgur.com/lXg2I.png';
ctx.drawImage(speaker, 162.5, 462.5);
}
Paint.prototype.setHovered = function(shape) {
for (var i=0; i<this.shapes.length; i++) {
this.shapes[i].hovered = this.shapes[i] == shape;
}
this.render();
}
Paint.prototype.setClicked = function(shape) {
for (var i=0; i<this.shapes.length; i++) {
this.shapes[i].clicked = this.shapes[i] == shape;
}
this.render();
}
Paint.prototype.setUnclicked = function(shape) {
for (var i=0; i<this.shapes.length; i++) {
this.shapes[i].clicked = false;
if (Number.isInteger(this.shapes[i].id)) {
this.shapes[i].lastClicked = this.shapes[i] == shape;
}
}
this.render();
}
Paint.prototype.select = function(x, y) {
for (var i=this.shapes.length-1; i >= 0; i--) {
if (this.shapes[i].hitTest(x, y)) {
return this.shapes[i];
}
}
return null
}
var paint = new Paint(c);
var btn = new button('speaker', 175, '#FFFCF8');
var selection = [];
for (i=0; i<15; i++) {
selection.push(new selectionForMenu(i+1, i, i*30));
}
paint.addShape(btn);
for (i=0; i<15; i++) {
paint.addShape(selection[i])
}
paint.render();
function mouseDown(event) {
var x = event.x - canvasX;
var y = event.y - canvasY;
var shape = paint.select(x, y);
paint.setClicked(shape);
}
function mouseUp(event) {
var x = event.x - canvasX;
var y = event.y - canvasY;
var shape = paint.select(x, y);
paint.setUnclicked(shape);
}
function mouseMove(event) {
var x = event.x - canvasX;
var y = event.y - canvasY;
var shape = paint.select(x, y);
paint.setHovered(shape);
}
c.addEventListener('mousedown', mouseDown);
c.addEventListener('mouseup', mouseUp);
c.addEventListener('mousemove', mouseMove);
canvas {
z-index: -1;
margin: 1em auto;
border: 1px solid black;
display: block;
background: #9F3A9B;
}
img {
z-index: 0;
position: absolute;
pointer-events: none;
}
#speaker {
top: 480px;
left: 592px;
}
#snail {
top: 475px;
left: 637.5px;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>uTalk Demo</title>
<link rel='stylesheet' type='text/css' href='wordpractice.css' media='screen'></style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
</head>
<body>
<canvas id="game" width = "350" height = "500"></canvas>
<script type='text/javascript' src='wordpractice copy.js'></script>
</body>
</html>
When you want responsiveness with audio, forget about MediaElements, and go with the Web Audio API.
MediaElements (<audio> and <video>) are slow, and http caching is an nightmare.
With the Web Audio API, you can first download all you media as arrayBuffers, decode their audio data to AudioBuffers, that you'll attach to your js objects.
From there, you'll be able to play new instances of these media in µs.
Beware, ES6 syntax below, for older browsers, here is an ES5 rewrite, also note that Internet Explorer < Edge does not support the Web Audio API, if you need to support these browsers, you'll have to make an fallback with audio elements.
(function myFirstDrumKit() {
const db_url = 'https://dl.dropboxusercontent.com/s/'; // all our medias are stored on dropbox
// we'll need to first load all the audios
function initAudios() {
const promises = drum.parts.map(part => {
return fetch(db_url + part.audio_src) // fetch the file
.then(resp => resp.arrayBuffer()) // as an arrayBuffer
.then(buf => drum.a_ctx.decodeAudioData(buf)) // then decode its audio data
.then(AudioBuf => {
part.buf = AudioBuf; // store the audioBuffer (won't change)
return Promise.resolve(part); // done
});
});
return Promise.all(promises); // when all are loaded
}
function initImages() {
// in this version we have only an static image,
// but we could have multiple per parts, with the same logic as for audios
var img = new Image();
img.src = db_url + drum.bg_src;
drum.bg = img;
return new Promise((res, rej) => {
img.onload = res;
img.onerror = rej;
});
}
let general_solo = false;
let part_solo = false;
const drum = {
a_ctx: new AudioContext(),
generate_sound: (part) => {
// called each time we need to play a source
const source = drum.a_ctx.createBufferSource();
source.buffer = part.buf;
source.connect(drum.gain);
// to keep only one playing at a time
// simply store this sourceNode, and stop the previous one
if(general_solo){
// stop all playing sources
drum.parts.forEach(p => (p.source && p.source.stop(0)));
}
else if (part_solo && part.source) {
// stop only the one of this part
part.source.stop(0);
}
// store the source
part.source = source;
source.start(0);
},
parts: [{
name: 'hihat',
x: 90,
y: 116,
w: 160,
h: 70,
audio_src: 'kbgd2jm7ezk3u3x/hihat.mp3'
},
{
name: 'snare',
x: 79,
y: 192,
w: 113,
h: 58,
audio_src: 'h2j6vm17r07jf03/snare.mp3'
},
{
name: 'kick',
x: 80,
y: 250,
w: 200,
h: 230,
audio_src: '1cdwpm3gca9mlo0/kick.mp3'
},
{
name: 'tom',
x: 290,
y: 210,
w: 110,
h: 80,
audio_src: 'h8pvqqol3ovyle8/tom.mp3'
}
],
bg_src: '0jkaeoxls18n3y5/_drumkit.jpg?dl=0',
};
drum.gain = drum.a_ctx.createGain();
drum.gain.gain.value = .5;
drum.gain.connect(drum.a_ctx.destination);
function initCanvas() {
const c = drum.canvas = document.createElement('canvas');
const ctx = drum.ctx = c.getContext('2d');
c.width = drum.bg.width;
c.height = drum.bg.height;
ctx.drawImage(drum.bg, 0, 0);
document.body.appendChild(c);
addEvents(c);
}
const isHover = (x, y) =>
(drum.parts.filter(p => (p.x < x && p.x + p.w > x && p.y < y && p.y + p.h > y))[0] || false);
function addEvents(canvas) {
let mouse_hovered = false;
canvas.addEventListener('mousemove', e => {
mouse_hovered = isHover(e.pageX - canvas.offsetLeft, e.pageY - canvas.offsetTop)
if (mouse_hovered) {
canvas.style.cursor = 'pointer';
} else {
canvas.style.cursor = 'default';
}
})
canvas.addEventListener('mousedown', e => {
e.preventDefault();
if (mouse_hovered) {
drum.generate_sound(mouse_hovered);
}
});
const checkboxes = document.querySelectorAll('input');
checkboxes[0].onchange = function() {
general_solo = this.checked;
general_solo && (checkboxes[1].checked = part_solo = true);
};
checkboxes[1].onchange = function() {
part_solo = this.checked;
!part_solo && (checkboxes[0].checked = general_solo = false);
};
}
Promise.all([initAudios(), initImages()])
.then(initCanvas);
})()
/*
Audio Samples are from https://sampleswap.org/filebrowser-new.php?d=DRUMS+%28FULL+KITS%29%2FSpasm+Kit%2F
Original image is from http://truimg.toysrus.co.uk/product/images/UK/0023095_CF0001.jpg?resize=500:500
*/
<label>general solo<input type="checkbox"></label><br>
<label>part solo<input type="checkbox"></label><br>
You could create an Audio Loader, that loads all the audios and keeps track of them:
function load(srcs){
var obj={};
srcs.forEach(src=>obj[src]=new Audio(src));
return obj;
}
Then you could do sth like this onload:
var audios=load(["audio1.mp3", "audio2.mp3"]);
And later:
(audios[src] || new Audio(src)).play();
This will just load the audio if it isnt already in the audios object.
I am trying to create a class which creates a Crafty entity with specific properties. So far, the functions within the class do not run because 'this' refers to the window object
$(document).ready(function () {
Crafty.init(window.innerWidth, window.innerHeight);
var player = new controller(37,38,39,40);
player.d.color("red").attr({
w: 50,
h: 50,
x: 0,
y: 0
});
// Jump Height = velocity ^ 2 / gravity * 2
// Terminal Velocity = push * (1 / viscosity)
var gravity = 1;
var viscosity = 0.5;
var frame = (1 / 20);
var distanceMultiplier = 10; //pixels per meter
var timeMultiplier = 20; //relative to actual time
var keystart = [];
var keyboard = [];
function controller (controls) {
this.d = Crafty.e();
this.d.addComponent("2D, Canvas, Color, Collision");
this.d.collision();
this.d.mass = 1;
this.d.a = {
extradistance : 0,
velocity : 0,
acceleration : 0,
force : 0,
resistance : 0
};
this.d.a.push = 0;
this.d.v = {
extradistance : 0,
velocity : 0,
acceleration : 0,
force : 0
};
this.d.jumping = true;
this.d.onHit("Collision", function () {
var a = this.d.hit("Collision");
if (a) {
for (var b in a) {
this.d.x = this.d.x - a[b].normal.x * a[b].overlap;
this.d.y = this.d.y - a[b].normal.y * a[b].overlap;
if (a[b].normal.y < -0.5) {
this.d.jumping = false;
}
if (Math.abs(a[b].normal.x) < 0.2) {
this.d.v.velocity = this.d.v.velocity * a[b].normal.y * 0.2;
}
if (Math.abs(a[b].normal.y) < 0.2) {
this.d.a.velocity = this.d.a.velocity * a[b].normal.x * 0.2;
}
}
return;
}
});
this.d.physics = function () {
if (keyboard[arguments[1]] && !this.jumping) {
this.v.velocity = 5;
this.jumping = true;
}
if (keyboard[arguments[1]] && this.jumping) {
var now = new Date();
if (now.getTime() - keystart[arguments[1]].getTime() < 500) {
this.v.velocity = 5;
}
}
if (keyboard[arguments[0]] && keyboard[arguments[2]]) {
this.a.velocity = 0;
} else {
if (keyboard[arguments[0]]) {
this.a.velocity = -3;
}
if (keyboard[arguments[2]]) {
this.a.velocity = 3;
}
}
if (keyboard[arguments[3]]) {
this.v.velocity = -5;
}
this.a.force = this.a.push - this.a.resistance;
this.a.acceleration = this.a.force / this.mass;
this.a.velocity = this.a.velocity + (this.a.acceleration * frame);
this.a.extradistance = (this.a.velocity * frame);
this.a.resistance = this.a.velocity * viscosity;
this.attr({
x: (this.x + (this.a.extradistance * distanceMultiplier))
});
this.v.force = gravity * this.mass;
this.v.acceleration = this.v.force / this.mass;
this.v.velocity = this.v.velocity - (this.v.acceleration * frame);
this.v.extradistance = (this.v.velocity * frame);
this.attr({
y: (this.y - (this.v.extradistance * distanceMultiplier))
});
setTimeout(this.physics, (frame * 1000) / timeMultiplier);
};
this.d.listen = function(){ document.body.addEventListener("keydown", function (code) {
var then = new Date();
if (!keyboard[code.keyCode] && !this.jumping && code.keyCode == arguments[1]) { //only if not yet pressed it will ignore everything until keyup
keyboard[code.keyCode] = true; //start movement
keystart[code.keyCode] = then; //set time
}
if (!keyboard[code.keyCode] && code.keyCode != arguments[1]) { //only if not yet pressed it will ignore everything until keyup
keyboard[code.keyCode] = true; //start movement
keystart[code.keyCode] = then; //set time
}
});
};
}
player.d.physics();
player.d.listen();
document.body.addEventListener("keyup", function (code) {
keyboard[code.keyCode] = false;
});
});
In trying to put the functions as prototypes of the class, I run into a problem.
Crafty.init(500,500);
function block () {
block.d = Crafty.e("2D, Color, Canvas");
block.d.color("red");
block.d.attr({x:0,y:0,h:50,w:50});
}
block.d.prototype.green = function() {
this.color("green");
}
var block1 = new block();
block1.d.color();
If an object is defined in the constructor, I cannot use it to add a prototype to.
Generally in Crafty, we favor composition. That is, you extend an entity by adding more components to it. You can have kind of a hierarchy by having one component automatically add others during init.
I haven't looked through all of your example code, because there's a lot! But consider the second block:
function block () {
block.d = Crafty.e("2D, Color, Canvas");
block.d.color("red");
block.d.attr({x:0,y:0,h:50,w:50});
}
block.d.prototype.green = function() {
this.color("green");
}
var block1 = new block();
block1.d.color();
You're trying to combine Crafty's way of doing things (an entity component system) with classes in a way that's not very idiomatic. Better to do this:
// Define a new component with Crafty.c(), rather than creating a class
Crafty.c("Block", {
// On init, add the correct components and setup the color and dimensions
init: function() {
this.requires("2D, Color, Canvas")
.color("red")
.attr({x:0,y:0,h:50,w:50});
},
// method for changing color
green: function() {
this.color("green");
}
});
// Create an entity with Crafty.e()
block1 = Crafty.e("Block");
// It's not easy being green!
block1.green();
Working on a sort of proof-of-concept object-oriented javascript project that emulates a chessboard. Currently I've got four canvases set up, each set to two different boards and two "sidebar" canvases which display the current turn and the list of any pieces taken for the associated game. Here's a screenshot of what it looks like currently:
http://i.imgur.com/GPoVkK2.png
The problem is, the elements within the second sidebar are for whatever reason drawing in the first sidebar, and I haven't been able to figure out why.
Here's all the code files for the project broken out and explained to the best of my ability:
index.html
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="scripts/Marker.js"></script>
<script type="text/javascript" src="scripts/TurnMarker.js"></script>
<script type="text/javascript" src="scripts/GameToken.js"></script>
<script type="text/javascript" src="scripts/GameBoard.js"></script>
<script type="text/javascript" src="scripts/Validation.js"></script>
<script type="text/javascript" src="scripts/Main.js"></script>
</head>
<body onLoad=initialize()>
<canvas id="gameBoard" width="400" height="400" style="border:1px solid #000000; position=relative;">
Your browser doesn't support HTML5 canvas.
</canvas>
<canvas id="sideBar" width="50" height="400" style="border:1px solid #000000; position=relative;">
</canvas>
<canvas id="gameBoardWizard" width="400" height="400" style="border:1px solid #000000; position=relative;">
Your browser doesn't support HTML5 canvas.
</canvas>
<canvas id="sideBarWizard" width="50" height="400" style="border:1px solid #000000; position=relative;">
</canvas>
</body>
</html>
Main.js
/*** Chess Board Program
* Author: Alex Jensen
* CS 3160: Concepts of Programming Languages
* 12/5/14
*/
var gameBoards = [];
/** Initialize is called when the page loads (set up in the HTML below)
*
*/
function initialize()
{
createBoard("gameBoard", "sideBar" ,8 , 8);
createBoard("gameBoardWizard", "sideBarWizard" ,8, 8);
// setInterval is a super awesome javascript function and I love it.
setInterval(function() {draw()}, 100);
}
/** CreateBoard is a helper function for Initialize that is responsible for loading a game board into the list of game boards.
* boardID= String title of the HTML5 table within the HTML index file.
*/
function createBoard(boardID, sideBarID, numSquaresRows, numSquaresColumns)
{
var initBoardValidator = [];
for (var i=0;i<numSquaresColumns;i++)
{
initBoardValidator[i] = [];
for (var j=0;j<numSquaresRows;j++)
{
initBoardValidator[i][j] = null;
}
}
var gameBoard = new GameBoard(boardID, sideBarID, numSquaresRows, numSquaresColumns, initBoardValidator, [], []);
gameBoard.tokens = createDefaultTokens(gameBoard);
gameBoard.takenTokens = createDefaultTakeMarkers(gameBoard);
gameBoards[gameBoards.length] = gameBoard;
}
/** Helper function for initialize which creates all the tokens and stores them in appropriate locations.
*
*/
function createDefaultTokens(game)
{
tokens = [];
// Create Pawns
for (var i = 0; i < 8; i++)
{
tokens[tokens.length] = new GameToken(game, "WP", 1, 'images/wp.png', i * game.squareWidth, game.squareHeight, pawnValidator);
tokens[tokens.length] = new GameToken(game, "BP", 2, 'images/bp.png', i * game.squareWidth, game.squareHeight * 6, pawnValidator);
}
// Create other pieces
tokens[tokens.length] = new GameToken(game, "WR", 1, 'images/wr.png', 0, 0, rookValidator);
tokens[tokens.length] = new GameToken(game, "WN", 1, 'images/wn.png', game.squareWidth, 0, knightValidator);
tokens[tokens.length] = new GameToken(game, "WB", 1, 'images/wb.png', game.squareWidth * 2, 0, bishopValidator);
tokens[tokens.length] = new GameToken(game, "WQ", 1, 'images/wq.png', game.squareWidth * 3, 0, queenValidator);
tokens[tokens.length] = new GameToken(game, "WK", 1, 'images/wk.png', game.squareWidth * 4, 0, kingValidator);
tokens[tokens.length] = new GameToken(game, "WB", 1, 'images/wb.png', game.squareWidth * 5, 0, bishopValidator);
tokens[tokens.length] = new GameToken(game, "WN", 1, 'images/wn.png', game.squareWidth * 6, 0, knightValidator);
tokens[tokens.length] = new GameToken(game, "WR", 1, 'images/wr.png', game.squareWidth * 7, 0, rookValidator);
tokens[tokens.length] = new GameToken(game, "BR", 2, 'images/br.png', 0, game.squareWidth * 7, rookValidator);
tokens[tokens.length] = new GameToken(game, "BN", 2, 'images/bn.png', game.squareWidth, game.squareHeight * 7, knightValidator);
tokens[tokens.length] = new GameToken(game, "BB", 2, 'images/bb.png', game.squareWidth * 2, game.squareHeight * 7, bishopValidator);
tokens[tokens.length] = new GameToken(game, "BQ", 2, 'images/bq.png', game.squareWidth * 3, game.squareHeight * 7, queenValidator);
tokens[tokens.length] = new GameToken(game, "BK", 2, 'images/bk.png', game.squareWidth * 4, game.squareHeight * 7, kingValidator);
tokens[tokens.length] = new GameToken(game, "BB", 2, 'images/bb.png', game.squareWidth * 5, game.squareHeight * 7, bishopValidator);
tokens[tokens.length] = new GameToken(game, "BN", 2, 'images/bn.png', game.squareWidth * 6, game.squareHeight * 7, knightValidator);
tokens[tokens.length] = new GameToken(game, "BR", 2, 'images/br.png', game.squareWidth * 7, game.squareHeight * 7, rookValidator);
return tokens;
}
function createDefaultTakeMarkers(game)
{
var takenTokens = [];
// Create Pawns
for (var i = 0; i < 8; i++)
{
takenTokens[takenTokens.length] = new Marker("WP", 1, 'images/wp.png', 5, (i * 20) + 5);
takenTokens[takenTokens.length] = new Marker("BP", 1, 'images/bp.png', 5, game.sideBar.height - ((i * 20) + 25));
}
// Create other pieces
takenTokens[takenTokens.length] = new Marker("WR", 1, 'images/wr.png', 25, 5);
takenTokens[takenTokens.length] = new Marker("WN", 1, 'images/wn.png', 25, 25);
takenTokens[takenTokens.length] = new Marker("WB", 1, 'images/wb.png', 25, 45);
takenTokens[takenTokens.length] = new Marker("WQ", 1, 'images/wq.png', 25, 65);
takenTokens[takenTokens.length] = new Marker("WK", 1, 'images/wk.png', 25, 85);
takenTokens[takenTokens.length] = new Marker("WB", 1, 'images/wb.png', 25, 105);
takenTokens[takenTokens.length] = new Marker("WN", 1, 'images/wn.png', 25, 125);
takenTokens[takenTokens.length] = new Marker("WR", 1, 'images/wr.png', 25, 145);
takenTokens[takenTokens.length] = new Marker("BR", 1, 'images/br.png', 25, game.sideBar.height - 25);
takenTokens[takenTokens.length] = new Marker("BN", 1, 'images/bn.png', 25, game.sideBar.height - 45);
takenTokens[takenTokens.length] = new Marker("BB", 1, 'images/bb.png', 25, game.sideBar.height - 65);
takenTokens[takenTokens.length] = new Marker("BQ", 1, 'images/bq.png', 25, game.sideBar.height - 85);
takenTokens[takenTokens.length] = new Marker("BK", 1, 'images/bk.png', 25, game.sideBar.height - 105);
takenTokens[takenTokens.length] = new Marker("BB", 1, 'images/bb.png', 25, game.sideBar.height - 125);
takenTokens[takenTokens.length] = new Marker("BN", 1, 'images/bn.png', 25, game.sideBar.height - 145);
takenTokens[takenTokens.length] = new Marker("BR", 1, 'images/br.png', 25, game.sideBar.height - 165);
console.log(takenTokens);
return takenTokens;
}
/** Helper function for draw responsible for drawing each gameBoard
*
*/
function draw()
{
for (var i = 0; i < gameBoards.length; i++)
{
gameBoards[i].draw();
}
}
GameBoard.js
function bind(scope, fn) {
return function() {
return fn.apply(scope, arguments);
}
}
function GameBoard(boardID, sideBarID, numSquareRows, numSquareColumns, validator, tokens, takenTokens)
{
this.game = document.getElementById(boardID);
this.gameContext = this.game.getContext("2d");
var gamerect = this.game.getBoundingClientRect();
//this.gameContext.translate(gamerect.left, gamerect.top);
this.sideBar = document.getElementById(sideBarID);
this.sideBarContext = sideBar.getContext("2d");
var siderect = this.sideBar.getBoundingClientRect();
//this.sideBarContext.translate(siderect.left, siderect.top);
this.boardWidth = this.game.width;
this.boardHeight = this.game.height;
this.squareWidth = this.boardWidth / numSquareColumns;
this.squareHeight = this.boardHeight / numSquareRows;
if (this.squareHeight % 1 != 0) alert("WARNING: squareHeight is not a solid number, the program might not work correctly! Always ensure that the board height divided by the number of rows comes out as a whole number.");
if (this.squareWidth % 1 != 0) alert("WARNING: squareWidth is not a solid number, the program might not work correctly! Always ensure that the board width divided by the number of columns comes out as a whole number.");
this.validator = validator;
this.tokens = tokens;
this.takenTokens = takenTokens;
this.turnOrderToken = new TurnMarker('images/wturn.png', 'images/bturn.png', siderect.width / 2 - 20, siderect.height / 2 - 20, 40, 40);
this.activePlayer = 1; // Whose turn is it?
this.selectedToken = null; // What token is currently being dragged around?
this.takePiece = null;
// Event listeners function nearly identically to how they are handled in C#.
this.game.addEventListener("mousedown", bind(this, this.onMouseDown), false);
this.game.addEventListener("mousemove", bind(this, this.onMouseMove), false);
this.game.addEventListener("mouseup", bind(this, this.onMouseUp), false);
/** Helper function for drawBoard responsible for swapping between two colors whenever it is called.
*
*/
this.swapColor = function swapColor()
{
if (this.gameContext.fillStyle != '#0000ff')
{
this.gameContext.fillStyle = '#0000ff';
} else {
this.gameContext.fillStyle = '#ffffff';
}
}
/** Responsible for drawing all the tokens
*
*/
this.drawTokens = function drawTokens()
{
for (var i = 0; i < this.tokens.length; i++)
{
var token = this.tokens[i];
this.gameContext.drawImage(token.image, token.x, token.y, this.squareWidth, this.squareHeight);
}
}
/** Responsible for drawing the checkerboard.
*
*/
this.drawBoard = function drawBoard()
{
this.gameContext.clearRect(0, 0, this.boardWidth, this.boardHeight);
for (var i = 0; i < this.boardWidth; i += this.squareWidth)
{
for (var j = 0; j < this.boardHeight; j += this.squareHeight)
{
this.swapColor();
this.gameContext.fillRect(i,j,this.squareWidth,this.squareHeight);
}
this.swapColor();
}
}
this.drawMarkers = function drawMarkers()
{
for (var i = 0; i < this.takenTokens.length; i++)
{
var marker = this.takenTokens[i];
console.log(marker.image + " " + marker.x + " " + marker.y + " " + marker.width + " " + marker.height);
if (marker.visible)
{
this.sideBarContext.drawImage(marker.image, marker.x, marker.y, marker.width, marker.height);
}
}
}
this.drawTurnMarker = function drawTurnMarker()
{
if (this.activePlayer == 1)
{
console.log(this.turnOrderToken.player1Image + " " + this.turnOrderToken.x + " " + this.turnOrderToken.y + " " + this.turnOrderToken.width + " " + this.turnOrderToken.height);
this.sideBarContext.drawImage(this.turnOrderToken.player1Image, this.turnOrderToken.x, this.turnOrderToken.y, this.turnOrderToken.width, this.turnOrderToken.height);
}
else
{
this.sideBarContext.drawImage(this.turnOrderToken.player2Image, this.turnOrderToken.x, this.turnOrderToken.y, this.turnOrderToken.width, this.turnOrderToken.height);
}
}
/** Container method which runs all draw functions on the board.
*
*/
this.draw = function draw()
{
this.drawBoard();
this.drawTokens();
this.drawMarkers();
this.drawTurnMarker();
}
/** Removes tokens from the board and adds them to the list of captured pieces in the sidebar
*
*/
this.capture = function capture(token)
{
for (var i = 0; i < this.tokens.length; i++)
{
var takenToken = this.tokens[i];
if (takenToken.x == token.x && takenToken.y == token.y)
{
this.tokens.splice(i, 1);
break;
}
}
for (var i = 0; i < this.takenTokens.length; i++)
{
var takenToken = this.takenTokens[i];
if (takenToken.name == token.name && takenToken.visible == false)
{
console.log(takenToken);
takenToken.visible = true;
break;
}
}
}
}
/** Event that fires when the mouse button is released
* Listeners in gameBoard
*/
GameBoard.prototype.onMouseUp = function (event)
{
if (this.selectedToken != null)
{
var gridx = Math.round(this.selectedToken.x / this.squareWidth);
var gridy = Math.round(this.selectedToken.y / this.squareHeight);
// Snap to the nearest tile
this.selectedToken.x = (gridx * this.squareWidth);
this.selectedToken.y = (gridy * this.squareHeight);
// Check to see if the move that was made is legal
this.takePiece = this.validator[gridx][gridy];
if (this.selectedToken.movementValidator())
{
// If it was, then advance the turn
if (this.activePlayer == 1)
{
this.activePlayer = 2;
}
else
{
this.activePlayer = 1;
}
}
else
{
// Otherwise move the token back to where it was
this.selectedToken.x = this.selectedToken.initX;
this.selectedToken.y = this.selectedToken.initY;
}
// Wherever the token ends up, update the grid to reflect that.
this.validator[this.selectedToken.initX / this.squareWidth][this.selectedToken.initY / this.squareHeight] = null;
this.validator[this.selectedToken.x / this.squareWidth][this.selectedToken.y / this.squareHeight] = this.selectedToken;
this.selectedToken = null;
}
}
/**
*
*/
GameBoard.prototype.onMouseDown = function(event)
{
var rect = this.game.getBoundingClientRect();
var mousePos = {x:event.clientX - rect.left, y:event.clientY - rect.top};
for (var i = 0; i < this.tokens.length; i++)
{
token = this.tokens[i];
// if you clicked this token and it's your turn
if (mousePos.x > token.x && mousePos.y > token.y && mousePos.x < token.x + this.squareWidth && mousePos.y < token.y + this.squareHeight && token.player == this.activePlayer)
{
this.selectedToken = token;
// Store where the token was before we picked it up. That way if we make an illegal move we can restore it to its initial location
this.selectedToken.initX = token.x;
this.selectedToken.initY = token.y;
}
}
}
/** Event that fires when the mouse position is updated
* Listeners in gameBoard
*/
GameBoard.prototype.onMouseMove = function(event)
{
if (this.selectedToken != null)
{
var rect = this.game.getBoundingClientRect();
var mousePos = {x:event.clientX - rect.left, y:event.clientY - rect.top};
this.selectedToken.x = mousePos.x - (this.squareWidth / 2);
this.selectedToken.y = mousePos.y - (this.squareHeight / 2);
}
}
Marker.js
/** Marker is a visual widget used to show taken pieces.
* player= The player associated with the marker
* tokenImagePath= The valid path to the location of the marker texture
* x,y= location of marker on the sidebar
*/
function Marker(name, player, markerPath, x, y)
{
this.name = name;
this.image = new Image();
this.x = x;
this.y = y;
this.width = 20;
this.height = 20;
this.player = player;
this.image.src = markerPath;
this.visible = true;
}
GameToken.js
/** GameToken represents a chess piece.
* player= The player the chess piece belongs to
* tokenImagePath= The valid path to the location of the chess piece texture
* x,y= location of token on the board
* movementValidator= Function to determine whether a move made by this token is legal or not.
* Validators are different for different types of tokens.
*/
function GameToken(game, name, player, tokenImagePath, x, y, movementValidator)
{
this.game = game;
this.name = name;
this.image = new Image();
this.x = x;
this.y = y;
game.validator[x / game.squareWidth][y / game.squareHeight] = this;
this.player = player;
this.image.src = tokenImagePath;
this.movementValidator = movementValidator;
}
And finally a WIP Validation script to check for legal moves
/** Specific validation code for Pawns.
*
*/
function pawnValidator()
{
// Pawns are tricky to validate because they can move one square directly forward, but can't take the square directly
// in front of them, and can only move diagonally when they can capture. In addition, they can move two squares
// forward as long as they're in starting position.
if (this.takePiece != null)
{
// If the square we moved to has an enemy in it and we've made a legal move with the pawn to take that piece
if ((this.takePiece.player != this.player &&
(this.x == this.initX + this.squareWidth || this.x == this.initX - this.squareWidth) &&
((this.player == 1 && this.y == this.initY + this.squareHeight) ||
(this.player == 2 && this.y == this.initY - this.squareHeight))))
{
// We're allowed to remove the token here because we've validated that the pawn has made the correct movement to take the piece.
capture(takePiece);
takePiece = null;
return true;
}
else
{
takePiece = null;
return false;
}
}
// The pawn is not capturing, so check to see that the move it is making is legal
else if (this.x == this.initX)
{
if (this.player == 1)
{
if ((this.y == this.initY + this.game.squareHeight) || (this.y == this.initY + (2*this.game.squareHeight)) && this.initY == (this.game.squareHeight))
{
return true;
}
}
else if (this.y == this.initY - this.game.squareHeight || (this.y == this.initY - (2*this.game.squareHeight)) && this.initY == (6*this.game.squareHeight))
{
{
return true;
}
}
}
return false;
}
/** Specific validation code for Rooks.
*
*/
function rookValidator()
{
// First check if the movement made was legal for a rook (straight line)
if (this.x == this.initX || this.y == this.initY)
{
// Next check if the movement made went through any other pieces
if (lineValidation(this.initX, this.initY, this.x, this.y))
{
if (this.game.takePiece != null)
{
this.game.capture(this.game.takePiece);
}
this.game.takePiece = null;
return true;
}
}
return false;
}
/** Specific validation code for Knights.
*
*/
function knightValidator()
{
// First check if the movement made was legal for a knight using relative positioning
var relativeX = Math.abs(this.x - this.initX) / this.game.squareWidth;
var relativeY = Math.abs(this.y - this.initY) / this.game.squareHeight;
if ((relativeX == 1 && relativeY == 2) || (relativeX == 2 && relativeY == 1))
{
// Knights can jump, so we don't need to validate the movement further
if (this.game.takePiece != null)
{
this.game.capture(this.game.takePiece);
}
takePiece = null;
return true;
}
}
/** Specific validation code for Bishops.
*
*/
function bishopValidator()
{
// First check if the movement made was legal for a bishop (diagonal line)
if (Math.abs(this.x - this.initX) == Math.abs(this.y - this.initY))
{
// Next check if the movement made went through any other pieces
if (lineValidation(this.initX, this.initY, this.x, this.y))
{
if (takePiece != null)
{
capture(takePiece);
}
takePiece = null;
return true;
}
}
}
/** Specific validation code for Kings.
*
*/
function kingValidator()
{
// First check if the movement made was legal for a king using relative positioning
var relativeX = Math.abs(this.x - this.initX) / squareSize;
var relativeY = Math.abs(this.y - this.initY) / squareSize;
if ((relativeX == 1 && relativeY == 1) || (relativeX == 1 && relativeY == 0) || (relativeX == 0 && relativeY == 1))
{
// TODO: Check to see if the move puts the king in check. That's a little past the scope of this project but would make for a nice addition.
if (takePiece != null)
{
capture(takePiece);
}
takePiece = null;
return true;
}
}
/** Specific validation code for Queens.
*
*/
function queenValidator()
{
// First check if the movement made was legal for a queen (diagonal line or straight line)
if ((Math.abs(this.x - this.initX) == Math.abs(this.y - this.initY)) ||
(this.x == this.initX || this.y == this.initY))
{
// Next check if the movement made went through any other pieces
if (lineValidation(this.initX, this.initY, this.x, this.y))
{
if (takePiece != null)
{
capture(takePiece);
}
takePiece = null;
return true;
}
}
}
/** Checks each square traveled over a line to see if it has traveled through another piece
* IMPORTANT: This function only works if the move is legal! If the move made is impossible in chess this function
* will not work correctly!
*/
function lineValidation(startX, startY, endX, endY)
{
while (startX != endX || startY != endY)
{
if (startX < endX) startX += squareSize;
if (startY < endY) startY += squareSize;
if (startX > endX) startX -= squareSize;
if (startY > endY) startY -= squareSize;
var checkTake = gameBoardValidator[startX / squareSize][startY / squareSize];
if (checkTake != null && (startX != endX || startY != endY))
{
return false;
}
}
return true;
}
Both games work but the UI elements in that second side canvas always seem to be drawing in the first canvas spot. Anybody see what I goofed up?
In your GameBoard object you are trying to get context from window.sideBar instead of this.sideBar (it should have produced an error in console):
this.sideBarContext = sideBar.getContext("2d");
Change that line to:
this.sideBarContext = this.sideBar.getContext("2d");
^^^^
and it should work.