/**
* Attack of the Crimson Plumber
* Copyright 2012 Dylan McCall, www.dylanmccall.com
*
* Attack of the Crimson Plumber is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Attack of the Crimson Plumber is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Attack of the Crimson Plumber. If not, see .
*/
import 'dart:html';
import 'dart:math' as math;
import 'utility.dart';
void showOverlay (String message) {
query("#game-overlay").style.setProperty("display", "block");
query("#game-overlay-message").text = message;
}
void hideOverlay () {
query("#game-overlay").style.setProperty("display", "none");
}
void gameFinishedCb (int gameResult) {
if (gameResult == Game.RESULT_VICTORY) {
showOverlay("You won! Finally, some peace and quiet.");
query("#start-game").text = "Play again";
} else if (gameResult == Game.RESULT_FAILURE) {
showOverlay("You have been defeated by that dastardly crimson plumber.");
query("#start-game").text = "Play again";
}
}
Game game = null;
void startGame () {
query("#start-game").text = "Restart";
hideOverlay();
if (game != null) game.stopped = true;
game = new Game(query("#game-container"), query("#game-lives-counter"), gameFinishedCb);
game.start();
}
void main () {
startGame();
query("#start-game").on.click.add((event) {
startGame();
}, false);
}
AbstractTile highlightTile = null;
class Game {
CanvasElement canvas;
DivElement livesCounter;
num width, height;
num renderTime;
static const int RESULT_VICTORY = 1;
static const int RESULT_FAILURE = 2;
var finishCb;
List friendlyEntities;
List targetEntities;
List attackerSpawnTiles;
int invaders = 0;
num deathTime = 0;
bool waitToSpawn = false;
bool finished = false;
bool stopped = false;
Scene scene;
Game(this.canvas, this.livesCounter, this.finishCb) {
}
void start() {
this.friendlyEntities = new List();
this.targetEntities = new List();
this.invaders = 3;
this.finished = false;
this.stopped = false;
// Measure the canvas element.
window.requestLayoutFrame(() {
var sceneMap = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,50, 0, 0, 0, 0,51, 0,10, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 2, 2, 2,50, 0, 0, 0, 0, 0,61, 1, 1, 1, 1, 1, 2, 2, 2],
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2,50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,50, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[20,0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,61, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
];
this.scene = new Scene(sceneMap[0].length, sceneMap.length);
this.width = this.scene.worldWidth;
this.height = this.scene.worldHeight;
this.canvas.width = this.width;
this.canvas.height = this.height;
this.renderTime = new Date.now().millisecondsSinceEpoch;
this.attackerSpawnTiles = new List();
for (int row = 0; row < sceneMap.length; row++) {
var rowList = sceneMap[row];
for (int col = 0; col < rowList.length; col++) {
var cell = rowList[col];
var tile = this.scene.getTile(new Vector(col, row));
switch (cell) {
case 1:
var entity = new WallEntity();
tile.add(entity);
break;
case 2:
var entity = new CollapsingWallEntity();
tile.add(entity);
break;
case 50:
var entity = new PlayerTriggerEntity();
tile.add(entity);
break;
case 51:
var entity = new WalkTriggerEntity();
tile.add(entity);
break;
case 61:
var entity = new FireLauncherEntity();
tile.add(entity);
break;
case 9:
var entity = new LavaEntity();
tile.add(entity);
break;
case 10:
var worldEntity = new CaptainVillain(this.scene, tile.getWorldPosition());
this.scene.addEntity(worldEntity);
this.friendlyEntities.add(worldEntity);
break;
case 20:
attackerSpawnTiles.add(tile);
break;
}
}
}
this.canvas.on.click.add(this.onClickListener);
this.canvas.on.mouseMove.add(this.onMouseMoveListener);
this.requestRedraw();
});
}
Vector getMouseEventCoordinates(event) {
ClientRect rect = this.canvas.getBoundingClientRect();
Vector clientVector = new Vector(event.pageX, event.pageY);
return new Vector(event.pageX - rect.left.toInt(), event.pageY - rect.top.toInt());
}
void onClickListener(MouseEvent event) {
Vector mousePosition = this.getMouseEventCoordinates(event);
var clickTile = this.scene.getTileForScreenPosition(mousePosition);
clickTile.activate(TileEntity.ACTIVATE_PLAYER);
}
void onMouseMoveListener(MouseEvent event) {
Vector mousePosition = this.getMouseEventCoordinates(event);
var hoverTile = this.scene.getTileForScreenPosition(mousePosition);
highlightTile = hoverTile;
}
void requestRedraw() {
window.requestAnimationFrame(this.draw);
}
void draw(num highResTime) {
if (this.stopped) return;
num time = new Date.now().millisecondsSinceEpoch;
num timeDelta = time - this.renderTime;
if (this.renderTime != null) {
//showFps((1000 / (time - renderTime)).round());
}
this.renderTime = time;
var context = this.canvas.context2d;
context.setFillColorRgb(226, 215, 188, 0.3);
CanvasGradient bgGradient = context.createLinearGradient(0, this.canvas.height-48, 0, this.canvas.height);
bgGradient.addColorStop(0, 'rgba(226, 215, 188, 0.3)');
bgGradient.addColorStop(1, 'rgba(231, 167, 140, 0.3)');
context.fillStyle = bgGradient;
context.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.scene.update(timeDelta, time);
this.scene.draw(context);
bool friendliesAlive = false;
this.friendlyEntities.forEach((friendlyEntity) {
friendliesAlive = friendliesAlive || friendlyEntity.isAlive;
});
bool targetsAlive = false;
this.targetEntities.forEach((targetEntity) {
targetsAlive = targetsAlive || targetEntity.isAlive;
});
if (! targetsAlive) {
livesCounter.text = "Invaders: $invaders";
if (invaders > 0) {
targetsAlive = true;
if (this.waitToSpawn && time - this.deathTime < 1500) {
// Wait a second for the new invader to appear
} else if (! this.waitToSpawn) {
// Set the spawn timer!
this.deathTime = time;
this.waitToSpawn = true;
} else {
var random = new math.Random();
var spawnTileIndex = random.nextInt(this.attackerSpawnTiles.length);
AbstractTile spawnTile = this.attackerSpawnTiles[spawnTileIndex];
var worldEntity = new CrimsonPlumber(this.scene, spawnTile.getWorldPosition());
this.scene.addEntity(worldEntity);
this.targetEntities.add(worldEntity);
invaders -= 1;
}
}
} else {
num allInvaders = invaders+1;
livesCounter.text = "Invaders: $allInvaders";
this.waitToSpawn = false;
}
if (! this.finished) {
if (! targetsAlive) {
this.finishCb(Game.RESULT_VICTORY);
this.finished = true;
} else if (! friendliesAlive) {
this.finishCb(Game.RESULT_FAILURE);
this.finished = true;
}
}
this.requestRedraw();
}
}
class Scene {
var _tiles;
var _worldEntities;
var _width, _height;
var _tileSize = 32;
int get worldWidth => this._width * this._tileSize;
int get worldHeight => this._height * this._tileSize;
Scene (this._width, this._height) {
this._tiles = new Map();
this._worldEntities = new List();
}
Vector tilePositionForScreenPosition (Vector screenPosition) {
int tileScreenSize = this._tileSize;
return new Vector(screenPosition.x ~/ tileScreenSize, screenPosition.y ~/ tileScreenSize);
}
Vector screenPositionForTilePosition (Vector tilePosition) {
int tileScreenSize = this._tileSize;
return new Vector(tilePosition.x * tileScreenSize, tilePosition.y * tileScreenSize);
}
Vector tilePositionForWorldPosition (Vector worldPosition) {
return new Vector(worldPosition.x ~/ this._tileSize, worldPosition.y ~/ this._tileSize);
}
Vector screenPositionForWorldPosition (Vector tilePosition) {
return new Vector(tilePosition.x * this._tileSize, tilePosition.y * this._tileSize);
}
AbstractTile getTile (Vector position) {
if (position.inBounds(new Vector(0,0), new Vector(this._width, this._height))) {
if (! this._tiles.containsKey(position)) {
this._tiles[position] = new Tile(this, position);
}
return this._tiles[position];
} else {
return new NullTile(this, position);
}
}
AbstractTile getTileForScreenPosition (Vector screenPosition) {
return this.getTile(this.tilePositionForScreenPosition(screenPosition));
}
AbstractTile getTileForWorldPosition (Vector worldPosition) {
return this.getTile(this.tilePositionForWorldPosition(worldPosition));
}
TilesList getTilesForWorldArea (Vector worldPosition, Vector worldSize) {
Vector startPosition = this.tilePositionForWorldPosition(worldPosition);
Vector endPosition= this.tilePositionForWorldPosition(worldPosition+worldSize);
var tiles = new List();
for (num x = startPosition.x; x <= endPosition.x; x++) {
for (num y = startPosition.y; y <= endPosition.y; y++) {
AbstractTile tile = this.getTile(new Vector(x, y));
tiles.add(tile);
}
}
return new TilesList(tiles);
}
void addEntity (WorldEntity entity) {
this._worldEntities.add(entity);
}
void update (num timeDelta, num time) {
for (num x = 0; x < this._width; x++) {
for (num y = 0; y < this._height; y++) {
var tilePosition = new Vector(x,y);
this.getTile(tilePosition).update(timeDelta, time);
}
};
var worldEntityTiles = new Map>();
this._worldEntities.forEach((WorldEntity entity) {
var gravity = new DoubleVector(0.0, entity.weight);
entity.update(timeDelta, time, gravity);
var entityTile = entity.getCurrentTile();
if (worldEntityTiles[entityTile] == null) worldEntityTiles[entityTile] = new List();
worldEntityTiles[entityTile].add(entity);
});
// World Entity collisions
worldEntityTiles.values.forEach((entityList) {
// This is generally not completely insane because world entities will rarely be in the same place
entityList.forEach((worldEntityA) {
entityList.forEach((worldEntityB) {
if (worldEntityB != worldEntityA) worldEntityA.worldCollision(worldEntityB);
});
});
});
}
void draw (CanvasRenderingContext2D context) {
// Draw all tile entities in the current view
// FIXME: We need a better way to iterate through tiles - one that supports arbitrary orders and fun stuff like that
var tileScreenSize = new Vector(this._tileSize, this._tileSize);
for (num x = 0; x < this._width; x++) {
for (num y = 0; y < this._height; y++) {
var tilePosition = new Vector(x,y);
var tileScreenPosition = this.screenPositionForTilePosition(tilePosition);
this.getTile(tilePosition).draw(context, tileScreenPosition, tileScreenSize);
}
}
this._worldEntities.forEach((WorldEntity entity) {
entity.draw(context);
});
}
}
// FIXME: This SHOULD extend List, but Dart is being a bit funny with that
// So, for now, it's a weird wrapper thing
class TilesList {
List tiles;
TilesList(this.tiles);
List getCollision(var testEntity) {
var collisions = new List();
this.tiles.forEach((tile) {
collisions.addAll(tile.getCollision(testEntity));
});
return collisions;
}
}
// Okay, this isn't very abstract any more. But fuck it, I'm not renaming this.
abstract class AbstractTile {
var _scene, _position;
var _entities;
const size = 32;
AbstractTile(this._scene, this._position) {
this._entities = new List();
}
void add (TileEntity entity);
List getAttached() {
return this._entities;
}
void update (num timeDelta, num time) {
this._entities.forEach((TileEntity tileEntity) {
tileEntity.update(timeDelta, time);
});
}
void draw (CanvasRenderingContext2D context, Vector screenPosition, Vector screenSize) {
this._entities.forEach((tileEntity) {
if (tileEntity.isVisible()) {
tileEntity.draw(context, this, screenPosition, screenSize);
}
});
}
bool hasCollisions (var testEntity) {
return this.getCollision(testEntity).length > 0;
}
List getCollision (var testEntity) {
var collisions = new List();
this._entities.forEach((TileEntity tileEntity) {
if (tileEntity.getCollision(testEntity)) collisions.add(tileEntity);
});
return collisions;
}
bool isDeadly (WorldEntity testEntity) {
bool deadly = false;
this._entities.forEach((TileEntity tileEntity) {
deadly = deadly || tileEntity.isDeadly(testEntity);
});
return deadly;
}
Vector getWorldPosition () {
return this._scene.screenPositionForTilePosition(this._position);
}
AbstractTile getNeighbour (Vector direction) {
return this._scene.getTile(this._position + direction);
}
AbstractTile getNeighbour2 (int x, int y) {
return this.getNeighbour(new Vector(x, y));
}
void spawnEntity (spawnFunction) {
var entity = spawnFunction(this._scene, this.getWorldPosition());
this._scene.addEntity(entity);
}
bool activate (int entityActivateType) {
this._entities.forEach((tileEntity) {
tileEntity.activate(entityActivateType, this);
});
}
}
class NullTile extends AbstractTile {
NullTile(scene, position) : super(scene, position);
void add (TileEntity entity) {}
void spawnEntity (WorldEntity entity) {}
}
class Tile extends AbstractTile {
var _entities;
Tile(scene, position) : super(scene, position);
void add (var entity) {
this._entities.add(entity);
}
void draw (CanvasRenderingContext2D context, Vector screenPosition, Vector screenSize) {
super.draw(context, screenPosition, screenSize);
if (highlightTile == this) {
context.beginPath();
context.rect(screenPosition.x, screenPosition.y, screenSize.x, screenSize.y);
context.setStrokeColorRgb(60, 60, 60, 0.6);
context.stroke();
context.closePath();
}
}
List getAttached() {
return this._entities;
}
}
abstract class TileEntity {
bool isVisible () {
return true;
}
bool getCollision(WorldEntity entity) {
return true;
}
bool isDeadly(var entity) {
return false;
}
void update (num timeDelta, num time) {}
void drawInner (CanvasRenderingContext2D context, Tile tile, num width, num height);
void draw (CanvasRenderingContext2D context, Tile tile, Vector screenPosition, Vector screenSize) {
context.save();
context.translate(screenPosition.x, screenPosition.y);
this.drawInner(context, tile, screenSize.x, screenSize.y);
context.restore();
}
static int ACTIVATE_TRIGGER = 1;
static int ACTIVATE_COLLAPSE = 2;
static int ACTIVATE_TOUCHING = 50;
static int ACTIVATE_PLAYER = 100;
void activate (int activateType, Tile tile) {
}
}
class WallEntity extends TileEntity {
void drawInner (CanvasRenderingContext2D context, Tile tile, num width, num height) {
var leftNeighbour = tile.getNeighbour(new Vector(-1,0));
context.beginPath();
context.rect(0, 0, width, height);
if (leftNeighbour.getAttached().length > 0) {
context.setFillColorRgb(10, 10, 10, 0.9);
} else {
context.setFillColorRgb(10, 10, 10, 0.9);
}
context.fill();
context.closePath();
}
}
class CollapsingWallEntity extends WallEntity {
bool _collapsed = false;
Tile _collapseTile = null;
num _collapseProgress = 0;
bool getCollision (var entity) {
return ! this._collapsed;
}
void update (num timeDelta, num time) {
if (this._collapsed) {
// Do the collapsing thing
this._collapseProgress += (timeDelta / 2);
// Carry activation to all horizontal neighbours
if (this._collapseProgress > 38) {
this._collapseTile.getNeighbour(new Vector(0, -1)).activate(TileEntity.ACTIVATE_COLLAPSE);
this._collapseTile.getNeighbour(new Vector(+1, 0)).activate(TileEntity.ACTIVATE_COLLAPSE);
this._collapseTile.getNeighbour(new Vector(0, +1)).activate(TileEntity.ACTIVATE_COLLAPSE);
this._collapseTile.getNeighbour(new Vector(-1, 0)).activate(TileEntity.ACTIVATE_COLLAPSE);
}
}
}
void draw (CanvasRenderingContext2D context, Tile tile, Vector screenPosition, Vector screenSize) {
num alpha = 0.9;
if (this._collapsed) alpha = 0.2;
num offsetY = this._collapseProgress;
context.beginPath();
context.rect(screenPosition.x, screenPosition.y + offsetY + 1, screenSize.x, screenSize.y - 2);
context.setFillColorRgb(144, 144, 144, alpha*0.1);
context.fill();
context.closePath();
context.beginPath();
context.rect(screenPosition.x, screenPosition.y + offsetY + 1, screenSize.x / 2, screenSize.y - 2);
context.rect(screenPosition.x + (screenSize.x / 2), screenPosition.y + offsetY + 1, screenSize.x / 2, screenSize.y - 2);
context.setStrokeColorRgb(50, 50, 50, alpha);
context.stroke();
context.closePath();
}
void activate (int activateType, Tile tile) {
if (activateType == TileEntity.ACTIVATE_TRIGGER ||
activateType == TileEntity.ACTIVATE_COLLAPSE) {
if (! this._collapsed) {
this._collapsed = true;
this._collapseTile = tile;
}
}
}
}
class TriggerEntity extends TileEntity {
bool toggled = false;
bool getCollision(WorldEntity entity) {
return false;
}
void drawInner (CanvasRenderingContext2D context, Tile tile, num width, num height) {
}
void trigger(Tile tile) {
if (this.toggled) return;
this.toggled = true;
tile.getNeighbour(new Vector(-1, -1)).activate(TileEntity.ACTIVATE_TRIGGER);
tile.getNeighbour(new Vector(0, -1)).activate(TileEntity.ACTIVATE_TRIGGER);
tile.getNeighbour(new Vector(+1, -1)).activate(TileEntity.ACTIVATE_TRIGGER);
tile.getNeighbour(new Vector(+1, 0)).activate(TileEntity.ACTIVATE_TRIGGER);
tile.getNeighbour(new Vector(+1, +1)).activate(TileEntity.ACTIVATE_TRIGGER);
tile.getNeighbour(new Vector(0, +1)).activate(TileEntity.ACTIVATE_TRIGGER);
tile.getNeighbour(new Vector(-1, +1)).activate(TileEntity.ACTIVATE_TRIGGER);
tile.getNeighbour(new Vector(-1, 0)).activate(TileEntity.ACTIVATE_TRIGGER);
}
}
class PlayerTriggerEntity extends TriggerEntity {
void drawInner (CanvasRenderingContext2D context, Tile tile, num width, num height) {
context.beginPath();
context.arc(width/2, height/2, width/3, 0, 2*math.PI, false);
if (this.toggled) {
context.setFillColorRgb(130, 157, 139, 1);
context.setStrokeColorRgb(118, 139, 125, 1);
} else {
context.setFillColorRgb(95, 207, 109, 1);
context.setStrokeColorRgb(144, 207, 152, 1);
}
context.fill();
context.stroke();
context.closePath();
}
void activate (int activateType, Tile tile) {
if (activateType == TileEntity.ACTIVATE_PLAYER) {
this.trigger(tile);
}
}
}
class WalkTriggerEntity extends TriggerEntity {
void drawInner (CanvasRenderingContext2D context, Tile tile, num width, num height) {
var buttonHeight;
if (this.toggled) {
buttonHeight = (height-5) / 3;
} else {
buttonHeight = (height-5) / 2;
}
context.beginPath();
context.rect(2, height - buttonHeight, width - 4, buttonHeight);
context.setStrokeColorRgb(10, 10, 10, 0.4);
context.stroke();
context.closePath();
}
void activate (int activateType, Tile tile) {
if (activateType == TileEntity.ACTIVATE_PLAYER ||
activateType == TileEntity.ACTIVATE_TOUCHING) {
this.trigger(tile);
}
}
}
class FireLauncherEntity extends TileEntity {
bool getCollision (WorldEntity entity) {
if (entity.weight == 0.0) {
return false;
} else {
return true;
}
}
void drawInner (CanvasRenderingContext2D context, Tile tile, num width, num height) {
var leftNeighbour = tile.getNeighbour(new Vector(-1,0));
context.beginPath();
context.rect(0, 0, width, height);
context.setFillColorRgb(80, 80, 80, 1);
context.setStrokeColorRgb(30, 30, 30, 1);
context.fill();
context.stroke();
context.closePath();
}
void activate (int activateType, Tile tile) {
if (activateType == TileEntity.ACTIVATE_TRIGGER) {
tile.spawnEntity(Fireball.spawn);
}
}
}
class LavaEntity extends TileEntity {
// TODO: Update function that kills any world entities that touch this!
int offset;
static int lavaCount = 0;
LavaEntity () {
this.offset = lavaCount*3;
lavaCount += 1;
}
bool getCollision(entity) {
return false;
}
bool isDeadly(var entity) {
return true;
}
void update (num timeDelta, num time) {
}
void drawInner (CanvasRenderingContext2D context, Tile tile, num width, num height) {
// There's a nice red border at the bottom of the screen, so that's our lava now :) */
/*
context.beginPath();
context.rect(2, 13, 2, 2);
context.rect(8, 18, 2, 2);
context.rect(1, 15, 2, 2);
context.rect(14, 12, 2, 2);
context.rect(23, 19, 2, 2);
context.rect(27, 25, 2, 2);
context.rect(29, 19, 2, 2);
context.rect(13, 15, 2, 2);
context.rect(14, 18, 2, 2);
context.rect(19, 23, 2, 2);
context.rect(16, 29, 2, 2);
context.rect(4, 27, 2, 2);
context.rect(29, 29, 2, 2);
context.rect(28, 12, 2, 2);
context.setFillColorHsl(10, 10, 10, 0.2);
context.fill();
context.closePath();
*/
}
}
abstract class WorldEntity {
Scene scene;
DoubleVector _position;
Vector get worldPosition => this._position.toVector();
Vector size;
Vector force = new Vector(0,0);
double weight = 3.0;
const double deadWeight = 5.0;
const num forceLimit = 2;
Vector cellOffset;
bool isAlive = true;
bool isDeadly = false;
// TODO: momentum and pushback
DoubleVector momentum = new DoubleVector(0.0, 0.0);
DoubleVector reaction = new DoubleVector(0.0, 0.0);
WorldEntity (this.scene, Vector position, this.size, this.cellOffset) {
this._position = position.toDoubleVector();
}
AbstractTile getCurrentTile () {
Vector center = new Vector(this.size.x ~/ 2, this.size.y ~/ 2) + this.worldPosition;
return this.scene.getTileForWorldPosition(center);
}
DoubleVector collide (List collisions, DoubleVector targetMovement) {
if (this.isAlive) {
return new DoubleVector(-targetMovement.x, -targetMovement.y);
} else {
return targetMovement;
}
}
void worldCollision (WorldEntity collidingEntity) {}
Vector updateMomentum (num timeDelta, DoubleVector worldForces) {
// Apply appropriate quantity of movement on each axis and return pushback
var moveDelta = timeDelta / 80;
this.momentum += new DoubleVector(worldForces.x * moveDelta, worldForces.y * moveDelta);
var targetMovement = new DoubleVector(this.momentum.x * moveDelta, this.momentum.y * moveDelta);
var targetPosition = this._position + targetMovement;
this.reaction = new DoubleVector(0.0, 0.0);
bool colliding = false;
DoubleVector targetMovementX = new DoubleVector(targetMovement.x, 0.0);
TilesList targetTilesX = this.scene.getTilesForWorldArea((this._position + targetMovementX).toVector(), this.size);
var collisionsX = targetTilesX.getCollision(this);
if (collisionsX.length > 0) {
this.reaction += this.collide(collisionsX, targetMovementX);
colliding = true;
}
DoubleVector targetMovementY = new DoubleVector(0.0, targetMovement.y);
TilesList targetTilesY = this.scene.getTilesForWorldArea((this._position + targetMovementY).toVector(), this.size);
var collisionsY = targetTilesY.getCollision(this);
if (collisionsY.length > 0) {
this.reaction += this.collide(collisionsY, targetMovementY);
colliding = true;
}
targetPosition += this.reaction;
if (colliding && !(targetMovement.x == 0 || targetMovement.y == 0) && targetPosition == this._position) {
// If we're stuck in an object, pull upwards to get away
targetPosition += new DoubleVector(0.0, -2.0*moveDelta);
}
this._position = targetPosition;
this.momentum += this.reaction;
}
void addForce (DoubleVector force) {
this.momentum += force;
}
void die () {
this.isAlive = false;
this.weight = this.deadWeight;
this.momentum = new DoubleVector(0.0, this.deadWeight);
}
void update (num timeDelta, num time, DoubleVector worldForces) {
this.updateMomentum(timeDelta, worldForces);
AbstractTile currentTile = this.getCurrentTile();
currentTile.activate(TileEntity.ACTIVATE_TOUCHING);
if (currentTile.isDeadly(this)) {
this.die();
}
}
void drawInner (CanvasRenderingContext2D context, num width, num height);
void draw (CanvasRenderingContext2D context) {
context.save();
if (! this.isAlive) context.globalAlpha = 0.8;
bool feetOnGround = this.reaction.y < 0;
int heightOffset = feetOnGround ? 0 : 3;
Vector cellPosition = new Vector(
this.worldPosition.x - (this.worldPosition.x % 16),
this.worldPosition.y - (this.worldPosition.y % 16) - heightOffset);
cellPosition += this.cellOffset;
context.translate(cellPosition.x, cellPosition.y);
this.drawInner(context, this.size.x, this.size.y+heightOffset);
context.restore();
}
}
class Fireball extends WorldEntity {
double weight = 0.0;
bool isDeadly = true;
Fireball (scene, startPosition) : super(scene, startPosition, new Vector(20, 20), new Vector(6, 6));
static Fireball spawn (scene, startPosition) {
return new Fireball(scene, startPosition+new Vector(6, 6));
}
DoubleVector collide (List collisions, DoubleVector targetMovement) {
if (collisions.length > 0) this.die();
return super.collide(collisions, targetMovement);
}
void update (num timeDelta, num time, DoubleVector worldForces) {
super.update(timeDelta, time, worldForces);
if (! this.isAlive) return;
this._position += new DoubleVector(-2.0, 0.0);
}
void drawInner (CanvasRenderingContext2D context, num width, num height) {
context.beginPath();
context.rect(0, 0, width, height);
context.setFillColorRgb(10, 10, 10, 0.05);
context.setStrokeColorRgb(10, 10, 10, 0.9);
context.fill();
context.stroke();
context.closePath();
}
}
class CaptainVillain extends WorldEntity {
CaptainVillain (scene, startPosition) : super(scene, startPosition, new Vector(24, 26), new Vector(4, 6));
void drawInner (CanvasRenderingContext2D context, num width, num height) {
context.beginPath();
context.rect(0, 12, width, height-12);
context.rect(6, 0, width-12, 12);
context.setStrokeColorRgb(10, 10, 10, 0.9);
context.stroke();
context.closePath();
context.beginPath();
context.rect(9, 5, 2, 2);
context.rect(width-10, 4, 2, 2);
context.rect(width-9, 7, 1, 1);
context.setFillColorRgb(10, 10, 10, 0.9);
context.fill();
context.closePath();
}
}
class CrimsonPlumber extends WorldEntity {
num lastJumpTime = 0;
CrimsonPlumber (scene, startPosition) : super(scene, startPosition, new Vector(16, 28), new Vector(4,3));
void worldCollision (WorldEntity collidingEntity) {
if (collidingEntity.isDeadly) this.die();
}
void update (num timeDelta, num time, DoubleVector worldForces) {
super.update(timeDelta, time, worldForces);
if (! this.isAlive) return;
AbstractTile currentTile = this.getCurrentTile();
bool feetOnGround = this.reaction.y < 0;
if (feetOnGround) {
DoubleVector walking = new DoubleVector(+0.1, 0.0);
this.addForce(walking);
/* TODO: Implement real pathfinding with A*. The following is just an unfortunate stopgap */
if (time - this.lastJumpTime > 250) {
// only jump once every 250ms
if (currentTile.getNeighbour2(+1, -1).hasCollisions(this)) {
// Something in the way above and ahead
// Don't jump!
} else if (currentTile.getNeighbour2(+2, -1).hasCollisions(this)) {
// Something will be in the way, so let's jump now
this.addForce(new DoubleVector(+0.2, -25.0));
} else if (currentTile.getNeighbour2(+1, 0).hasCollisions(this)) {
// Something in the way immediately ahead. Jump to avoid it.
this.addForce(new DoubleVector(+0.2, -25.0));
} else if (! currentTile.getNeighbour2(+1, +1).hasCollisions(this) &&
! currentTile.getNeighbour2(+2, +2).hasCollisions(this)) {
// Looks like a pit below and ahead. Jump to avoid it.
this.addForce(new DoubleVector(+0.2, -25.0));
}
}
}
}
void drawInner (CanvasRenderingContext2D context, num width, num height) {
context.beginPath();
context.rect(0, 12, width, height-12);
context.rect(2, 2, width-4, 10);
context.setStrokeColorRgb(10, 10, 10, 0.9);
context.stroke();
context.closePath();
context.beginPath();
context.rect(5, 4, 2, 2);
context.rect(width-5, 4, 2, 2);
context.setFillColorRgb(10, 10, 10, 0.9);
context.fill();
context.closePath();
context.beginPath();
context.rect(1, 0, width-2, 2);
context.setStrokeColorRgb(10, 10, 10, 0.5);
context.stroke();
context.closePath();
}
}