body { margin: 0; }
const TAU = 2 * Math.PI;
class Actor extends Phaser.GameObjects.Sprite {
constructor(scene, x, y, texture) {
super(scene, x, y, texture);
scene.add.existing(this);
this.halfWidth;
this.halfHeight;
}
getBottomLeft(output) {
if (!output) {
output = new Phaser.Math.Vector2();
}
output.x = Math.floor(this.x - this.halfWidth);
output.y = Math.floor(this.y + this.halfHeight);
return output;
}
getBottomRight(output) {
if (!output) {
output = new Phaser.Math.Vector2();
}
output.x = Math.floor(this.x + this.halfWidth - 1);
output.y = Math.floor(this.y + this.halfHeight);
return output;
}
getRect(output) {
if (!output) {
output = new Phaser.Geom.Rectangle();
}
output.set(
this.x - this.halfWidth,
this.y - this.halfHeight,
2 * this.halfWidth,
2 * this.halfHeight
);
return output;
}
isAir(coord) {
return this.scene.canvas.getPixel(coord.x, coord.y).a === 0;
}
checkBelow() {
const bottomleft = this.getBottomLeft();
const bottomright = this.getBottomRight();
let leftbelow = 0;
while (leftbelow < 3) {
if (!this.isAir(bottomleft)) break;
leftbelow++;
bottomleft.add(Phaser.Math.Vector2.DOWN);
}
let rightbelow = 0;
while (rightbelow < 3) {
if (!this.isAir(bottomright)) break;
rightbelow++;
bottomright.add(Phaser.Math.Vector2.DOWN);
}
return {
isOnGround: leftbelow === 0 || rightbelow === 0,
leftbelow: leftbelow,
rightbelow: rightbelow,
maxbelow: Math.max(leftbelow, rightbelow),
minbelow: Math.min(leftbelow, rightbelow)
};
}
}
class Lemming extends Actor {
constructor(scene, x, y) {
super(scene, x, y, "lemming");
this.direction = 1;
this.velocity = 0.2;
this.climbheight = 2;
this.status = Lemming.Status.Falling;
this.statusBelow = this.checkBelow();
this.statusFront;
this.debug = false;
this.on("animationcomplete-dig", this.onDigComplete, this);
this.once("animationcomplete-explode", this.onExplodeComplete, this);
}
update(t, d) {
this.statusBelow = this.checkBelow();
const prevStatus = this.status;
if (this.statusBelow.isOnGround) {
if (prevStatus === Lemming.Status.Falling) {
this.status = Lemming.Status.Walking;
}
} else {
this.status = Lemming.Status.Falling;
}
switch (this.status) {
case Lemming.Status.Falling:
this.disableInteractive();
this.y += 0.5;
if (this.statusBelow.leftbelow > 2 && this.statusBelow.rightbelow > 2) {
this.play("fall", true);
}
break;
case Lemming.Status.Walking:
this.setInteractive();
this.play("walk", true);
this.velocity = 0.2;
this.halfWidth = 3;
this.halfHeight = 6;
this.statusFront = this.checkFront(this.direction);
if (this.statusFront.canProceed) {
this.x += this.velocity * this.direction;
// rise up to new ground level
this.y -= this.statusFront.height;
} else {
this.direction *= -1; // turn the lemming around if the ground in front is too high to climb
this.setFlipX(this.direction === 1 ? false : true);
}
break;
case Lemming.Status.Digging:
this.disableInteractive();
this.halfWidth = 6;
this.play("dig", true);
break;
case Lemming.Status.Exploding:
this.disableInteractive();
this.play("explode", true);
break;
}
if (this.debug) {
const bl = this.getBottomLeft();
const br = this.getBottomRight();
const ctx = this.scene.debugCanvas.context;
const { leftbelow, rightbelow, minbelow, maxbelow } = this.statusBelow;
ctx.fillStyle = this.statusBelow.isOnGround ? "red" : "yellow";
ctx.fillRect(bl.x, bl.y, 1, 1 + leftbelow);
ctx.fillRect(br.x, br.y, 1, 1 + rightbelow);
ctx.fillText(this.status, this.x, this.y);
// ctx.fillText(`${leftbelow} ${rightbelow}`, this.x, this.y);
}
}
checkFront(direction) {
let positioninfront = new Phaser.Math.Vector2();
let height = 0;
let canProceed = false;
// find the height of the ground in front of a lemming
// up to the maximum height a lemming can climb
while (!canProceed && height <= this.climbheight) {
// the pixel 'in front' of a lemming will depend on the direction it's traveling
if (direction === 1) {
// facing right
const { x, y } = this.getBottomRight();
positioninfront.set(x + 1, y - 1 - height);
} else {
// facing left
const { x, y } = this.getBottomLeft();
positioninfront.set(x - 1, y - 1 - height);
}
if (this.isAir(positioninfront)) canProceed = true;
height++;
}
if (this.debug) {
const ctx = this.scene.debugCanvas.context;
ctx.fillStyle = canProceed ? "#0f0" : "#f00";
ctx.fillRect(positioninfront.x, positioninfront.y, 1, 1);
}
return { canProceed: canProceed, height: height - 1 };
}
onDigComplete() {
const left = this.getBottomLeft();
const right = this.getBottomRight();
const ctx = this.scene.canvas.context;
ctx.fillRect(left.x, left.y - 2, right.x - left.x, 3);
this.scene.canvas.update();
console.count('update');
this.y += 1;
}
onExplodeComplete() {
Phaser.Utils.Array.Remove(this.scene.lemmings, this);
const ctx = this.scene.canvas.context;
ctx.beginPath();
ctx.arc(this.x, this.y, 16, 0, TAU);
ctx.fill();
this.scene.canvas.update();
console.count('update');
this.destroy();
}
onPointerDown(pointer) {
if (!this.statusBelow.isOnGround) return;
if (pointer.event.shiftKey) {
this.status = Lemming.Status.Exploding;
} else {
this.status = Lemming.Status.Digging;
}
}
}
Lemming.Status = {
Walking: 1,
Falling: 2,
Digging: 3,
Exploding: 4
};
class Game extends Phaser.Scene {
constructor() {
super({ key: "Game" });
this.lemmings = [];
this.max_lemmings = 10;
this.startPos = new Phaser.Math.Vector2(100, 100);
this.timer = 0;
this.interval = 10;
}
preload() {
this.load.setBaseURL("https://raw.githubusercontent.com/cedarcantab/LEMMINGS/main/");
this.load.image("level1", "assets/level1.png");
this.load.spritesheet("lemming", "assets/lemming.png", {
frameWidth: 16,
frameHeight: 16
});
this.load.spritesheet("cursors", "assets/cursors.png", {
frameWidth: 29,
frameHeight: 29
});
}
create() {
this.debugCanvas = this.textures.createCanvas("debug", 512, 256);
this.debugCanvas.context.font = "small-caption";
this.canvas = this.textures.createCanvas("canvastexture", 512, 256);
this.canvas.drawFrame("level1", "__BASE", 0, 0);
this.canvas.context.globalCompositeOperation = "destination-out";
this.add.image(0, 0, "canvastexture").setOrigin(0).setAlpha(1);
const cam = this.cameras.main;
cam.setBounds(0, 0, 512, 256);
cam.setScroll(115, 0);
this.createAnims();
this.cursor = this.add.image(270, 100, "cursors", 3).setScale(0.5);
this.add.image(0, 0, "debug").setOrigin(0, 0);
const timer = this.time.addEvent({
delay: 1000,
startAt: 1000,
callback: this.spawn,
callbackScope: this,
repeat: this.max_lemmings - 1
});
this.input.on("pointermove", (pointer) => {
this.cursor.x = pointer.worldX;
this.cursor.y = pointer.worldY;
});
this.input.on("gameobjectover", (pointer, gameObject) => {
this.cursor.setFrame(2);
});
this.input.on("gameobjectout", (pointer, gameObject) => {
this.cursor.setFrame(3);
});
this.input.on("gameobjectdown", (pointer, gameObject) => {
gameObject.onPointerDown(pointer);
});
}
spawn() {
const lemming = new Lemming(this, 220, 40).setInteractive();
this.lemmings.push(lemming);
}
createAnims() {
this.anims.create({
key: "walk",
frames: this.anims.generateFrameNumbers("lemming", { start: 0, end: 8 }),
frameRate: 10,
repeat: -1
});
this.anims.create({
key: "fall",
frames: this.anims.generateFrameNumbers("lemming", {
start: 32,
end: 35
}),
frameRate: 10,
repeat: -1
});
this.anims.create({
key: "dig",
frames: this.anims.generateFrameNumbers("lemming", {
start: 128,
end: 135
}),
frameRate: 10
});
this.anims.create({
key: "explode",
frames: this.anims.generateFrameNumbers("lemming", {
start: 208,
end: 221
}),
frameRate: 10
});
}
update(t, d) {
this.debugCanvas.clear();
this.lemmings.forEach((lemming) => lemming.update(t, d));
this.debugCanvas.refresh();
}
}
const game = new Phaser.Game({
// type: Phaser.CANVAS,
width: 320,
height: 200,
zoom: 4,
roundPixels: true,
// backgroundColor: 0xff00ff,
scene: [Game]
});
This Pen doesn't use any external CSS resources.