forked from sent/waves
8960 lines
329 KiB
TypeScript
8960 lines
329 KiB
TypeScript
// @echo '/// <reference path="GameStartr-0.2.0.ts" />'
|
|
|
|
// @ifdef INCLUDE_DEFINITIONS
|
|
/// <reference path="References/GameStartr-0.2.0.ts" />
|
|
/// <reference path="FullScreenMario.d.ts" />
|
|
// @endif
|
|
|
|
// @include ../Source/FullScreenMario.d.ts
|
|
|
|
module FullScreenMario {
|
|
"use strict";
|
|
|
|
/**
|
|
* A free HTML5 remake of Nintendo's original Super Mario Bros, expanded for the
|
|
* modern web. It includes the original 32 levels, a random map generator, a
|
|
* level editor, and over a dozen custom mods.
|
|
*/
|
|
export class FullScreenMario extends GameStartr.GameStartr implements IFullScreenMario {
|
|
// For the sake of reset functions, constants are stored as members of the
|
|
// FullScreenMario Function itself - this allows prototype setters to use
|
|
// them regardless of whether the prototype has been instantiated yet.
|
|
|
|
/**
|
|
* Static settings passed to individual reset Functions. Each of these
|
|
* should be filled out separately, after the FullScreenMario class
|
|
* has been declared but before an instance has been instantiated.
|
|
*/
|
|
public static settings: GameStartr.IGameStartrStoredSettings = {
|
|
"audio": undefined,
|
|
"collisions": undefined,
|
|
"devices": undefined,
|
|
"editor": undefined,
|
|
"generator": undefined,
|
|
"groups": undefined,
|
|
"events": undefined,
|
|
"input": undefined,
|
|
"math": undefined,
|
|
"maps": undefined,
|
|
"mods": undefined,
|
|
"objects": undefined,
|
|
"quadrants": undefined,
|
|
"renderer": undefined,
|
|
"runner": undefined,
|
|
"scenes": undefined,
|
|
"sprites": undefined,
|
|
"statistics": undefined,
|
|
"touch": undefined,
|
|
"ui": undefined
|
|
};
|
|
|
|
/**
|
|
* Static unitsize of 4, as that's how Super Mario Bros. is.
|
|
*/
|
|
public static unitsize: number = 4;
|
|
|
|
/**
|
|
* Static scale of 2, to exand to two pixels per one game pixel.
|
|
*/
|
|
public static scale: number = 2;
|
|
|
|
/**
|
|
* Gravity is always a function of unitsize (and about .48).
|
|
*/
|
|
public static gravity: number = Math.round(12 * FullScreenMario.unitsize) / 100;
|
|
|
|
/**
|
|
* Levels of points to award for hopping on / shelling enemies.
|
|
*/
|
|
public static pointLevels: number[] = [
|
|
100, 200, 400, 500, 800, 1000, 2000, 4000, 5000, 8000
|
|
];
|
|
|
|
/**
|
|
* Useful for custom text Things, where "text!" cannot be a Function name.
|
|
*/
|
|
public static customTextMappings: { [i: string]: string } = {
|
|
" ": "Space",
|
|
".": "Period",
|
|
"!": "ExclamationMark",
|
|
":": "Colon",
|
|
"/": "Slash",
|
|
"©": "Copyright"
|
|
};
|
|
|
|
/**
|
|
* Overriden MapScreenr refers to the IMapScreenr defined in FullScreenMario.d.ts.
|
|
*/
|
|
public MapScreener: IMapScreenr;
|
|
|
|
/**
|
|
* Internal reference to the static settings.
|
|
*/
|
|
public settings: GameStartr.IGameStartrStoredSettings;
|
|
|
|
/**
|
|
* Internal reference to the static unitsize.
|
|
*/
|
|
public unitsize: number;
|
|
|
|
/**
|
|
* Internal reference to the static pointLevels.
|
|
*/
|
|
public pointLevels: number[];
|
|
|
|
/**
|
|
* Internal reference to the static customTextMappings.
|
|
*/
|
|
public customTextMappings: { [i: string]: string };
|
|
|
|
/**
|
|
* The game's player, which (when defined) will always be a Player Thing.
|
|
*/
|
|
public player: IPlayer;
|
|
|
|
/**
|
|
* Container for device motion information, used by this.deviceMotion.
|
|
*/
|
|
public deviceMotionStatus: IDeviceMotionStatus;
|
|
|
|
/**
|
|
* Constructor for a new FullScreenMario game object.
|
|
* Static game settings are stored in the appropriate settings/*.js object
|
|
* as members of the FullScreenMario.prototype object.
|
|
* Dynamic game settings may be given as members of the "customs" argument.
|
|
* On typical machines, game startup time is approximately 500-700ms.
|
|
*/
|
|
constructor(settings: GameStartr.IGameStartrSettings) {
|
|
this.settings = FullScreenMario.settings;
|
|
|
|
this.deviceMotionStatus = {
|
|
"motionDown": false,
|
|
"motionLeft": false,
|
|
"motionRight": false,
|
|
"x": undefined,
|
|
"y": undefined,
|
|
"dy": undefined
|
|
};
|
|
|
|
super(
|
|
this.proliferate(
|
|
{
|
|
"constantsSource": FullScreenMario,
|
|
"constants": [
|
|
"unitsize",
|
|
"scale",
|
|
"gravity",
|
|
"pointLevels",
|
|
"customTextMappings"
|
|
]
|
|
},
|
|
settings));
|
|
}
|
|
|
|
|
|
/* Resets
|
|
*/
|
|
resetObjectMaker(FSM: FullScreenMario, settings: GameStartr.IGameStartrSettings): void {
|
|
FSM.ObjectMaker = new ObjectMakr.ObjectMakr(
|
|
FSM.proliferate(
|
|
{
|
|
"properties": {
|
|
"Quadrant": {
|
|
"EightBitter": FSM,
|
|
"GameStarter": FSM,
|
|
"FSM": FSM
|
|
},
|
|
"Thing": {
|
|
"EightBitter": FSM,
|
|
"GameStarter": FSM,
|
|
"FSM": FSM
|
|
}
|
|
}
|
|
},
|
|
FSM.settings.objects));
|
|
}
|
|
|
|
/**
|
|
* Sets this.AudioPlayer.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} customs
|
|
*/
|
|
resetAudioPlayer(FSM: FullScreenMario, settings: GameStartr.IGameStartrSettings): void {
|
|
super.resetAudioPlayer(FSM, settings);
|
|
|
|
FSM.AudioPlayer.setGetVolumeLocal(FSM.getVolumeLocal.bind(FSM, FSM));
|
|
|
|
FSM.AudioPlayer.setGetThemeDefault(FSM.getAudioThemeDefault.bind(FSM, FSM));
|
|
}
|
|
|
|
/**
|
|
* Sets this.ThingHitter.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} customs
|
|
*/
|
|
resetThingHitter(FSM: FullScreenMario, settings: GameStartr.IGameStartrSettings): void {
|
|
super.resetThingHitter(FSM, settings);
|
|
|
|
FSM.ThingHitter.cacheHitCheckGroup("Solid");
|
|
FSM.ThingHitter.cacheHitCheckGroup("Character");
|
|
}
|
|
|
|
/**
|
|
* Sets this.MapsHandler.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} customs
|
|
*/
|
|
resetMapsHandler(FSM: FullScreenMario, settings: GameStartr.IGameStartrSettings): void {
|
|
FSM.MapsHandler = new MapsHandlr.MapsHandlr({
|
|
"MapsCreator": FSM.MapsCreator,
|
|
"MapScreener": FSM.MapScreener,
|
|
"screenAttributes": (<any>FSM.settings.maps).screenAttributes,
|
|
"onSpawn": (<any>FSM.settings.maps).onSpawn.bind(FSM),
|
|
"stretchAdd": FSM.mapAddStretched.bind(FSM),
|
|
"afterAdd": FSM.mapAddAfter.bind(FSM)
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Resets this.ItemsHolder via the parent GameStartr resetItemsHolder.
|
|
*
|
|
* If the screen isn't wide enough to fit the 'lives' display, it's hidden.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} customs
|
|
*/
|
|
resetItemsHolder(FSM: FullScreenMario, settings: GameStartr.IGameStartrSettings): void {
|
|
super.resetItemsHolder(FSM, settings);
|
|
|
|
if (settings.width < 560) {
|
|
(<HTMLElement>(<HTMLTableRowElement>FSM.ItemsHolder.getContainer().children[0]).cells[4]).style.display = "none";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets this.MathDecider, using its existing MapScreenr as its constants.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} customs
|
|
*/
|
|
resetMathDecider(FSM: FullScreenMario, customs: GameStartr.IMathDecidrCustoms): void {
|
|
FSM.MathDecider = new MathDecidr.MathDecidr(
|
|
FSM.proliferate(
|
|
{
|
|
"constants": FSM.MapScreener
|
|
},
|
|
FSM.settings.math));
|
|
}
|
|
|
|
/**
|
|
* Sets this.container via the parent GameStartr resetContaienr.
|
|
*
|
|
* The container is given the "Press Start" font, the PixelRender is told
|
|
* to draw the scenery, solid, character, and text groups, and the container
|
|
* width is set to the custom's width.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} customs
|
|
*/
|
|
resetContainer(FSM: FullScreenMario, settings: GameStartr.IGameStartrSettings): void {
|
|
super.resetContainer(FSM, settings);
|
|
|
|
FSM.container.style.fontFamily = "Press Start";
|
|
FSM.container.className += " FullScreenMario";
|
|
|
|
FSM.PixelDrawer.setThingArrays([
|
|
<GameStartr.IThing[]>FSM.GroupHolder.getGroup("Scenery"),
|
|
<GameStartr.IThing[]>FSM.GroupHolder.getGroup("Solid"),
|
|
<GameStartr.IThing[]>FSM.GroupHolder.getGroup("Character"),
|
|
<GameStartr.IThing[]>FSM.GroupHolder.getGroup("Text")
|
|
]);
|
|
|
|
FSM.ItemsHolder.getContainer().style.width = settings.width + "px";
|
|
FSM.container.appendChild(FSM.ItemsHolder.getContainer());
|
|
}
|
|
|
|
|
|
/* Global manipulations
|
|
*/
|
|
|
|
/**
|
|
* Completely restarts the game. Lives are reset to 3, the map goes back
|
|
* to default, and the onGameStart mod trigger is fired.
|
|
*/
|
|
gameStart(): void {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this);
|
|
|
|
FSM.setMap(FSM.settings.maps.mapDefault, FSM.settings.maps.locationDefault);
|
|
FSM.ItemsHolder.setItem(
|
|
"lives",
|
|
(<any>FSM.settings.statistics.values).lives.valueDefault);
|
|
|
|
FSM.ModAttacher.fireEvent("onGameStart");
|
|
}
|
|
|
|
/**
|
|
* Completely ends the game. All Thing groups are clared, sounds are
|
|
* stopped, the screen goes to black, "GAME OVER" is displayed. After a
|
|
* while, the game restarts again via gameStart.
|
|
*/
|
|
gameOver(): void {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this),
|
|
text: ICustomText = <ICustomText>FSM.ObjectMaker.make("CustomText", {
|
|
"texts": [{
|
|
"text": "GAME OVER"
|
|
}]
|
|
}),
|
|
texts: IThing[],
|
|
textWidth: number,
|
|
i: number;
|
|
|
|
FSM.killNPCs();
|
|
|
|
FSM.AudioPlayer.clearAll();
|
|
FSM.AudioPlayer.play("Game Over");
|
|
|
|
FSM.GroupHolder.clearArrays();
|
|
FSM.ItemsHolder.hideContainer();
|
|
FSM.TimeHandler.cancelAllEvents();
|
|
FSM.PixelDrawer.setBackground("black");
|
|
|
|
FSM.addThing(text, FSM.MapScreener.width / 2, FSM.MapScreener.height / 2);
|
|
|
|
texts = text.children;
|
|
textWidth = -(texts[texts.length - 1].right - texts[0].left) / 2;
|
|
for (i = 0; i < texts.length; i += 1) {
|
|
FSM.shiftHoriz(texts[i], textWidth);
|
|
}
|
|
|
|
FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
FSM.gameStart();
|
|
FSM.ItemsHolder.displayContainer();
|
|
},
|
|
420);
|
|
|
|
FSM.ModAttacher.fireEvent("onGameOver");
|
|
}
|
|
|
|
/**
|
|
* Slight addition to the GameStartr thingProcess Function. The Thing's hit
|
|
* check type is cached immediately.
|
|
*/
|
|
thingProcess(thing: IThing, title: string, settings: any, defaults: any): void {
|
|
// Infinite height refers to objects that reach exactly to the bottom
|
|
if (<any>thing.height === "Infinity" || thing.height === Infinity) {
|
|
thing.height = thing.FSM.getAbsoluteHeight(thing.y) / thing.FSM.unitsize;
|
|
}
|
|
|
|
super.thingProcess(thing, title, settings, defaults);
|
|
|
|
// ThingHittr becomes very non-performant if functions aren't generated
|
|
// for each Thing constructor (optimization does not respect prototypal
|
|
// inheritance, sadly).
|
|
thing.FSM.ThingHitter.cacheHitCheckType(thing.title, thing.groupType);
|
|
}
|
|
|
|
/**
|
|
* Adds a Thing via addPreThing based on the specifications in a PreThing.
|
|
* This is done relative to MapScreener.left and MapScreener.floor.
|
|
*
|
|
* @param {PreThing} prething
|
|
*/
|
|
addPreThing(prething: IPreThing): void {
|
|
var thing: IThing = prething.thing,
|
|
position: string = prething.position || thing.position;
|
|
|
|
thing.FSM.addThing(
|
|
thing,
|
|
prething.left * thing.FSM.unitsize - thing.FSM.MapScreener.left,
|
|
((<IMapScreenr>thing.FSM.MapScreener).floor - prething.top) * thing.FSM.unitsize);
|
|
|
|
// Either the prething or thing, in that order, may request to be in the
|
|
// front or back of its container using the "position" attribute
|
|
if (position) {
|
|
thing.FSM.TimeHandler.addEvent(function (): void {
|
|
switch (position) {
|
|
case "beginning":
|
|
thing.FSM.arrayToBeginning(thing, <any[]>thing.FSM.GroupHolder.getGroup(thing.groupType));
|
|
break;
|
|
case "end":
|
|
thing.FSM.arrayToEnd(thing, <any[]>thing.FSM.GroupHolder.getGroup(thing.groupType));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onAddPreThing", prething);
|
|
}
|
|
|
|
/**
|
|
* Adds a new Player Thing to the game and sets it as EightBitter.play. Any
|
|
* required additional settings (namely keys, power/size, and swimming) are
|
|
* applied here.
|
|
*
|
|
* @this {EightBittr}
|
|
* @param {Number} [left] A left coordinate to place the Thing at (by
|
|
* default, unitsize * 16).
|
|
* @param {Number} [bottom] A bottom coordinate to place the Thing upon
|
|
* (by default, unitsize * 16).
|
|
*/
|
|
addPlayer(left: number = this.unitsize * 16, bottom: number = this.unitsize * 16): IPlayer {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this),
|
|
player: IPlayer;
|
|
|
|
player = FSM.player = <IPlayer>FSM.ObjectMaker.make("Player", {
|
|
"power": FSM.ItemsHolder.getItem("power")
|
|
});
|
|
player.keys = player.getKeys();
|
|
|
|
if ((<IMapScreenr>FSM.MapScreener).underwater) {
|
|
player.swimming = true;
|
|
|
|
FSM.TimeHandler.addClassCycle(
|
|
player,
|
|
[
|
|
"swim1", "swim2"
|
|
],
|
|
"swimming",
|
|
5);
|
|
|
|
FSM.TimeHandler.addEventInterval(
|
|
player.FSM.animatePlayerBubbling,
|
|
96, Infinity,
|
|
player);
|
|
}
|
|
|
|
FSM.setPlayerSizeSmall(player);
|
|
|
|
if (player.power >= 2) {
|
|
FSM.playerGetsBig(player, true);
|
|
if (player.power === 3) {
|
|
FSM.playerGetsFire(player);
|
|
}
|
|
}
|
|
|
|
FSM.addThing(player, left, bottom - player.height * FSM.unitsize);
|
|
|
|
FSM.ModAttacher.fireEvent("onAddPlayer", player);
|
|
|
|
return player;
|
|
}
|
|
|
|
/**
|
|
* Shortcut to call scrollThing on the player.
|
|
*
|
|
* @this {EightBittr}
|
|
* @param {Number} dx
|
|
* @param {Number} dy
|
|
*/
|
|
scrollPlayer(dx: number, dy?: number): void {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this);
|
|
|
|
FSM.scrollThing(FSM.player, dx, dy);
|
|
|
|
FSM.ModAttacher.fireEvent("onScrollPlayer", dx, dy);
|
|
}
|
|
|
|
/**
|
|
* Triggered Function for when the game is paused. Music stops, the pause
|
|
* bleep is played, and the mod event is fired.
|
|
*/
|
|
onGamePause(FSM: FullScreenMario): void {
|
|
FSM.AudioPlayer.pauseAll();
|
|
FSM.AudioPlayer.play("Pause");
|
|
FSM.ModAttacher.fireEvent("onGamePause");
|
|
}
|
|
|
|
/**
|
|
* Triggered Function for when the game is played or unpause. Music resumes
|
|
* and the mod event is fired.
|
|
*/
|
|
onGamePlay(FSM: FullScreenMario): void {
|
|
FSM.AudioPlayer.resumeAll();
|
|
FSM.ModAttacher.fireEvent("onGamePlay");
|
|
}
|
|
|
|
|
|
/* Input
|
|
*/
|
|
|
|
/**
|
|
* Reacts to the left key being pressed. keys.run and leftDown are marked
|
|
* and the mod event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyDownLeft(FSM: FullScreenMario, event?: Event): void {
|
|
if (FSM.GamesRunner.getPaused()) {
|
|
return;
|
|
}
|
|
|
|
var player: IPlayer = FSM.player;
|
|
|
|
player.keys.run = -1;
|
|
player.keys.leftDown = true; // independent of changes to keys.run
|
|
player.FSM.ModAttacher.fireEvent("onKeyDownLeft");
|
|
}
|
|
|
|
/**
|
|
* Reacts to the right key being pressed. keys.run and keys.rightDown are
|
|
* marked and the mod event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyDownRight(FSM: FullScreenMario, event?: Event): void {
|
|
if (FSM.GamesRunner.getPaused()) {
|
|
return;
|
|
}
|
|
|
|
var player: IPlayer = FSM.player;
|
|
|
|
player.keys.run = 1;
|
|
player.keys.rightDown = true; // independent of changes to keys.run
|
|
player.FSM.ModAttacher.fireEvent("onKeyDownRight");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to the up key being pressed. If the player can jump, it does, and
|
|
* underwater paddling is checked. The mod event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyDownUp(FSM: FullScreenMario, event?: Event): void {
|
|
if (FSM.GamesRunner.getPaused()) {
|
|
return;
|
|
}
|
|
|
|
var player: IPlayer = FSM.player;
|
|
|
|
player.keys.up = true;
|
|
|
|
if (player.canjump && (player.resting || FSM.MapScreener.underwater)) {
|
|
player.keys.jump = true;
|
|
player.canjump = false;
|
|
player.keys.jumplev = 0;
|
|
|
|
if (player.power > 1) {
|
|
FSM.AudioPlayer.play("Jump Super");
|
|
} else {
|
|
FSM.AudioPlayer.play("Jump Small");
|
|
}
|
|
|
|
if (FSM.MapScreener.underwater) {
|
|
FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
player.jumping = player.keys.jump = false;
|
|
},
|
|
14);
|
|
}
|
|
}
|
|
|
|
FSM.ModAttacher.fireEvent("onKeyDownUp");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to the down key being pressed. The player's keys.crouch is marked
|
|
* and the mod event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyDownDown(FSM: FullScreenMario, event?: Event): void {
|
|
if (FSM.GamesRunner.getPaused()) {
|
|
return;
|
|
}
|
|
|
|
var player: IPlayer = FSM.player;
|
|
|
|
player.keys.crouch = true;
|
|
FSM.ModAttacher.fireEvent("onKeyDownDown");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to the sprint key being pressed. Firing happens if the player is
|
|
* able, keys.spring is marked, and the mod event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyDownSprint(FSM: FullScreenMario, event?: Event): void {
|
|
if (FSM.GamesRunner.getPaused()) {
|
|
return;
|
|
}
|
|
|
|
var player: IPlayer = FSM.player;
|
|
|
|
if (player.power === 3 && player.keys.sprint === false && !player.crouching) {
|
|
player.fire(player);
|
|
}
|
|
|
|
player.keys.sprint = true;
|
|
player.FSM.ModAttacher.fireEvent("onKeyDownSprint");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to the pause key being pressed. The game is either paused or unpaused,
|
|
* and the mod event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyDownPause(FSM: FullScreenMario, event?: Event): void {
|
|
if (FSM.GamesRunner.getPaused()) {
|
|
FSM.GamesRunner.play();
|
|
} else {
|
|
FSM.GamesRunner.pause();
|
|
}
|
|
|
|
FSM.ModAttacher.fireEvent("onKeyDownPause");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to the mute key being lifted. Muting is toggled and the mod event
|
|
* is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyDownMute(FSM: FullScreenMario, event?: Event): void {
|
|
if (FSM.GamesRunner.getPaused()) {
|
|
return;
|
|
}
|
|
|
|
FSM.AudioPlayer.toggleMuted();
|
|
FSM.ModAttacher.fireEvent("onKeyDownMute");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to the left key being lifted. keys.run and keys.leftDown are
|
|
* marked and the mod event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyUpLeft(FSM: FullScreenMario, event?: Event): void {
|
|
var player: IPlayer = FSM.player;
|
|
|
|
player.keys.run = 0;
|
|
player.keys.leftDown = false;
|
|
|
|
FSM.ModAttacher.fireEvent("onKeyUpLeft");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to the right key being lifted. keys.run and keys.rightDown are
|
|
* marked and the mod event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyUpRight(FSM: FullScreenMario, event?: Event): void {
|
|
var player: IPlayer = FSM.player;
|
|
|
|
player.keys.run = 0;
|
|
player.keys.rightDown = false;
|
|
|
|
FSM.ModAttacher.fireEvent("onKeyUpRight");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to the up key being lifted. Jumping stops and the mod event is
|
|
* fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyUpUp(FSM: FullScreenMario, event?: Event): void {
|
|
var player: IPlayer = FSM.player;
|
|
|
|
if (!FSM.MapScreener.underwater) {
|
|
player.keys.jump = player.keys.up = false;
|
|
}
|
|
|
|
player.canjump = true;
|
|
FSM.ModAttacher.fireEvent("onKeyUpUp");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to the down key being lifted. keys.crouch is marked, crouch
|
|
* removal happens if necessary, and the mod event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyUpDown(FSM: FullScreenMario, event?: Event): void {
|
|
var player: IPlayer = FSM.player;
|
|
|
|
player.keys.crouch = false;
|
|
|
|
if (!player.piping) {
|
|
FSM.animatePlayerRemoveCrouch(player);
|
|
}
|
|
|
|
FSM.ModAttacher.fireEvent("onKeyUpDown");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to the spring key being lifted. keys.sprint is marked and the mod
|
|
* event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyUpSprint(FSM: FullScreenMario, event?: Event): void {
|
|
var player: IPlayer = FSM.player;
|
|
|
|
player.keys.sprint = false;
|
|
FSM.ModAttacher.fireEvent("onKeyUpSprint");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to the pause key being lifted. The mod event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
keyUpPause(FSM: FullScreenMario, event?: Event): void {
|
|
FSM.ModAttacher.fireEvent("onKeyUpPause");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to a right click being pressed. Pausing is toggled and the mod
|
|
* event is fired.
|
|
*
|
|
* @param {Player} player
|
|
*/
|
|
mouseDownRight(FSM: FullScreenMario, event?: Event): void {
|
|
FSM.GamesRunner.togglePause();
|
|
FSM.ModAttacher.fireEvent("onMouseDownRight");
|
|
|
|
if (event && event.preventDefault !== undefined) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reacts to a regularly caused device motion event. Acceleration is checked
|
|
* for changed tilt horizontally (to trigger left or right key statuses) or
|
|
* changed tilt vertically (jumping). The mod event is also fired.
|
|
*
|
|
* @param {Player} player
|
|
* @param {DeviceMotionEvent} event
|
|
*/
|
|
deviceMotion(FSM: FullScreenMario, event: DeviceMotionEvent): void {
|
|
var player: IPlayer = FSM.player,
|
|
deviceMotionStatus: IDeviceMotionStatus = FSM.deviceMotionStatus,
|
|
acceleration: DeviceAcceleration = event.accelerationIncludingGravity;
|
|
|
|
FSM.ModAttacher.fireEvent("onDeviceMotion", event);
|
|
|
|
if (deviceMotionStatus.y !== undefined) {
|
|
deviceMotionStatus.dy = acceleration.y - deviceMotionStatus.y;
|
|
if (deviceMotionStatus.dy > 0.21) {
|
|
FSM.keyDownUp(FSM);
|
|
} else if (deviceMotionStatus.dy < -0.14) {
|
|
FSM.keyUpUp(FSM);
|
|
}
|
|
}
|
|
|
|
deviceMotionStatus.x = acceleration.x;
|
|
deviceMotionStatus.y = acceleration.y;
|
|
|
|
if (deviceMotionStatus.x > 2.1) {
|
|
if (!deviceMotionStatus.motionLeft) {
|
|
player.FSM.keyDownLeft(FSM);
|
|
deviceMotionStatus.motionLeft = true;
|
|
}
|
|
} else if (deviceMotionStatus.x < -2.1) {
|
|
if (!deviceMotionStatus.motionRight) {
|
|
player.FSM.keyDownRight(FSM);
|
|
deviceMotionStatus.motionRight = true;
|
|
}
|
|
} else {
|
|
if (deviceMotionStatus.motionLeft) {
|
|
player.FSM.keyUpLeft(FSM);
|
|
deviceMotionStatus.motionLeft = false;
|
|
}
|
|
if (deviceMotionStatus.motionRight) {
|
|
player.FSM.keyUpRight(FSM);
|
|
deviceMotionStatus.motionRight = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks whether inputs can be fired, which is equivalent to the status of
|
|
* the MapScreener's nokeys variable (an inverse value).
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
*/
|
|
canInputsTrigger(FSM: FullScreenMario): boolean {
|
|
return !FSM.MapScreener.nokeys;
|
|
}
|
|
|
|
|
|
/* Upkeep maintenence
|
|
*/
|
|
|
|
/**
|
|
* Regular maintenance Function called to decrease time every 25 game
|
|
* ticks. Returns whether time should stop counting, which is only when
|
|
* it becomes 0.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
*/
|
|
maintainTime(FSM: FullScreenMario): boolean {
|
|
if (!(<IMapScreenr>FSM.MapScreener).notime) {
|
|
FSM.ItemsHolder.decrease("time", 1);
|
|
return false;
|
|
}
|
|
|
|
if (!FSM.ItemsHolder.getItem("time")) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Regular maintenance Function called on the Scenery group every 350
|
|
* upkeeps (slightly over 5 seconds). Things are checked for being alive
|
|
* and to the left of QuadsKeeper.left; if they aren't, they are removed.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
*/
|
|
maintainScenery(FSM: FullScreenMario): void {
|
|
var things: IScenery[] = <IScenery[]>FSM.GroupHolder.getGroup("Scenery"),
|
|
delx: number = FSM.QuadsKeeper.left,
|
|
thing: IThing,
|
|
i: number;
|
|
|
|
for (i = 0; i < things.length; i += 1) {
|
|
thing = things[i];
|
|
|
|
if (thing.right < delx && thing.outerok !== 2) {
|
|
FSM.arrayDeleteThing(thing, things, i);
|
|
i -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Regular maintenance Function called on the Solids group every upkeep.
|
|
* Things are checked for being alive and to the right of QuadsKeeper.left;
|
|
* if they aren't, they are removed. Each Thing is also allowed a movement
|
|
* Function.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Solid[]} solids FSM's GroupHolder's Solid group.
|
|
*/
|
|
maintainSolids(FSM: FullScreenMario, solids: ISolid[]): void {
|
|
var delx: number = FSM.QuadsKeeper.left,
|
|
solid: IThing,
|
|
i: number;
|
|
|
|
FSM.QuadsKeeper.determineAllQuadrants("Solid", solids);
|
|
|
|
for (i = 0; i < solids.length; i += 1) {
|
|
solid = solids[i];
|
|
|
|
if (solid.alive && solid.right > delx) {
|
|
if (solid.movement) {
|
|
solid.movement(solid);
|
|
}
|
|
} else if (!solid.alive || solid.outerok !== 2) {
|
|
FSM.arrayDeleteThing(solid, solids, i);
|
|
i -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Regular maintenance Function called on the Characters group every upkeep.
|
|
* Things have gravity and y-velocities, collision detection, and resting
|
|
* checks applied before they're checked for being alive. If they are, they
|
|
* are allowed a movement Function; if not, they are removed.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Character[]} characters EightBittr's GroupHolder's Characters
|
|
* group.
|
|
*/
|
|
maintainCharacters(FSM: FullScreenMario, characters: ICharacter[]): void {
|
|
var delx: number = FSM.QuadsKeeper.right,
|
|
character: ICharacter,
|
|
i: number;
|
|
|
|
for (i = 0; i < characters.length; i += 1) {
|
|
character = characters[i];
|
|
|
|
// Gravity
|
|
if (character.resting) {
|
|
character.yvel = 0;
|
|
} else {
|
|
if (!character.nofall) {
|
|
character.yvel += character.gravity || FSM.MapScreener.gravity;
|
|
}
|
|
character.yvel = Math.min(character.yvel, FSM.MapScreener.maxyvel);
|
|
}
|
|
|
|
// Position updating and collision detection
|
|
character.under = character.undermid = undefined;
|
|
FSM.updatePosition(character);
|
|
FSM.QuadsKeeper.determineThingQuadrants(character);
|
|
FSM.ThingHitter.checkHitsOf[character.title](character);
|
|
|
|
// Overlaps
|
|
if (character.overlaps && character.overlaps.length) {
|
|
FSM.maintainOverlaps(<ICharacterOverlapping>character);
|
|
}
|
|
|
|
// Resting tests
|
|
if (character.resting) {
|
|
if (!FSM.isCharacterOnResting(character, character.resting)) {
|
|
if (character.onRestingOff) {
|
|
character.onRestingOff(character, character.resting);
|
|
} else {
|
|
// Necessary for moving platforms
|
|
character.resting = undefined;
|
|
}
|
|
} else {
|
|
character.yvel = 0;
|
|
FSM.setBottom(character, character.resting.top);
|
|
}
|
|
}
|
|
|
|
// Movement or deletion
|
|
// To do: rethink this...
|
|
if (character.alive) {
|
|
if (!character.player &&
|
|
(character.numquads === 0 || character.left > delx) &&
|
|
(!character.outerok || (
|
|
character.outerok !== 2
|
|
&& character.right < FSM.MapScreener.width - delx
|
|
))) {
|
|
FSM.arrayDeleteThing(character, characters, i);
|
|
i -= 1;
|
|
} else {
|
|
if (!character.nomove && character.movement) {
|
|
character.movement(character);
|
|
}
|
|
}
|
|
} else {
|
|
FSM.arrayDeleteThing(character, characters, i);
|
|
i -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maintenance Function only triggered for Things that are known to have
|
|
* overlapping Solids stored in their overlaps attribute. This will slide
|
|
* the offending Thing away from the midpoint of those overlaps once a call
|
|
* until it's past the boundary (and check for those boundaries if not
|
|
* already set).
|
|
*
|
|
* @param {Thing} thing
|
|
*/
|
|
maintainOverlaps(character: ICharacterOverlapping): void {
|
|
// If checkOverlaps is still true, this is the first maintain call
|
|
if (character.checkOverlaps) {
|
|
if (!character.FSM.setOverlapBoundaries(<ICharacterOverlapping>character)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
character.FSM.slideToX(
|
|
character,
|
|
character.overlapGoal,
|
|
character.FSM.unitsize
|
|
);
|
|
|
|
// Goal to the right: has the thing gone far enough to the right?
|
|
if (character.overlapGoRight) {
|
|
if (character.left >= character.overlapCheck) {
|
|
character.FSM.setLeft(character, character.overlapCheck);
|
|
} else {
|
|
return;
|
|
}
|
|
} else {
|
|
// Goal to the left: has the thing gone far enough to the left?
|
|
if (character.right <= character.overlapCheck) {
|
|
character.FSM.setRight(character, character.overlapCheck);
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// A check above didn't fail into a return, so overlapping is solved
|
|
character.overlaps.length = 0;
|
|
character.checkOverlaps = true;
|
|
}
|
|
|
|
/**
|
|
* Sets the overlapping properties of a Thing when it is first detected as
|
|
* overlapping in maintainOverlaps. All solids in its overlaps Array are
|
|
* checked to find the leftmost and rightmost extremes and midpoint.
|
|
* Then, the Thing is checked for being to the left or right of the
|
|
* midpoint, and the goal set to move it away from the midpoint.
|
|
*
|
|
* @param {Thing} thing
|
|
* @return {Boolean} Whether the Thing's overlaps were successfully
|
|
* recorded (if there was only one, not so).
|
|
*/
|
|
setOverlapBoundaries(thing: ICharacterOverlapping): boolean {
|
|
// Only having one overlap means nothing should be done
|
|
if (thing.overlaps.length === 1) {
|
|
thing.overlaps.length = 0;
|
|
return false;
|
|
}
|
|
|
|
var rightX: number = -Infinity,
|
|
leftX: number = Infinity,
|
|
overlaps: ISolid[] = thing.overlaps,
|
|
other: ISolid,
|
|
leftThing: IThing,
|
|
rightThing: IThing,
|
|
midpoint: number,
|
|
i: number;
|
|
|
|
for (i = 0; i < overlaps.length; i += 1) {
|
|
other = overlaps[i];
|
|
|
|
if (other.right > rightX) {
|
|
rightThing = other;
|
|
}
|
|
if (other.left < leftX) {
|
|
leftThing = other;
|
|
}
|
|
}
|
|
|
|
midpoint = (leftX + rightX) / 2;
|
|
|
|
if (thing.FSM.getMidX(thing) >= midpoint) {
|
|
thing.overlapGoal = Infinity;
|
|
thing.overlapGoRight = true;
|
|
thing.overlapCheck = rightThing.right;
|
|
} else {
|
|
thing.overlapGoal = -Infinity;
|
|
thing.overlapGoRight = false;
|
|
thing.overlapCheck = leftThing.left;
|
|
}
|
|
|
|
thing.checkOverlaps = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Regular maintenance Function called on the player every upkeep. A barrage
|
|
* of tests are applied, namely falling/jumping, dying, x- and y-velocities,
|
|
* running, and scrolling. This is separate from the movePlayer movement
|
|
* Function that will be called in maintainCharacters.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
*/
|
|
maintainPlayer(FSM: FullScreenMario): void {
|
|
var player: IPlayer = FSM.player;
|
|
if (!FSM.isThingAlive(player)) {
|
|
return;
|
|
}
|
|
|
|
// Player is falling
|
|
if (player.yvel > 0) {
|
|
if (!FSM.MapScreener.underwater) {
|
|
player.keys.jump = false;
|
|
}
|
|
// Jumping?
|
|
if (!player.jumping && !player.crouching) {
|
|
// Paddling? (from falling off a solid)
|
|
if (FSM.MapScreener.underwater) {
|
|
if (!player.paddling) {
|
|
FSM.switchClass(player, "paddling", "paddling");
|
|
player.paddling = true;
|
|
}
|
|
} else {
|
|
FSM.addClass(player, "jumping");
|
|
player.jumping = true;
|
|
}
|
|
}
|
|
// Player has fallen too far
|
|
if (!player.dying && player.top > FSM.MapScreener.bottom) {
|
|
// If the map has an exit (e.g. cloud world), transport there
|
|
if ((<IArea>FSM.MapsHandler.getArea()).exit) {
|
|
FSM.setLocation((<IArea>FSM.MapsHandler.getArea()).exit);
|
|
} else {
|
|
// Otherwise, since Player is below the screen, kill him dead
|
|
FSM.killPlayer(player, 2);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Player is moving to the right
|
|
if (player.xvel > 0) {
|
|
if (player.right > FSM.MapScreener.middleX) {
|
|
// If Player is to the right of the screen's middle, move the screen
|
|
if (player.right > FSM.MapScreener.right - FSM.MapScreener.left) {
|
|
player.xvel = Math.min(0, player.xvel);
|
|
}
|
|
}
|
|
} else if (player.left < 0) {
|
|
// Player is moving to the left
|
|
// Stop Player from going to the left.
|
|
player.xvel = Math.max(0, player.xvel);
|
|
}
|
|
|
|
// Player is hitting something (stop jumping)
|
|
if (player.under) {
|
|
player.jumpcount = 0;
|
|
}
|
|
|
|
// Scrolloffset is how far over the middle player's right is
|
|
if (FSM.MapScreener.canscroll) {
|
|
var scrolloffset: number = player.right - FSM.MapScreener.middleX;
|
|
if (scrolloffset > 0) {
|
|
FSM.scrollWindow(Math.min(player.scrollspeed, scrolloffset));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Collision detectors
|
|
*/
|
|
|
|
/**
|
|
* Function generator for the generic canThingCollide checker. This is used
|
|
* repeatedly by ThingHittr to generate separately optimized Functions for
|
|
* different Thing types.
|
|
*
|
|
* @return {Function}
|
|
*/
|
|
generateCanThingCollide(): (thing: IThing) => boolean {
|
|
/**
|
|
* Generic checker for canCollide, used for both Solids and Characters.
|
|
* This just returns if the Thing is alive and doesn't have the
|
|
* nocollide flag.
|
|
*
|
|
* @param {Thing} thing
|
|
* @return {Boolean}
|
|
*/
|
|
return function canThingCollide(thing: IThing): boolean {
|
|
return thing.alive && !thing.nocollide;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {Thing} thing
|
|
* @return {Boolean} Whether the Thing is alive, meaning it has a true alive
|
|
* flag and a false dead flag.
|
|
*/
|
|
isThingAlive(thing: IThing): boolean {
|
|
return thing && thing.alive && !thing.dead;
|
|
}
|
|
|
|
/**
|
|
* Generic base function to check if one Thing is touching another. This
|
|
* will be called by the more specific Thing touching functions.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Thing} other
|
|
* @return {Boolean}
|
|
* @remarks Only the horizontal checks use unitsize
|
|
*/
|
|
isThingTouchingThing(thing: IThing, other: IThing): boolean {
|
|
return (
|
|
!thing.nocollide && !other.nocollide
|
|
&& thing.right - thing.FSM.unitsize > other.left
|
|
&& thing.left + thing.FSM.unitsize < other.right
|
|
&& thing.bottom >= other.top
|
|
&& thing.top <= other.bottom);
|
|
}
|
|
|
|
/**
|
|
* General top collision detection Function for two Things to determine if
|
|
* one Thing is on top of another. This takes into consideration factors
|
|
* such as which are solid or an enemy, and y-velocity.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Thing} other
|
|
* @return {Boolean}
|
|
* @remarks This is a more specific form of isThingTouchingThing
|
|
*/
|
|
isThingOnThing(thing: IThing, other: IThing): boolean {
|
|
// If thing is a solid and other is falling, thing can't be above other
|
|
if (thing.groupType === "Solid" && other.yvel > 0) {
|
|
return false;
|
|
}
|
|
|
|
// If other is falling faster than thing, and isn't a solid,
|
|
// thing can't be on top (if anything, the opposite is true)
|
|
if (thing.yvel < other.yvel && other.groupType !== "Solid") {
|
|
return false;
|
|
}
|
|
|
|
// If thing is the player, and it's on top of an enemy, that's true
|
|
if (
|
|
(<ICharacter>thing).player && thing.bottom < other.bottom
|
|
&& (<ICharacter>other).type === "enemy"
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// If thing is too far to the right, it can't be touching other
|
|
if (thing.left + thing.FSM.unitsize >= other.right) {
|
|
return false;
|
|
}
|
|
|
|
// If thing is too far to the left, it can't be touching other
|
|
if (thing.right - thing.FSM.unitsize <= other.left) {
|
|
return false;
|
|
}
|
|
|
|
// If thing's bottom is below other's top, factoring tolerance and
|
|
// other's vertical velocity, they're touching
|
|
if (thing.bottom <= other.top + other.toly + other.yvel) {
|
|
return true;
|
|
}
|
|
|
|
// Same as before, but with velocity as the absolute difference between
|
|
// their two velocities
|
|
if (thing.bottom <= other.top + other.toly + Math.abs(thing.yvel - other.yvel)) {
|
|
return true;
|
|
}
|
|
|
|
// None of the above checks passed for true, so this is false (thing's
|
|
// bottom is above other's top
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Top collision Function to determine if one Thing is on top of a solid.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Solid} other
|
|
* @remarks Similar to isThingOnThing, but more specifically used for
|
|
* isCharacterOnSolid and isCharacterOnResting
|
|
*/
|
|
isThingOnSolid(thing: IThing, other: IThing): boolean {
|
|
// If thing is too far to the right, they're not touching
|
|
if (thing.left + thing.FSM.unitsize >= other.right) {
|
|
return false;
|
|
}
|
|
|
|
// If thing is too far to the left, they're not touching
|
|
if (thing.right - thing.FSM.unitsize <= other.left) {
|
|
return false;
|
|
}
|
|
|
|
// If thing's bottom is below other's top, factoring thing's velocity
|
|
// and other's tolerance, they're touching
|
|
if (thing.bottom - thing.yvel <= other.top + other.toly + thing.yvel) {
|
|
return true;
|
|
}
|
|
|
|
// Same as before, but with velocity as the absolute difference between
|
|
// their two velocities
|
|
if (thing.bottom <= other.top + other.toly + Math.abs(thing.yvel - other.yvel)) {
|
|
return true;
|
|
}
|
|
|
|
// None of the above checks passed for true, so this is false (thing's
|
|
// bottom is above other's top
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Top collision Function to determine if a character is on top of a solid.
|
|
* This is always true for resting (since resting checks happen before when
|
|
* this should be called).
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Solid} other
|
|
* @return {Boolean}
|
|
*/
|
|
isCharacterOnSolid(thing: ICharacter, other: ISolid): boolean {
|
|
// If character is resting on solid, this is automatically true
|
|
if (thing.resting === other) {
|
|
return true;
|
|
}
|
|
|
|
// If the character is jumping upwards, it's not on a solid
|
|
// (removing this check would cause Mario to have "sticky" behavior when
|
|
// jumping at the corners of solids)
|
|
if (thing.yvel < 0) {
|
|
return false;
|
|
}
|
|
|
|
// The character and solid must be touching appropriately
|
|
if (!thing.FSM.isThingOnSolid(thing, other)) {
|
|
return false;
|
|
}
|
|
|
|
// Corner case: when character is exactly falling off the right (false)
|
|
if (thing.left + thing.xvel + thing.FSM.unitsize === other.right) {
|
|
return false;
|
|
}
|
|
|
|
// Corner case: when character is exactly falling off the left (false)
|
|
if (thing.right - thing.xvel - thing.FSM.unitsize === other.left) {
|
|
return false;
|
|
}
|
|
|
|
// None of the above checks caught a falsity, so this must be true
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Top collision Function to determine if a character should be considered
|
|
* resting on a solid. This mostly uses isThingOnSolid, but also checks for
|
|
* the corner cases of the character being exactly at the edge of the solid
|
|
* (such as when jumping while next to it).
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Solid} other
|
|
* @return {Boolean}
|
|
*/
|
|
isCharacterOnResting(thing: ICharacter, other: ISolid): boolean {
|
|
if (!thing.FSM.isThingOnSolid(thing, other)) {
|
|
return false;
|
|
}
|
|
|
|
// Corner case: when character is exactly falling off the right (false)
|
|
if (thing.left + thing.xvel + thing.FSM.unitsize === other.right) {
|
|
return false;
|
|
}
|
|
|
|
// Corner case: when character is exactly falling off the left (false)
|
|
if (thing.right - thing.xvel - thing.FSM.unitsize === other.left) {
|
|
return false;
|
|
}
|
|
|
|
// None of the above checks caught a falsity, so this must be true
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Function generator for the generic isCharacterTouchingCharacter checker.
|
|
* This is used repeatedly by ThingHittr to generate separately optimized
|
|
* Functions for different Thing types.
|
|
*
|
|
* @return {Function}
|
|
*/
|
|
generateIsCharacterTouchingCharacter(): (thing: ICharacter, other: ICharacter) => boolean {
|
|
/**
|
|
* Generic checker for whether two characters are touching each other.
|
|
* This mostly checks to see if either has the nocollidechar flag, and
|
|
* if the other is a player. isThingTouchingThing is used after.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Character} other
|
|
* @return {Boolean}
|
|
*/
|
|
return function isCharacterTouchingCharacter(thing: ICharacter, other: ICharacter): boolean {
|
|
if (thing.nocollidechar && (!other.player || thing.nocollideplayer)) {
|
|
return false;
|
|
}
|
|
|
|
if (other.nocollidechar && (!thing.player || other.nocollideplayer)) {
|
|
return false;
|
|
}
|
|
|
|
return thing.FSM.isThingTouchingThing(thing, other);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Function generator for the generic isCharacterTouchingSolid checker. This
|
|
* is used repeatedly by ThingHittr to generate separately optimized
|
|
* Functions for different Thing types.
|
|
*
|
|
* @return {Function}
|
|
*/
|
|
generateIsCharacterTouchingSolid(): (thing: ICharacter, other: ISolid) => boolean {
|
|
/**
|
|
* Generic checker for whether a character is touching a solid. The
|
|
* hidden, collideHidden, and nocollidesolid flags are most relevant.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Solid} other
|
|
*/
|
|
return function isCharacterTouchingSolid(thing: ICharacter, other: ISolid): boolean {
|
|
// Hidden solids can only be touched by the player bottom-bumping
|
|
// them, or by specifying collideHidden
|
|
if (other.hidden && !other.collideHidden) {
|
|
if (!thing.player || !thing.FSM.isSolidOnCharacter(other, thing)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (thing.nocollidesolid && !(thing.allowUpSolids && other.up)) {
|
|
return false;
|
|
}
|
|
|
|
return thing.FSM.isThingTouchingThing(thing, other);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {Character} thing
|
|
* @param {Enemy} other
|
|
* @return {Boolean} Whether the Thing's bottom is above the other's top,
|
|
* allowing for the other's toly.
|
|
*/
|
|
isCharacterAboveEnemy(thing: ICharacter, other: ICharacter): boolean {
|
|
return thing.bottom < other.top + other.toly;
|
|
}
|
|
|
|
/**
|
|
* @param {Character} thing
|
|
* @param {Solid} other
|
|
* @return {Boolean} Whether the Thing's top is above the other's bottom,
|
|
* allowing for the Thing's toly and yvel.
|
|
*/
|
|
isCharacterBumpingSolid(thing: ICharacter, other: ISolid): boolean {
|
|
return thing.top + thing.toly + Math.abs(thing.yvel) > other.bottom;
|
|
}
|
|
|
|
/**
|
|
* @param {Character} thing
|
|
* @param {Solid} other
|
|
* @return {Boolean} Whether the Thing is "overlapping" the solid, which
|
|
* should move the Thing until it isn't.
|
|
*/
|
|
isCharacterOverlappingSolid(thing: ICharacter, other: ISolid): boolean {
|
|
return thing.top <= other.top && thing.bottom > other.bottom;
|
|
}
|
|
|
|
/**
|
|
* @param {Solid} thing
|
|
* @param {Character} other
|
|
* @return {Boolean} Whether the Thing, typically a solid, is on top of the
|
|
* other.
|
|
* @remarks Similar to isThingOnThing, but more specifically used for
|
|
* characterTouchedSolid
|
|
*/
|
|
isSolidOnCharacter(thing: ISolid, other: ICharacter): boolean {
|
|
// This can never be true if other is falling
|
|
if (other.yvel >= 0) {
|
|
return false;
|
|
}
|
|
|
|
// Horizontally, all that's required is for the other's midpoint to
|
|
// be within the thing's left and right
|
|
var midx: number = thing.FSM.getMidX(other);
|
|
if (midx <= thing.left || midx >= thing.right) {
|
|
return false;
|
|
}
|
|
|
|
// If the thing's bottom is below the other's top, factoring
|
|
// tolerance and velocity, that's false (this function assumes they're
|
|
// already touching)
|
|
if (thing.bottom - thing.yvel > other.top + other.toly - other.yvel) {
|
|
return false;
|
|
}
|
|
|
|
// The above checks never caught falsities, so this must be true
|
|
return true;
|
|
}
|
|
|
|
|
|
/* Collision reactions
|
|
*/
|
|
|
|
/**
|
|
* Externally facing Function to gain some number of lives. ItemsHolder
|
|
* increases the "score" statistic, an audio is played, and the mod event is
|
|
* fired.
|
|
*
|
|
* @this {EightBittr}
|
|
* @param {Number} [amount] How many lives to gain (by default, 1).
|
|
* @param {Boolean} [nosound] Whether the sound should be skipped (by
|
|
* default, false).
|
|
*/
|
|
gainLife(amount: number, nosound?: boolean): void {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this);
|
|
|
|
amount = Number(amount) || 1;
|
|
|
|
FSM.ItemsHolder.increase("lives", amount);
|
|
|
|
if (!nosound) {
|
|
this.AudioPlayer.play("Gain Life");
|
|
}
|
|
|
|
FSM.ModAttacher.fireEvent("onGainLife", amount);
|
|
}
|
|
|
|
/**
|
|
* Basic Function for an item to jump slightly into the air, such as from
|
|
* the player hitting a solid below it.
|
|
*
|
|
* @param {Item} thing
|
|
* @remarks This simply moves the thing up slightly and decreases its
|
|
* y-velocity, without considering x-direction.
|
|
*/
|
|
itemJump(thing: IThing): void {
|
|
thing.yvel -= FullScreenMario.unitsize * 1.4;
|
|
this.shiftVert(thing, -FullScreenMario.unitsize);
|
|
}
|
|
|
|
/**
|
|
* Generic Function for when the player jumps on top of an enemy. The enemy
|
|
* is killed, the player's velocity points upward, and score is gained.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Enemy} other
|
|
*/
|
|
jumpEnemy(thing: IPlayer, other: IEnemy): void {
|
|
if (thing.keys.up) {
|
|
thing.yvel = thing.FSM.unitsize * -1.4;
|
|
} else {
|
|
thing.yvel = thing.FSM.unitsize * -0.7;
|
|
}
|
|
|
|
thing.xvel *= 0.91;
|
|
thing.FSM.AudioPlayer.play("Kick");
|
|
|
|
if (other.group !== "item" || other.shell) {
|
|
thing.jumpcount += 1;
|
|
thing.FSM.scoreOn(thing.FSM.findScore(thing.jumpcount + thing.jumpers), other);
|
|
}
|
|
|
|
thing.jumpers += 1;
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (thing: IPlayer): void {
|
|
thing.jumpers -= 1;
|
|
},
|
|
1,
|
|
thing);
|
|
}
|
|
|
|
/**
|
|
* Callback for the player hitting a Mushroom or FireFlower. The player's
|
|
* power and the ItemsHolder's "power" statistic both go up, and the
|
|
* corresponding animations and mod event are triggered.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Item} [other]
|
|
*/
|
|
playerShroom(thing: IPlayer, other: IItem): void {
|
|
if (thing.shrooming || !thing.player) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.AudioPlayer.play("Powerup");
|
|
thing.FSM.scoreOn(1000, thing.FSM.player);
|
|
|
|
if (thing.power < 3) {
|
|
thing.FSM.ItemsHolder.increase("power");
|
|
|
|
if (thing.power < 3) {
|
|
thing.shrooming = true;
|
|
thing.power += 1;
|
|
|
|
if (thing.power === 3) {
|
|
thing.FSM.playerGetsFire(thing.FSM.player);
|
|
} else {
|
|
thing.FSM.playerGetsBig(thing.FSM.player);
|
|
}
|
|
}
|
|
}
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerShroom", thing, other);
|
|
}
|
|
|
|
/**
|
|
* Callback for the player hitting a Mushroom1Up. The game simply calls
|
|
* gainLife and triggers the mod event.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Item} [other]
|
|
*/
|
|
playerShroom1Up(thing: ICharacter, other: IItem): void {
|
|
if (!thing.player) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.gainLife(1);
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerShroom1Up", thing, other);
|
|
}
|
|
|
|
/**
|
|
* Callback for the player hitting a Star. A set of animation loops and
|
|
* sounds play, and the mod event is triggered. After some long period time,
|
|
* playerStarDown is called to start the process of removing star power.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Number} [timeout] How long to wait before calling
|
|
* playerStarDown (by default, 560).
|
|
*/
|
|
playerStarUp(thing: IPlayer, timeout: number = 560): void {
|
|
thing.star += 1;
|
|
|
|
thing.FSM.switchClass(thing, "normal fiery", "star");
|
|
|
|
thing.FSM.AudioPlayer.play("Powerup");
|
|
thing.FSM.AudioPlayer.addEventListener(
|
|
"Powerup",
|
|
"ended",
|
|
thing.FSM.AudioPlayer.playTheme.bind(
|
|
thing.FSM.AudioPlayer, "Star", true
|
|
));
|
|
|
|
thing.FSM.TimeHandler.addClassCycle(
|
|
thing,
|
|
[
|
|
"star1", "star2", "star3", "star4"
|
|
],
|
|
"star",
|
|
2);
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.playerStarDown,
|
|
timeout || 560,
|
|
thing);
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerStarUp", thing);
|
|
}
|
|
|
|
/**
|
|
* Trigger to commence reducing the player's star power. This slows the
|
|
* class cycle, times a playerStarOffCycle trigger, and fires the mod event.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
playerStarDown(thing: IPlayer): void {
|
|
if (!thing.player) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.TimeHandler.cancelClassCycle(thing, "star");
|
|
thing.FSM.TimeHandler.addClassCycle(
|
|
thing,
|
|
[
|
|
"star1", "star2", "star3", "star4"
|
|
],
|
|
"star",
|
|
5);
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.playerStarOffCycle,
|
|
140,
|
|
thing
|
|
);
|
|
|
|
thing.FSM.AudioPlayer.removeEventListeners("Powerup", "ended");
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerStarDown", thing);
|
|
}
|
|
|
|
/**
|
|
* Trigger to continue reducing the player's star power. This resumes
|
|
* playing the regular theme, times a playerStarOffFinal trigger, and fires
|
|
* the mod event.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
playerStarOffCycle(thing: IPlayer): void {
|
|
if (!thing.player) {
|
|
return;
|
|
}
|
|
|
|
if (thing.star > 1) {
|
|
thing.star -= 1;
|
|
return;
|
|
}
|
|
|
|
if (!thing.FSM.AudioPlayer.getTheme().paused) {
|
|
thing.FSM.AudioPlayer.playTheme();
|
|
}
|
|
|
|
thing.FSM.TimeHandler.addEvent(thing.FSM.playerStarOffFinal, 70, thing);
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerStarOffCycle", thing);
|
|
}
|
|
|
|
/**
|
|
* Trigger to finish reducing the player's star power. This actually reduces
|
|
* the player's star attribute, cancels the sprite cycle, adds the previous
|
|
* classes back, and fires the mod event.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
playerStarOffFinal(thing: IPlayer): void {
|
|
if (!thing.player) {
|
|
return;
|
|
}
|
|
|
|
thing.star -= 1;
|
|
thing.FSM.TimeHandler.cancelClassCycle(thing, "star");
|
|
thing.FSM.removeClasses(thing, "star star1 star2 star3 star4");
|
|
thing.FSM.addClass(thing, "normal");
|
|
|
|
if (thing.power === 3) {
|
|
thing.FSM.addClass(thing, "fiery");
|
|
}
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerStarOffFinal", thing);
|
|
}
|
|
|
|
/**
|
|
* Sizing modifier for the player, typically called when entering a location
|
|
* or colliding with a Mushroom. This sets the player's size to the large
|
|
* mode and optionally plays the animation. The mod event is then fired.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Boolean} [noAnimation] Whether to skip the animation (by
|
|
* default, false).
|
|
*/
|
|
playerGetsBig(thing: IPlayer, noAnimation?: boolean): void {
|
|
thing.FSM.setPlayerSizeLarge(thing);
|
|
thing.FSM.removeClasses(thing, "crouching small");
|
|
thing.FSM.updateBottom(thing, 0);
|
|
thing.FSM.updateSize(thing);
|
|
|
|
if (noAnimation) {
|
|
thing.FSM.addClass(thing, "large");
|
|
} else {
|
|
thing.FSM.playerGetsBigAnimation(thing);
|
|
}
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerGetsBig", thing);
|
|
}
|
|
|
|
/**
|
|
* Animation scheduler for the player getting big. The shrooming classes are
|
|
* cycled through rapidly while the player's velocity is paused.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
playerGetsBigAnimation(thing: IPlayer): void {
|
|
var stages: (string | { (...args: any[]): boolean })[] = [
|
|
"shrooming1", "shrooming2",
|
|
"shrooming1", "shrooming2",
|
|
"shrooming3", "shrooming2", "shrooming3"
|
|
];
|
|
|
|
thing.FSM.addClass(thing, "shrooming");
|
|
thing.FSM.thingPauseVelocity(thing);
|
|
|
|
// The last stage in the events clears it, resets movement, and stops
|
|
stages.push(function (thing: IPlayer, stages: string[]): boolean {
|
|
thing.shrooming = false;
|
|
stages.length = 0;
|
|
|
|
thing.FSM.addClass(thing, "large");
|
|
thing.FSM.removeClasses(thing, "shrooming shrooming3");
|
|
thing.FSM.thingResumeVelocity(thing);
|
|
|
|
return true;
|
|
});
|
|
|
|
thing.FSM.TimeHandler.addClassCycle(thing, stages, "shrooming", 6);
|
|
}
|
|
|
|
/**
|
|
* Sizing modifier for the player, typically called when going down to
|
|
* normal size after being large. This containst eha nimation scheduling
|
|
* to cycle through paddling classes, then flickers the player. The mod
|
|
* event is fired.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
playerGetsSmall(thing: IPlayer): void {
|
|
var bottom: number = thing.bottom;
|
|
|
|
thing.FSM.thingPauseVelocity(thing);
|
|
|
|
// Step one
|
|
thing.nocollidechar = true;
|
|
thing.FSM.animateFlicker(thing);
|
|
thing.FSM.removeClasses(
|
|
thing, "running skidding jumping fiery"
|
|
);
|
|
thing.FSM.addClasses(thing, "paddling small");
|
|
|
|
// Step two (t+21)
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (thing: IPlayer): void {
|
|
thing.FSM.removeClass(thing, "large");
|
|
thing.FSM.setPlayerSizeSmall(thing);
|
|
thing.FSM.setBottom(
|
|
thing, bottom - FullScreenMario.unitsize
|
|
);
|
|
},
|
|
21,
|
|
thing);
|
|
|
|
// Step three (t+42)
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (thing: IPlayer): void {
|
|
thing.FSM.thingResumeVelocity(thing, false);
|
|
thing.FSM.removeClass(thing, "paddling");
|
|
if (thing.running || thing.xvel) {
|
|
thing.FSM.addClass(thing, "running");
|
|
}
|
|
thing.FSM.PixelDrawer.setThingSprite(thing);
|
|
},
|
|
42,
|
|
thing);
|
|
|
|
// Step four (t+70)
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (thing: IPlayer): void {
|
|
thing.nocollidechar = false;
|
|
},
|
|
70,
|
|
thing);
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerGetsSmall");
|
|
}
|
|
|
|
/**
|
|
* Visual changer for when the player collides with a FireFlower. The
|
|
* "fiery" class is added, and the mod event is fired.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
playerGetsFire(thing: IPlayer): void {
|
|
thing.shrooming = false;
|
|
|
|
if (!thing.star) {
|
|
thing.FSM.addClass(thing, "fiery");
|
|
}
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerGetsFire");
|
|
}
|
|
|
|
/**
|
|
* Actually sets the size for a player to small (8x8) via setSize and
|
|
* updateSize.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
setPlayerSizeSmall(thing: IPlayer): void {
|
|
thing.FSM.setSize(thing, 8, 8, true);
|
|
thing.FSM.updateSize(thing);
|
|
}
|
|
|
|
/**
|
|
* Actually sets the size for a player to large (8x16) via setSize and
|
|
* updateSize.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
setPlayerSizeLarge(thing: IPlayer): void {
|
|
thing.FSM.setSize(thing, 8, 16, true);
|
|
thing.FSM.updateSize(thing);
|
|
}
|
|
|
|
/**
|
|
* Removes the crouching flag from the player and re-adds the running cycle.
|
|
* If the player is large (has power > 1), size and classes must be set.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
animatePlayerRemoveCrouch(thing: IPlayer): void {
|
|
thing.crouching = false;
|
|
thing.toly = thing.tolyOld || 0;
|
|
|
|
if (thing.power !== 1) {
|
|
thing.FSM.setHeight(thing, 16, true, true);
|
|
thing.FSM.removeClasses(thing, "crouching");
|
|
thing.FSM.updateBottom(thing, 0);
|
|
thing.FSM.updateSize(thing);
|
|
}
|
|
|
|
thing.FSM.animatePlayerRunningCycle(thing);
|
|
}
|
|
|
|
/**
|
|
* Officially unattaches a player from a solid. The thing's physics flags
|
|
* are reset to normal, the two have their attachment flags set, and the
|
|
* thing is set to be jumping off.
|
|
*
|
|
* @param {Player} thing A character attached to other.
|
|
* @param {Solid} other A solid the thing is attached to.
|
|
*/
|
|
unattachPlayer(thing: IPlayer, other: ISolid): void {
|
|
thing.nofall = false;
|
|
thing.nocollide = false;
|
|
thing.checkOverlaps = true;
|
|
thing.attachedSolid = undefined;
|
|
thing.xvel = thing.keys ? thing.keys.run : 0;
|
|
thing.movement = thing.FSM.movePlayer;
|
|
|
|
thing.FSM.addClass(thing, "jumping");
|
|
thing.FSM.removeClasses(thing, "climbing", "animated");
|
|
|
|
other.attachedCharacter = undefined;
|
|
}
|
|
|
|
/**
|
|
* Adds an invisible RestingStone underneath the player. It is hidden and
|
|
* unable to collide until the player falls to its level, at which point the
|
|
* stone is set underneath the player to be rested upon.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
playerAddRestingStone(thing: IPlayer): void {
|
|
var stone: IRestingStone = <IRestingStone>thing.FSM.addThing(
|
|
"RestingStone",
|
|
thing.left,
|
|
thing.top + thing.FSM.unitsize * 48);
|
|
|
|
thing.nocollide = true;
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
if (thing.bottom < stone.top) {
|
|
return false;
|
|
}
|
|
|
|
thing.nocollide = false;
|
|
thing.FSM.setMidXObj(stone, thing);
|
|
thing.FSM.setBottom(thing, stone.top);
|
|
return true;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
/**
|
|
* Marks a new overlapping Thing in the first Thing's overlaps Array,
|
|
* creating the Array if needed.
|
|
*
|
|
* @param {Thing} thing The Thing that is overlapping another Thing.
|
|
* @param {Thing} other The Thing being added to the overlaps Array.
|
|
*/
|
|
markOverlap(thing: ICharacterOverlapping, other: ISolid): void {
|
|
if (!thing.overlaps) {
|
|
thing.overlaps = [other];
|
|
} else {
|
|
thing.overlaps.push(other);
|
|
}
|
|
}
|
|
|
|
|
|
/* Spawn / activate functions
|
|
*/
|
|
|
|
/**
|
|
* Spawn callback for DeadGoombas. They simply disappear after 21 steps.
|
|
*
|
|
* @param {DeadGoomba} thing
|
|
*/
|
|
spawnDeadGoomba(thing: IThing): void {
|
|
thing.FSM.TimeHandler.addEvent(FullScreenMario.prototype.killNormal, 21, thing);
|
|
}
|
|
|
|
/**
|
|
* Spawn callback for HammerBros. Gravity is reduced, and the hammer and
|
|
* jump event intervals are started. The cyclical movement counter is set to
|
|
* 0.
|
|
*
|
|
* @param {HammerBro} thing
|
|
*/
|
|
spawnHammerBro(thing: IHammerBro): void {
|
|
thing.counter = 0;
|
|
|
|
thing.gravity = thing.FSM.MapScreener.gravity / 2.1;
|
|
|
|
thing.FSM.TimeHandler.addEvent(thing.FSM.animateThrowingHammer, 35, thing, 7);
|
|
thing.FSM.TimeHandler.addEventInterval(thing.FSM.animateJump, 140, Infinity, thing);
|
|
}
|
|
|
|
/**
|
|
* Spawn callback for Bowsers. The cyclical movement counter is set to 0 and
|
|
* the firing and jumping event intervals are started. If it also specifies
|
|
* a throwing interval, that's started too.
|
|
*
|
|
* @param {Bowser} thing
|
|
*/
|
|
spawnBowser(thing: IBowser): void {
|
|
var i: number;
|
|
|
|
thing.counter = 0;
|
|
thing.deathcount = 0;
|
|
|
|
for (i = 0; i < thing.fireTimes.length; i += 1) {
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
thing.FSM.animateBowserFire,
|
|
thing.fireTimes[i],
|
|
Infinity,
|
|
thing);
|
|
}
|
|
|
|
for (i = 0; i < thing.jumpTimes.length; i += 1) {
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
thing.FSM.animateBowserJump,
|
|
thing.jumpTimes[i],
|
|
Infinity,
|
|
thing);
|
|
}
|
|
|
|
if (thing.throwing) {
|
|
for (i = 0; i < thing.throwAmount; i += 1) {
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
thing.FSM.animateBowserThrow,
|
|
thing.throwPeriod,
|
|
Infinity,
|
|
thing);
|
|
},
|
|
thing.throwDelay + i * thing.throwBetween);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Spawn callback for Piranhas. The movement counter and direction are
|
|
* reset, and if the Piranha is on a pipe, it has a reduced height (6).
|
|
*
|
|
* @param {Piranha} thing
|
|
*/
|
|
spawnPiranha(thing: IPiranha): void {
|
|
var bottom: number;
|
|
|
|
thing.counter = 0;
|
|
thing.direction = thing.FSM.unitsize / -40;
|
|
|
|
if (thing.onPipe) {
|
|
bottom = thing.bottom;
|
|
thing.FSM.setHeight(thing, 6);
|
|
thing.FSM.setBottom(thing, bottom);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Spawn callback for Bloopers. Its squeeze and movement counters are set to
|
|
* 0.
|
|
*
|
|
* @param {Blooper} thing
|
|
*/
|
|
spawnBlooper(thing: IBlooper): void {
|
|
thing.squeeze = 0;
|
|
thing.counter = 0;
|
|
}
|
|
|
|
/**
|
|
* Spawn callback for Podoboos. The jumping interval is set to the Thing's
|
|
* frequency.
|
|
*
|
|
* @param {Podoboo} thing
|
|
*/
|
|
spawnPodoboo(thing: IPodoboo): void {
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
thing.FSM.animatePodobooJumpUp,
|
|
thing.frequency,
|
|
Infinity,
|
|
thing);
|
|
}
|
|
|
|
/**
|
|
* Spawn callback for Lakitus. MapScreenr registers the most recently
|
|
* added lakitu, as some areas spawn them every once in a while.
|
|
*
|
|
* @param {Lakitu} thing
|
|
*/
|
|
spawnLakitu(thing: ILakitu): void {
|
|
thing.FSM.MapScreener.lakitu = thing;
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
thing.FSM.animateLakituThrowingSpiny, 140, Infinity, thing);
|
|
}
|
|
|
|
/**
|
|
* Spawning callback for Cannons. Unless specified by the noBullets flag,
|
|
* the firing interval is set to the Thing's frequency.
|
|
*
|
|
* @param {Cannon} thing
|
|
*/
|
|
spawnCannon(thing: ICannon): void {
|
|
if (thing.noBullets) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
thing.FSM.animateCannonFiring,
|
|
thing.frequency,
|
|
thing.frequency,
|
|
thing);
|
|
}
|
|
|
|
/**
|
|
* Spawning callback for CastleBlocks. If the Thing has fireballs, an Array
|
|
* of them are made and animated to tick around the block like a clock, set
|
|
* by the thing's speed and direction.
|
|
*
|
|
* @param {CastleBlock} thing
|
|
*/
|
|
spawnCastleBlock(thing: ICastleBlock): void {
|
|
if (!thing.fireballs) {
|
|
return;
|
|
}
|
|
|
|
var balls: ICastleFireball[] = [],
|
|
i: number;
|
|
|
|
for (i = 0; i < thing.fireballs; i += 1) {
|
|
balls.push(<ICastleFireball>thing.FSM.addThing("CastleFireball"));
|
|
thing.FSM.setMidObj(balls[i], thing);
|
|
}
|
|
|
|
if (thing.speed >= 0) {
|
|
thing.dt = 0.07;
|
|
thing.angle = 0.25;
|
|
} else {
|
|
thing.dt = -0.07;
|
|
thing.angle = -0.25;
|
|
}
|
|
|
|
if (!thing.direction) {
|
|
thing.direction = -1;
|
|
}
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
thing.FSM.animateCastleBlock,
|
|
Math.round(7 / Math.abs(thing.speed)),
|
|
Infinity,
|
|
thing,
|
|
balls);
|
|
}
|
|
|
|
/**
|
|
* Spawning callback for floating Things, such as Koopas and Platforms. The
|
|
* Thing's begin and end attributes are set relative to the MapScreener's
|
|
* floor, so its movement can handle cycling between the two.
|
|
*
|
|
* @param {Thing} thing
|
|
*/
|
|
spawnMoveFloating(thing: IThingFloating): void {
|
|
// Make sure thing.begin <= thing.end
|
|
thing.FSM.setMovementEndpoints(thing);
|
|
|
|
// Make thing.begin and thing.end relative to the area's floor
|
|
thing.begin = (
|
|
thing.FSM.MapScreener.floor
|
|
* thing.FSM.unitsize - thing.begin);
|
|
thing.end = (
|
|
thing.FSM.MapScreener.floor
|
|
* thing.FSM.unitsize - thing.end);
|
|
}
|
|
|
|
/**
|
|
* Spawning callback for sliding Things, such as Platforms. The Thing's
|
|
* begin and end attributes do not need to be relative to anything.
|
|
*
|
|
* @param {Thing} thing
|
|
*/
|
|
spawnMoveSliding(thing: IThingSliding): void {
|
|
// Make sure thing.begin <= thing.end
|
|
thing.FSM.setMovementEndpoints(thing);
|
|
}
|
|
|
|
/**
|
|
* Spawning callback for a Platform that's a part of a Scale. ???
|
|
*
|
|
* @param {Platform} thing
|
|
*/
|
|
spawnScalePlatform(thing: IPlatform): void {
|
|
var collection: any = thing.collection || {},
|
|
ownKey: string = thing.collectionKey === "platformLeft" ? "Left" : "Right",
|
|
partnerKey: string = ownKey === "Left" ? "Right" : "Left";
|
|
|
|
thing.partners = {
|
|
"ownString": collection["string" + ownKey],
|
|
"partnerString": collection["string" + partnerKey],
|
|
"partnerPlatform": collection["platform" + partnerKey]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generator callback to create a random CheepCheep. The spawn is given a
|
|
* random x-velocity, is placed at a random point just below the screen, and
|
|
* is oriented towards the player.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
*/
|
|
spawnRandomCheep(FSM: FullScreenMario): boolean {
|
|
if (!FSM.MapScreener.spawningCheeps) {
|
|
return true;
|
|
}
|
|
|
|
var spawn: ICheepCheep;
|
|
|
|
spawn = FSM.ObjectMaker.make("CheepCheep", {
|
|
"flying": true,
|
|
"xvel": FSM.NumberMaker.random() * FSM.unitsize * 1.4,
|
|
"yvel": FSM.unitsize * -1.4
|
|
});
|
|
|
|
FSM.addThing(spawn, FSM.NumberMaker.random() * FSM.MapScreener.width, FSM.MapScreener.height);
|
|
|
|
if (spawn.left < FSM.MapScreener.width / 2) {
|
|
FSM.flipHoriz(spawn);
|
|
} else {
|
|
spawn.xvel *= -1;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Generator callback to create a BulleBill. The spawn moves horizontally
|
|
* at a constant rate towards the left side of the bill, and is placed at a
|
|
* random point to the right side of the screen.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @return {Boolean} Whether the spawn cancelled (FSM.MapScreenr's
|
|
* spawningBulletBills is false).
|
|
*/
|
|
spawnRandomBulletBill(FSM: FullScreenMario): boolean {
|
|
if (!FSM.MapScreener.spawningBulletBills) {
|
|
return true;
|
|
}
|
|
|
|
var spawn: IBulletBill;
|
|
|
|
spawn = FSM.ObjectMaker.make("BulletBill");
|
|
spawn.direction = 1;
|
|
spawn.moveleft = true;
|
|
spawn.xvel *= -1;
|
|
FSM.flipHoriz(spawn);
|
|
|
|
FSM.addThing(
|
|
spawn,
|
|
FSM.MapScreener.width,
|
|
Math.floor(
|
|
FSM.NumberMaker.randomIntWithin(0, FSM.MapScreener.floor) / 8
|
|
) * 8 * FSM.unitsize);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Spawns a CustomText by killing it and placing the contents of its texts
|
|
* member variable. These are written with a determined amount of spacing
|
|
* between them, as if by a typewriter.
|
|
*
|
|
* @param {CustomText} thing
|
|
*/
|
|
spawnCustomText(thing: ICustomText): void {
|
|
var top: number = thing.top,
|
|
texts: ICustomTextInfo[] = thing.texts,
|
|
attributes: any = thing.textAttributes,
|
|
spacingHorizontal: number = thing.spacingHorizontal * thing.FSM.unitsize,
|
|
spacingVertical: number = thing.spacingVertical * thing.FSM.unitsize,
|
|
spacingVerticalBlank: number = thing.spacingVerticalBlank * thing.FSM.unitsize,
|
|
children: IThing[] = [],
|
|
left: number,
|
|
text: string,
|
|
letter: string,
|
|
textThing: IThing,
|
|
i: number,
|
|
j: number;
|
|
|
|
thing.children = children;
|
|
|
|
for (i = 0; i < texts.length; i += 1) {
|
|
if (!texts[i]) {
|
|
top += spacingVerticalBlank;
|
|
continue;
|
|
}
|
|
|
|
text = texts[i].text;
|
|
|
|
if (texts[i].offset) {
|
|
left = thing.left + texts[i].offset * thing.FSM.unitsize;
|
|
} else {
|
|
left = thing.left;
|
|
}
|
|
|
|
for (j = 0; j < text.length; j += 1) {
|
|
letter = text[j];
|
|
|
|
if (thing.FSM.customTextMappings.hasOwnProperty(letter)) {
|
|
letter = thing.FSM.customTextMappings[letter];
|
|
}
|
|
letter = "Text" + thing.size + letter;
|
|
|
|
textThing = thing.FSM.ObjectMaker.make(
|
|
letter, attributes
|
|
);
|
|
textThing.FSM.addThing(textThing, left, top);
|
|
children.push(textThing);
|
|
|
|
left += textThing.width * thing.FSM.unitsize;
|
|
left += spacingHorizontal;
|
|
}
|
|
top += spacingVertical;
|
|
}
|
|
|
|
thing.FSM.killNormal(thing);
|
|
}
|
|
|
|
/**
|
|
* Spawning callback for generic detectors, activated as soon as they are
|
|
* placed. The Thing's activate trigger is called, then it is killed.
|
|
*
|
|
* @param {Detector} thing
|
|
*/
|
|
spawnDetector(thing: IDetector): void {
|
|
thing.activate(thing);
|
|
thing.FSM.killNormal(thing);
|
|
}
|
|
|
|
/**
|
|
* Spawning callback for ScrollBlockers. If the Thing is too the right of
|
|
* the visible viewframe, it should limit scrolling when triggered.
|
|
*
|
|
* @param {ScrollBlocker} thing
|
|
*/
|
|
spawnScrollBlocker(thing: IScrollBlocker): void {
|
|
if (thing.FSM.MapScreener.width < thing.right) {
|
|
thing.setEdge = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used by Things in a collection to register themselves as a part of their
|
|
* container collection Object. This is called by onThingMake, so they're
|
|
* immediately put in the collection and have it as a member variable.
|
|
*
|
|
* @param {Object} collection The collection Object shared by all members
|
|
* of it. It should be automatically generated.
|
|
* @param {Thing} thing A member of the collection being spawned.
|
|
* @remarks This should be bound in prethings as ".bind(scope, collection)"
|
|
*/
|
|
spawnCollectionComponent(collection: any, thing: IThing): void {
|
|
thing.collection = collection;
|
|
collection[thing.collectionName] = thing;
|
|
}
|
|
|
|
/**
|
|
* Used by Things in a collection to get direct references to other Things
|
|
* ("partners") in that collection. This is called by onThingAdd, so it's
|
|
* always after spawnCollectionComponent (which is by onThingMake).
|
|
*
|
|
* @param {Object} collection The collection Object shared by all members
|
|
* of it. It should be automatically generated.
|
|
* @param {Thing} thing A member of the collection being spawned.
|
|
* @remarks This should be bound in prethings as ".bind(scope, collection)"
|
|
*/
|
|
spawnCollectionPartner(collection: any, thing: IThing): void {
|
|
var partnerNames: any = thing.collectionPartnerNames,
|
|
partners: any = {},
|
|
name: string;
|
|
|
|
for (name in partnerNames) {
|
|
if (partnerNames.hasOwnProperty(name)) {
|
|
partners[name] = collection[partnerNames[name]];
|
|
}
|
|
}
|
|
|
|
thing.partners = {};
|
|
}
|
|
|
|
/**
|
|
* Spawning callback for RandomSpawner Things, which generate a set of
|
|
* commands using the WorldSeeder to be piped into the MapsHandlr, then
|
|
* spawn the immediate area.
|
|
*
|
|
* @param {RandomSpawner} thing
|
|
*/
|
|
spawnRandomSpawner(thing: IRandomSpawner): void {
|
|
var FSM: FullScreenMario = thing.FSM,
|
|
left: number = (thing.left + FSM.MapScreener.left) / FSM.unitsize;
|
|
|
|
FSM.WorldSeeder.clearGeneratedCommands();
|
|
FSM.WorldSeeder.generateFull({
|
|
"title": thing.randomization,
|
|
"top": thing.randomTop,
|
|
"right": left + thing.randomWidth,
|
|
"bottom": thing.randomBottom,
|
|
"left": left,
|
|
"width": thing.randomWidth,
|
|
"height": thing.randomTop - thing.randomBottom
|
|
});
|
|
FSM.WorldSeeder.runGeneratedCommands();
|
|
|
|
FSM.MapsHandler.spawnMap(
|
|
"xInc",
|
|
FSM.QuadsKeeper.top / FSM.unitsize,
|
|
FSM.QuadsKeeper.right / FSM.unitsize,
|
|
FSM.QuadsKeeper.bottom / FSM.unitsize,
|
|
FSM.QuadsKeeper.left / FSM.unitsize);
|
|
}
|
|
|
|
/**
|
|
* Activation callback for starting spawnRandomCheep on an interval.
|
|
* MapScreener is notified that spawningCheeps is true.
|
|
*
|
|
* @param {Detector} thing
|
|
*/
|
|
activateCheepsStart(thing: IDetector): void {
|
|
thing.FSM.MapScreener.spawningCheeps = true;
|
|
thing.FSM.TimeHandler.addEventInterval(thing.FSM.spawnRandomCheep, 21, Infinity, thing.FSM);
|
|
}
|
|
|
|
/**
|
|
* Activation callback to stop spawning CheepCheeps. MapScreener is notified
|
|
* that spawningCheeps is false.
|
|
*
|
|
* @param {Detector} thing
|
|
*/
|
|
activateCheepsStop(thing: IDetector): void {
|
|
thing.FSM.MapScreener.spawningCheeps = false;
|
|
}
|
|
|
|
/**
|
|
* Activation callback for starting spawnRandomBulletBill on an interval.
|
|
* MapScreener is notified that spawningBulletBills is true.
|
|
*
|
|
* @param {Detector} thing
|
|
*/
|
|
activateBulletBillsStart(thing: IDetector): void {
|
|
thing.FSM.MapScreener.spawningBulletBills = true;
|
|
thing.FSM.TimeHandler.addEventInterval(thing.FSM.spawnRandomBulletBill, 210, Infinity, thing.FSM);
|
|
}
|
|
|
|
/**
|
|
* Activation callback to stop spawning BulletBills. MapScreener is notified
|
|
* that spawningBulletBills is false.
|
|
*
|
|
* @param {Detector} thing
|
|
*/
|
|
activateBulletBillsStop(thing: IDetector): void {
|
|
thing.FSM.MapScreener.spawningBulletBills = false;
|
|
}
|
|
|
|
/**
|
|
* Activation callback to tell the area's Lakitu, if it exists, to start
|
|
* fleeing the scene.
|
|
*
|
|
* @param {Detector} thing
|
|
*/
|
|
activateLakituStop(thing: IDetector): void {
|
|
var lakitu: ILakitu = thing.FSM.MapScreener.lakitu;
|
|
|
|
if (!lakitu) {
|
|
return;
|
|
}
|
|
|
|
lakitu.fleeing = true;
|
|
lakitu.movement = thing.FSM.moveLakituFleeing;
|
|
}
|
|
|
|
/**
|
|
* Activation callback for a warp world area, triggered by the player
|
|
* touching a collider on top of it. Piranhas disappear and texts are
|
|
* revealed.
|
|
*
|
|
* @param {Thing} player
|
|
* @param {DetectCollision} other
|
|
*/
|
|
activateWarpWorld(thing: ICharacter, other: IDetectCollision): void {
|
|
var collection: any = other.collection,
|
|
key: number = 0,
|
|
keyString: string,
|
|
texts: IThing[],
|
|
j: number;
|
|
|
|
if (!thing.player) {
|
|
return;
|
|
}
|
|
|
|
texts = collection.Welcomer.children;
|
|
for (j = 0; j < texts.length; j += 1) {
|
|
if (texts[j].title !== "TextSpace") {
|
|
texts[j].hidden = false;
|
|
}
|
|
}
|
|
|
|
while (true) {
|
|
keyString = key + "-Text";
|
|
if (!collection.hasOwnProperty(keyString)) {
|
|
break;
|
|
}
|
|
|
|
texts = collection[keyString].children;
|
|
for (j = 0; j < texts.length; j += 1) {
|
|
if (texts[j].title !== "TextSpace") {
|
|
texts[j].hidden = false;
|
|
}
|
|
}
|
|
|
|
thing.FSM.killNormal(collection[key + "-Piranha"]);
|
|
|
|
key += 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Activation callback for when the player lands on a RestingStone. The
|
|
* stone "appears" (via opacity), the regular theme plays if it wasn't
|
|
* already, and the RestingStone waits to kill itself when the player isn't
|
|
* touching it.
|
|
*
|
|
* @param {RestingStone} thing
|
|
* @param {Player} other
|
|
*/
|
|
activateRestingStone(thing: IRestingStone, other: IPlayer): void {
|
|
if (thing.activated) {
|
|
return;
|
|
}
|
|
|
|
thing.activated = true;
|
|
thing.opacity = 1;
|
|
thing.FSM.AudioPlayer.playTheme();
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
if (other.resting === thing) {
|
|
return false;
|
|
}
|
|
|
|
thing.FSM.killNormal(thing);
|
|
return true;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
/**
|
|
* Generic activation callback for DetectWindow Things. This is typically
|
|
* set as a .movement Function, so it waits until the calling Thing is
|
|
* within the MapScreener's area to call the activate Function and kill
|
|
* itself.
|
|
*
|
|
* @param {DetectWindow} thing
|
|
*/
|
|
activateWindowDetector(thing: IDetectWindow): void {
|
|
if (thing.FSM.MapScreener.right - thing.FSM.MapScreener.left < thing.left) {
|
|
return;
|
|
}
|
|
|
|
thing.activate(thing);
|
|
thing.FSM.killNormal(thing);
|
|
}
|
|
|
|
/**
|
|
* Activation callback for ScrollBlocker Things. These are WindowDetectors
|
|
* that set MapScreener.canscroll to false when they're triggered. If the
|
|
* latest scrollWindow call pushed it too far to the left, it scrolls back
|
|
* the other way.
|
|
*
|
|
* @param {ScrollBlocker} thing
|
|
*/
|
|
activateScrollBlocker(thing: IScrollBlocker): void {
|
|
var dx: number = thing.FSM.MapScreener.width - thing.left;
|
|
|
|
thing.FSM.MapScreener.canscroll = false;
|
|
if (thing.setEdge && dx > 0) {
|
|
thing.FSM.scrollWindow(-dx);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Activation callback for ScrollBlocker Things. These are DetectCollision
|
|
* that set MapScreener.canscroll to true when they're triggered.
|
|
*
|
|
* @param {DetectCollision} thing
|
|
*/
|
|
activateScrollEnabler(thing: IDetectCollision): void {
|
|
thing.FSM.MapScreener.canscroll = true;
|
|
}
|
|
|
|
/**
|
|
* Activates the "before" component of a stretchable section. The creation
|
|
* commands of the section are loaded onto the screen as is and a
|
|
* DetectWindow is added to their immediate right that will trigger the
|
|
* equivalent activateSectionStretch.
|
|
*
|
|
* @param {DetectWindow} thing
|
|
*/
|
|
activateSectionBefore(thing: ISectionDetector): void {
|
|
var FSM: FullScreenMario = thing.FSM,
|
|
MapsCreator: MapsCreatr.IMapsCreatr = FSM.MapsCreator,
|
|
MapScreener: MapScreenr.MapScreenr = FSM.MapScreener,
|
|
MapsHandler: MapsHandlr.IMapsHandlr = FSM.MapsHandler,
|
|
area: IArea = <IArea>MapsHandler.getArea(),
|
|
map: MapsCreatr.IMapsCreatrMap = MapsHandler.getMap(),
|
|
prethings: { [i: string]: MapsCreatr.IPreThing[] } = MapsHandler.getPreThings(),
|
|
section: any = area.sections[thing.section || 0],
|
|
left: number = (thing.left + MapScreener.left) / FSM.unitsize,
|
|
before: any[] = section.before ? section.before.creation : undefined,
|
|
command: any,
|
|
i: number;
|
|
|
|
// If there is a before, parse each command into the prethings array
|
|
if (before) {
|
|
for (i = 0; i < before.length; i += 1) {
|
|
// A copy of the command must be used to not modify the original
|
|
command = FSM.proliferate({}, before[i]);
|
|
|
|
// The command's x must be shifted by the thing's placement
|
|
if (!command.x) {
|
|
command.x = left;
|
|
} else {
|
|
command.x += left;
|
|
}
|
|
|
|
// For Platforms that slide around, start and end are dynamic
|
|
if (command.sliding) {
|
|
command.begin += left;
|
|
command.end += left;
|
|
}
|
|
|
|
MapsCreator.analyzePreSwitch(command, prethings, area, map);
|
|
}
|
|
}
|
|
|
|
// Add a prething at the end of all this to trigger the stretch part
|
|
command = {
|
|
"thing": "DetectWindow",
|
|
"x": left + (before ? section.before.width : 0), "y": 0,
|
|
"activate": FSM.activateSectionStretch,
|
|
"section": thing.section || 0
|
|
};
|
|
|
|
MapsCreator.analyzePreSwitch(command, prethings, area, map);
|
|
|
|
// Spawn new Things that should be placed for being nearby
|
|
MapsHandler.spawnMap(
|
|
"xInc",
|
|
MapScreener.top / FSM.unitsize,
|
|
(MapScreener.left + FSM.QuadsKeeper.right) / FSM.unitsize,
|
|
MapScreener.bottom / FSM.unitsize,
|
|
left);
|
|
}
|
|
|
|
/**
|
|
* Activates the "stretch" component of a stretchable section. The creation
|
|
* commands of the section are loaded onto the screen and have their widths
|
|
* set to take up the entire width of the screen. A DetectWindow is added
|
|
* to their immediate right that will trigger the equivalent
|
|
* activateSectionAfter.
|
|
*
|
|
* @param {DetectWindow} thing
|
|
*/
|
|
activateSectionStretch(thing: ISectionDetector): void {
|
|
var FSM: FullScreenMario = thing.FSM,
|
|
MapsCreator: MapsCreatr.IMapsCreatr = FSM.MapsCreator,
|
|
MapScreener: MapScreenr.MapScreenr = FSM.MapScreener,
|
|
MapsHandler: MapsHandlr.IMapsHandlr = FSM.MapsHandler,
|
|
area: IArea = <IArea>MapsHandler.getArea(),
|
|
map: MapsCreatr.IMapsCreatrMap = MapsHandler.getMap(),
|
|
prethings: { [i: string]: MapsCreatr.IPreThing[] } = MapsHandler.getPreThings(),
|
|
section: any = area.sections[thing.section || 0],
|
|
stretch: any[] = section.stretch ? section.stretch.creation : undefined,
|
|
left: number = (thing.left + MapScreener.left) / FSM.unitsize,
|
|
width: number = MapScreener.width / FSM.unitsize,
|
|
command: IPreThingSettings,
|
|
i: number;
|
|
|
|
// If there is a stretch, parse each command into the current prethings array
|
|
if (stretch) {
|
|
for (i = 0; i < stretch.length; i += 1) {
|
|
// A copy of the command must be used, so the original isn't modified
|
|
command = FSM.proliferate({}, stretch[i]);
|
|
command.x = left;
|
|
|
|
// "stretch" the command by making its width equal to the screen
|
|
command.width = width;
|
|
MapsCreator.analyzePreSwitch(command, prethings, area, map);
|
|
}
|
|
|
|
// Add a prething at the end of all this to trigger the after part
|
|
command = {
|
|
"thing": "DetectWindow",
|
|
"x": left + width,
|
|
"y": 0,
|
|
"activate": FSM.activateSectionAfter,
|
|
"section": thing.section || 0
|
|
};
|
|
MapsCreator.analyzePreSwitch(command, prethings, area, map);
|
|
}
|
|
|
|
// Spawn the map, so new Things that should be placed will be spawned if nearby
|
|
MapsHandler.spawnMap(
|
|
"xInc",
|
|
MapScreener.top / FSM.unitsize,
|
|
left + (MapScreener.width / FSM.unitsize),
|
|
MapScreener.bottom / FSM.unitsize,
|
|
left);
|
|
}
|
|
|
|
/**
|
|
* Activates the "after" component of a stretchable sectin. The creation
|
|
* commands of the stretch are loaded onto the screen as is.
|
|
*
|
|
* @param {DetectWindow} thing
|
|
*/
|
|
activateSectionAfter(thing: ISectionDetector): void {
|
|
// Since the section was passed, do the rest of things normally
|
|
var FSM: FullScreenMario = thing.FSM,
|
|
MapsCreator: MapsCreatr.IMapsCreatr = FSM.MapsCreator,
|
|
MapScreener: MapScreenr.MapScreenr = FSM.MapScreener,
|
|
MapsHandler: MapsHandlr.IMapsHandlr = FSM.MapsHandler,
|
|
area: IArea = <IArea>MapsHandler.getArea(),
|
|
map: MapsCreatr.IMapsCreatrMap = MapsHandler.getMap(),
|
|
prethings: { [i: string]: MapsCreatr.IPreThing[] } = MapsHandler.getPreThings(),
|
|
section: any = area.sections[thing.section || 0],
|
|
left: number = (thing.left + MapScreener.left) / FSM.unitsize,
|
|
after: any[] = section.after ? section.after.creation : undefined,
|
|
command: any,
|
|
i: number;
|
|
|
|
// If there is an after, parse each command into the current prethings array
|
|
if (after) {
|
|
for (i = 0; i < after.length; i += 1) {
|
|
// A copy of the command must be used, so the original isn't modified
|
|
command = FSM.proliferate({}, after[i]);
|
|
|
|
// The command's x-location must be shifted by the thing's placement
|
|
if (!command.x) {
|
|
command.x = left;
|
|
} else {
|
|
command.x += left;
|
|
}
|
|
|
|
// For Platforms that slide around, start and end are dynamic
|
|
if (command.sliding) {
|
|
command.begin += left;
|
|
command.end += left;
|
|
}
|
|
|
|
MapsCreator.analyzePreSwitch(command, prethings, area, map);
|
|
}
|
|
}
|
|
|
|
// Spawn the map, so new Things that should be placed will be spawned if nearby
|
|
MapsHandler.spawnMap(
|
|
"xInc",
|
|
MapScreener.top / FSM.unitsize,
|
|
left + (MapScreener.right / FSM.unitsize),
|
|
MapScreener.bottom / FSM.unitsize,
|
|
left);
|
|
}
|
|
|
|
|
|
/* Collision functions
|
|
*/
|
|
|
|
/**
|
|
* Function generator for the generic hitCharacterSolid callback. This is
|
|
* used repeatedly by ThingHittr to generate separately optimized Functions
|
|
* for different Thing types.
|
|
*
|
|
* @return {Function}
|
|
*/
|
|
generateHitCharacterSolid(): (thing: ICharacter, other: ISolid) => void {
|
|
/**
|
|
* Generic callback for when a character touches a solid. Solids that
|
|
* "up" kill anything that didn't cause the up, but otherwise this will
|
|
* normally involve the solid's collide callback being called and
|
|
* under/undermid checks activating.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Solid} other
|
|
*/
|
|
return function hitCharacterSolid(thing: ICharacter, other: ISolid): void {
|
|
// "Up" solids are special (they kill things that aren't their .up)
|
|
if (other.up && thing !== other.up) {
|
|
return thing.FSM.collideCharacterSolidUp(thing, other);
|
|
}
|
|
|
|
other.collide(thing, other);
|
|
|
|
// If a character is bumping into the bottom, call that
|
|
if (thing.undermid) {
|
|
if (thing.undermid.bottomBump) {
|
|
thing.undermid.bottomBump(thing.undermid, thing);
|
|
}
|
|
} else if (thing.under && thing.under && (<any>thing.under).bottomBump) {
|
|
(<any>thing.under).bottomBump(thing.under[0], thing);
|
|
}
|
|
|
|
// If the character is overlapping the solid, call that too
|
|
if (
|
|
thing.checkOverlaps
|
|
&& thing.FSM.isCharacterOverlappingSolid(thing, other)) {
|
|
thing.FSM.markOverlap(<ICharacterOverlapping>thing, other);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Function generator for the generic hitCharacterCharacter callback. This
|
|
* is used repeatedly by ThingHittr to generate separately optimized
|
|
* Functions for different Thing types.
|
|
*
|
|
* @return {Function}
|
|
*/
|
|
generateHitCharacterCharacter(): (thing: ICharacter, other: ICharacter) => void {
|
|
/**
|
|
* Generic callback for when a character touches another character. The
|
|
* first Thing's collide callback is called unless it's a player, in
|
|
* which the other Thing's is.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Character} other
|
|
*/
|
|
return function hitCharacterCharacter(thing: ICharacter, other: ICharacter): void {
|
|
// The player calls the other's collide function, such as playerStar
|
|
if (thing.player) {
|
|
if (other.collide) {
|
|
return other.collide(thing, other);
|
|
}
|
|
} else if (thing.collide) {
|
|
// Otherwise just use thing's collide function
|
|
thing.collide(other, thing);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Collision callback used by most Items. The item's action callback will
|
|
* be called only if the first Thing is a player.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Item} other
|
|
*/
|
|
collideFriendly(thing: ICharacter, other: IItem): void {
|
|
if (!thing.player || !thing.FSM.isThingAlive(other)) {
|
|
return;
|
|
}
|
|
|
|
if (other.action) {
|
|
other.action(<IPlayer>thing, other);
|
|
}
|
|
|
|
other.death(other);
|
|
}
|
|
|
|
/**
|
|
* General callback for when a character touches a solid. This mostly
|
|
* determines if the character is on top (it should rest on the solid), to
|
|
* the side (it should shouldn't overlap), or undernearth (it also shouldn't
|
|
* overlap).
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Solid} other
|
|
*/
|
|
collideCharacterSolid(thing: ICharacter, other: ISolid): void {
|
|
if (other.up === thing) {
|
|
return;
|
|
}
|
|
|
|
// Character on top of solid
|
|
if (thing.FSM.isCharacterOnSolid(thing, other)) {
|
|
if (other.hidden && !other.collideHidden) {
|
|
return;
|
|
}
|
|
|
|
if (thing.resting !== other) {
|
|
thing.resting = other;
|
|
if (thing.onResting) {
|
|
thing.onResting(thing, other);
|
|
}
|
|
if (other.onRestedUpon) {
|
|
other.onRestedUpon(other, thing);
|
|
}
|
|
}
|
|
} else if (thing.FSM.isSolidOnCharacter(other, thing)) {
|
|
// Solid on top of character
|
|
var midx: number = thing.FSM.getMidX(thing);
|
|
|
|
if (midx > other.left && midx < other.right) {
|
|
thing.undermid = other;
|
|
} else if (other.hidden && !other.collideHidden) {
|
|
return;
|
|
}
|
|
|
|
if (!thing.under) {
|
|
thing.under = [other];
|
|
} else {
|
|
thing.under.push(other);
|
|
}
|
|
|
|
if (thing.player) {
|
|
(<IPlayer>thing).keys.jump = false;
|
|
thing.FSM.setTop(thing, other.bottom - thing.toly + other.yvel);
|
|
}
|
|
|
|
thing.yvel = other.yvel;
|
|
}
|
|
|
|
if (other.hidden && !other.collideHidden) {
|
|
return;
|
|
}
|
|
|
|
// Character bumping into the side of the solid
|
|
if (
|
|
thing.resting !== other
|
|
&& !thing.FSM.isCharacterBumpingSolid(thing, other)
|
|
&& !thing.FSM.isThingOnThing(thing, other)
|
|
&& !thing.FSM.isThingOnThing(other, thing)
|
|
&& !thing.under
|
|
) {
|
|
// Character to the left of the solid
|
|
if (thing.right <= other.right) {
|
|
thing.xvel = Math.min(thing.xvel, 0);
|
|
thing.FSM.shiftHoriz(
|
|
thing,
|
|
Math.max(
|
|
other.left + thing.FSM.unitsize - thing.right,
|
|
thing.FSM.unitsize / -2
|
|
)
|
|
);
|
|
} else {
|
|
// Character to the right of the solid
|
|
thing.xvel = Math.max(thing.xvel, 0);
|
|
thing.FSM.shiftHoriz(
|
|
thing,
|
|
Math.min(
|
|
other.right - thing.FSM.unitsize - thing.left,
|
|
thing.FSM.unitsize / 2
|
|
)
|
|
);
|
|
}
|
|
|
|
// Non-players flip horizontally
|
|
if (!thing.player) {
|
|
if (!thing.noflip) {
|
|
thing.moveleft = !thing.moveleft;
|
|
}
|
|
// Some items require fancy versions (e.g. Shell)
|
|
if (thing.group === "item") {
|
|
thing.collide(other, thing);
|
|
}
|
|
} else if (other.actionLeft) {
|
|
// Players trigger other actions (e.g. Pipe's mapExitPipeHorizontal)
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerActionLeft", thing, other);
|
|
other.actionLeft(thing, other, other.transport);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a character hitting an "up" solid. If it has an
|
|
* onCollideUp callback, that is called; otherwise, it is killed.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Solid} other
|
|
*/
|
|
collideCharacterSolidUp(thing: ICharacter, other: ISolid): void {
|
|
if (thing.onCollideUp) {
|
|
thing.onCollideUp(thing, other);
|
|
} else {
|
|
thing.FSM.scoreOn(thing.scoreBelow, thing);
|
|
thing.death(thing, 2);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collision callback for an item hitting an "up" solid. Items just hop
|
|
* and switch direction.
|
|
*
|
|
* @param {Item} thing
|
|
* @param {Solid} other
|
|
*/
|
|
collideUpItem(thing: ICharacter, other: ISolid): void {
|
|
thing.FSM.animateCharacterHop(thing);
|
|
thing.moveleft = thing.FSM.objectToLeft(thing, other);
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a floating coin being hit by an "up" solid. It is
|
|
* animated, as if it were hit as the contents of a solid.
|
|
*
|
|
* @param {Coin} thing
|
|
* @param {Solid} other
|
|
*/
|
|
collideUpCoin(thing: ICoin, other: ISolid): void {
|
|
thing.blockparent = other;
|
|
thing.animate(thing, other);
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a player hitting a regular Coin. The Coin
|
|
* disappears but points and Coin totals are both increased, along with
|
|
* the "Coin" sound being played.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Coin} other
|
|
*/
|
|
collideCoin(thing: IPlayer, other: ICoin): void {
|
|
if (!thing.player) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.AudioPlayer.play("Coin");
|
|
thing.FSM.ItemsHolder.increase("score", 200);
|
|
thing.FSM.ItemsHolder.increase("coins", 1);
|
|
thing.FSM.killNormal(other);
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a player hitting a Star. The Star is killed, and
|
|
* the playerStarUp trigger is called on the Thing.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Star} other
|
|
*/
|
|
collideStar(thing: IPlayer, other: IStar): void {
|
|
if (!thing.player || thing.star) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.playerStarUp(thing);
|
|
thing.FSM.ModAttacher.fireEvent("onCollideStar", thing, other);
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a character being hit by a fireball. It will
|
|
* most likely be killed with an explosion unless it has the nofiredeath
|
|
* flag, in which case only the fireball dies.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Fireball} other
|
|
*/
|
|
collideFireball(thing: ICharacter, other: IFireball): void {
|
|
if (
|
|
!thing.FSM.isThingAlive(thing)
|
|
|| thing.height < thing.FSM.unitsize
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (thing.nofire) {
|
|
if (thing.nofire > 1) {
|
|
other.death(other);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (thing.nofiredeath) {
|
|
thing.FSM.AudioPlayer.playLocal(
|
|
"Bump", thing.FSM.getMidX(other));
|
|
thing.death(thing);
|
|
} else {
|
|
thing.FSM.AudioPlayer.playLocal(
|
|
"Kick", thing.FSM.getMidX(other));
|
|
thing.death(thing, 2);
|
|
thing.FSM.scoreOn(thing.scoreFire, thing);
|
|
}
|
|
|
|
other.death(other);
|
|
}
|
|
|
|
/**
|
|
* Collision callback for hitting a CastleFireball. The character is killed
|
|
* unless it has the star flag, in which case the CastleFireball is.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {CastleFireball} other
|
|
*/
|
|
collideCastleFireball(thing: ICharacter, other: ICastleFireball): void {
|
|
if ((<any>thing).star) {
|
|
other.death(other);
|
|
} else {
|
|
thing.death(thing);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collision callback for when a character hits a Shell. This covers various
|
|
* cases, such as deaths, side-to-side Shell collisions, player stomps, and
|
|
* so on.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Shell} other
|
|
*/
|
|
collideShell(thing: ICharacter, other: IShell): void {
|
|
// If only one is a shell, it should be other, not thing
|
|
if (thing.shell) {
|
|
if (other.shell) {
|
|
return thing.FSM.collideShellShell(<IShell>thing, other);
|
|
}
|
|
return thing.FSM.collideShell(thing, other);
|
|
}
|
|
|
|
// Hitting a solid (e.g. wall)
|
|
if (thing.groupType === "Solid") {
|
|
return thing.FSM.collideShellSolid(<any>thing, other);
|
|
}
|
|
|
|
// Hitting the player
|
|
if (thing.player) {
|
|
return thing.FSM.collideShellPlayer(<any>thing, <any>other);
|
|
}
|
|
|
|
// Assume anything else to be an enemy, which only moving shells kill
|
|
if (other.xvel) {
|
|
thing.FSM.killFlip(thing);
|
|
if (thing.shellspawn) {
|
|
thing = <any>thing.FSM.killSpawn(thing);
|
|
}
|
|
|
|
thing.FSM.AudioPlayer.play("Kick");
|
|
thing.FSM.scoreOn(thing.FSM.findScore(other.enemyhitcount), thing);
|
|
other.enemyhitcount += 1;
|
|
} else {
|
|
thing.moveleft = thing.FSM.objectToLeft(thing, other);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a solid being hit by a Shell. The Shell will
|
|
* bounce the opposition direction.
|
|
*
|
|
* @param {Solid} thing
|
|
* @param {Shell} other
|
|
*/
|
|
collideShellSolid(thing: ISolid, other: IShell): void {
|
|
if (other.right < thing.right) {
|
|
thing.FSM.AudioPlayer.playLocal("Bump", thing.left);
|
|
thing.FSM.setRight(other, thing.left);
|
|
other.xvel = -other.speed;
|
|
other.moveleft = true;
|
|
} else {
|
|
thing.FSM.AudioPlayer.playLocal("Bump", thing.right);
|
|
thing.FSM.setLeft(other, thing.right);
|
|
other.xvel = other.speed;
|
|
other.moveleft = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collision callback for when the player hits a Shell. This covers all the
|
|
* possible scenarios, and is much larger than common sense dictates.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Shell} other
|
|
*/
|
|
collideShellPlayer(thing: IPlayer, other: IShell): void {
|
|
var shelltoleft: boolean = thing.FSM.objectToLeft(other, thing),
|
|
playerjump: boolean = thing.yvel > 0 && (
|
|
thing.bottom <= other.top + thing.FSM.unitsize * 2);
|
|
|
|
// Star players kill the shell no matter what
|
|
if (thing.star) {
|
|
thing.FSM.scorePlayerShell(thing, other);
|
|
other.death(other, 2);
|
|
return;
|
|
}
|
|
|
|
// If the shell is already being landed on by the player, see if it's
|
|
// still being pushed to the side, or has reversed direction (is deadly)
|
|
if (other.landing) {
|
|
// Equal shelltoleft measurements: it's still being pushed
|
|
if (other.shelltoleft === shelltoleft) {
|
|
// Tepmorarily increase the landing count of the shell; if it is
|
|
// just being started, that counts as the score hit
|
|
other.landing += 1;
|
|
if (other.landing === 1) {
|
|
thing.FSM.scorePlayerShell(thing, other);
|
|
}
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (other: IShell): void {
|
|
other.landing -= 1;
|
|
},
|
|
2,
|
|
other);
|
|
} else {
|
|
// Different shelltoleft measurements: it's deadly
|
|
thing.death(thing);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If the shell is being kicked by the player, either by hitting a still
|
|
// shell or jumping onto an already moving one
|
|
if (other.xvel === 0 || playerjump) {
|
|
// Reset any signs of peeking from the shell
|
|
other.counting = 0;
|
|
|
|
// If the shell is standing still, make it move
|
|
if (other.xvel === 0) {
|
|
thing.FSM.AudioPlayer.play("Kick");
|
|
thing.FSM.scorePlayerShell(thing, other);
|
|
if (shelltoleft) {
|
|
other.moveleft = true;
|
|
other.xvel = -other.speed;
|
|
} else {
|
|
other.moveleft = false;
|
|
other.xvel = other.speed;
|
|
}
|
|
other.hitcount += 1;
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (other: IShell): void {
|
|
other.hitcount -= 1;
|
|
},
|
|
2,
|
|
other);
|
|
} else {
|
|
// Otherwise it was moving, but should now be still
|
|
other.xvel = 0;
|
|
}
|
|
|
|
if (other.peeking) {
|
|
other.peeking = 0;
|
|
thing.FSM.removeClass(other, "peeking");
|
|
other.height -= thing.FSM.unitsize / 8;
|
|
thing.FSM.updateSize(other);
|
|
}
|
|
|
|
// If the player is landing on the shell (with movements and xvels
|
|
// already set), the player should then jump up a bit
|
|
if (playerjump) {
|
|
thing.FSM.AudioPlayer.play("Kick");
|
|
|
|
if (!other.xvel) {
|
|
thing.FSM.jumpEnemy(thing, other);
|
|
thing.yvel *= 2;
|
|
// thing.FSM.scorePlayerShell(thing, other);
|
|
thing.FSM.setBottom(thing, other.top - thing.FSM.unitsize);
|
|
} else {
|
|
// thing.FSM.scorePlayerShell(thing, other);
|
|
}
|
|
|
|
other.landing += 1;
|
|
other.shelltoleft = shelltoleft;
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (other: IShell): void {
|
|
other.landing -= 1;
|
|
},
|
|
2,
|
|
other);
|
|
}
|
|
} else {
|
|
// Since the player is touching the shell normally, that's a death if
|
|
// the shell isn't moving away
|
|
if (!other.hitcount && (
|
|
(shelltoleft && other.xvel > 0)
|
|
|| (!shelltoleft && other.xvel < 0)
|
|
)) {
|
|
thing.death(thing);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collision callback for two Shells. If one is moving, it kills the other;
|
|
* otherwise, they bounce off.
|
|
*
|
|
* @param {Shell} thing
|
|
* @param {Shell} other
|
|
*/
|
|
collideShellShell(thing: IShell, other: IShell): void {
|
|
if (thing.xvel !== 0) {
|
|
if (other.xvel !== 0) {
|
|
var temp: number = thing.xvel;
|
|
thing.xvel = other.xvel;
|
|
other.xvel = temp;
|
|
|
|
thing.FSM.shiftHoriz(thing, thing.xvel);
|
|
thing.FSM.shiftHoriz(other, other.xvel);
|
|
} else {
|
|
thing.FSM.ItemsHolder.increase("score", 500);
|
|
other.death(other);
|
|
}
|
|
} else {
|
|
thing.FSM.ItemsHolder.increase("score", 500);
|
|
thing.death(thing);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a general character hitting an enemy. This covers
|
|
* many general cases, most of which involve a player and an enemy.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Enemy} other
|
|
*/
|
|
collideEnemy(thing: ICharacter, other: IEnemy): void {
|
|
// If either is a player, make it thing (not other)
|
|
if (!thing.player && other.player) {
|
|
return thing.FSM.collideEnemy(thing, other);
|
|
}
|
|
|
|
// Death: nothing happens
|
|
if (
|
|
!thing.FSM.isThingAlive(thing)
|
|
|| !thing.FSM.isThingAlive(other)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Items
|
|
if (thing.group === "item") {
|
|
if (thing.collidePrimary) {
|
|
return thing.collide(other, thing);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// For non-players, it's just to characters colliding: they bounce
|
|
if (!thing.player) {
|
|
thing.moveleft = thing.FSM.objectToLeft(thing, other);
|
|
other.moveleft = !thing.moveleft;
|
|
return;
|
|
}
|
|
|
|
// Player landing on top of an enemy
|
|
if (
|
|
((<IStar>thing).star && !other.nostar)
|
|
|| (
|
|
!thing.FSM.MapScreener.underwater
|
|
&& (!other.deadly && thing.FSM.isThingOnThing(thing, other))
|
|
)
|
|
) {
|
|
// For the sake of typing. Should be optimized during runtime.
|
|
var player: IPlayer = <IPlayer>thing;
|
|
|
|
// Enforces toly (not touching means stop)
|
|
if (player.FSM.isCharacterAboveEnemy(player, other)) {
|
|
return;
|
|
}
|
|
|
|
// A star player just kills the enemy, no matter what
|
|
if (player.star) {
|
|
other.nocollide = true;
|
|
other.death(other, 2);
|
|
player.FSM.scoreOn(other.scoreStar, other);
|
|
player.FSM.AudioPlayer.play("Kick");
|
|
} else {
|
|
// A non-star player kills the enemy with spawn, and hops
|
|
player.FSM.setBottom(
|
|
player,
|
|
Math.min(player.bottom, other.top + player.FSM.unitsize)
|
|
);
|
|
player.FSM.TimeHandler.addEvent(player.FSM.jumpEnemy, 0, player, other);
|
|
|
|
other.death(other, player.star ? 2 : 0);
|
|
|
|
player.FSM.addClass(player, "hopping");
|
|
player.FSM.removeClasses(
|
|
player, "running skidding jumping one two three"
|
|
);
|
|
player.hopping = true;
|
|
|
|
if (player.power === 1) {
|
|
player.FSM.setPlayerSizeSmall(player);
|
|
}
|
|
}
|
|
} else if (!thing.FSM.isCharacterAboveEnemy(thing, other)) {
|
|
// Player being landed on by an enemy
|
|
thing.death(thing);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Collision callback for a character bumping into the bottom of a solid.
|
|
* Only players cause the solid to jump and be considered "up", though large
|
|
* players will kill solids that have the breakable flag on. If the solid
|
|
* does jump and has contents, they emerge.
|
|
*
|
|
* @param {Solid} thing
|
|
* @param {Character} other
|
|
*/
|
|
collideBottomBrick(thing: IBrick, other: ICharacter): void {
|
|
if ((<any>other).solid && !thing.solid) {
|
|
return thing.FSM.collideBottomBrick(<any>other, <any>thing);
|
|
}
|
|
|
|
if (thing.up || !other.player) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.AudioPlayer.play("Bump");
|
|
|
|
if (thing.used) {
|
|
return;
|
|
}
|
|
|
|
thing.up = other;
|
|
if ((<IPlayer>other).power > 1 && thing.breakable && !thing.contents) {
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.killBrick, 2, thing, other);
|
|
return;
|
|
}
|
|
|
|
thing.FSM.animateSolidBump(thing);
|
|
|
|
if (thing.contents) {
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.FSM.animateSolidContents(thing, <any>other);
|
|
|
|
if (thing.contents !== "Coin") {
|
|
thing.FSM.animateBlockBecomesUsed(<any>thing);
|
|
} else {
|
|
if (thing.lastcoin) {
|
|
thing.FSM.animateBlockBecomesUsed(<any>thing);
|
|
} else {
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.lastcoin = true;
|
|
},
|
|
245);
|
|
}
|
|
}
|
|
},
|
|
7);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collision callback for the player hitting the bottom of a Block. Unused
|
|
* Blocks have their contents emerge (by default a Coin), while used Blocks
|
|
* just have a small bump noise played.
|
|
*
|
|
* @param {Solid} thing
|
|
* @param {Player} other
|
|
*/
|
|
collideBottomBlock(thing: IBlock, other: IPlayer): void {
|
|
if ((<any>other).solid && !thing.solid) {
|
|
return thing.FSM.collideBottomBlock(<any>other, <any>thing);
|
|
}
|
|
|
|
if (thing.up || !other.player) {
|
|
return;
|
|
}
|
|
|
|
if (thing.used) {
|
|
thing.FSM.AudioPlayer.play("Bump");
|
|
return;
|
|
}
|
|
|
|
thing.used = true;
|
|
thing.hidden = false;
|
|
thing.up = other;
|
|
|
|
thing.FSM.animateSolidBump(thing);
|
|
thing.FSM.removeClass(thing, "hidden");
|
|
thing.FSM.switchClass(thing, "unused", "used");
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.animateSolidContents, 7, thing, other);
|
|
}
|
|
|
|
/**
|
|
* Collision callback for Vines. The player becomes "attached" to the Vine
|
|
* and starts climbing it, with movement set to movePlayerVine.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Solid} other
|
|
*/
|
|
collideVine(thing: IPlayer, other: ISolid): void {
|
|
if (!thing.player || thing.attachedSolid || thing.climbing) {
|
|
return;
|
|
}
|
|
|
|
if (thing.bottom > other.bottom + thing.FSM.unitsize * 2) {
|
|
return;
|
|
}
|
|
|
|
other.attachedCharacter = thing;
|
|
thing.attachedSolid = other;
|
|
|
|
thing.nofall = true;
|
|
thing.checkOverlaps = false;
|
|
thing.resting = undefined;
|
|
|
|
// To the left of the vine
|
|
if (thing.right < other.right) {
|
|
thing.lookleft = false;
|
|
thing.moveleft = false;
|
|
thing.attachedDirection = -1;
|
|
thing.FSM.unflipHoriz(thing);
|
|
} else {
|
|
// To the right of the vine
|
|
thing.lookleft = true;
|
|
thing.moveleft = true;
|
|
thing.attachedDirection = 1;
|
|
thing.FSM.flipHoriz(thing);
|
|
}
|
|
|
|
thing.FSM.thingPauseVelocity(thing);
|
|
thing.FSM.addClass(thing, "climbing");
|
|
thing.FSM.removeClasses(
|
|
thing, "running", "jumping", "skidding");
|
|
|
|
thing.FSM.TimeHandler.cancelClassCycle(thing, "running");
|
|
thing.FSM.TimeHandler.addClassCycle(
|
|
thing, ["one", "two"], "climbing", 0
|
|
);
|
|
|
|
thing.attachedLeft = !thing.FSM.objectToLeft(thing, other);
|
|
thing.attachedOff = thing.attachedLeft ? 1 : -1;
|
|
|
|
thing.movement = thing.FSM.movePlayerVine;
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a character hitting a Springboard. This acts as a
|
|
* normal solid to non-players, and only acts as a spring if the player is
|
|
* above it and moving down.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Springboard} other
|
|
*/
|
|
collideSpringboard(thing: ICharacter, other: ISpringboard): void {
|
|
if (
|
|
thing.player && thing.yvel >= 0 && !other.tension
|
|
&& thing.FSM.isCharacterOnSolid(thing, other)
|
|
) {
|
|
other.tension = other.tensionSave = Math.max(
|
|
thing.yvel * 0.77,
|
|
thing.FSM.unitsize
|
|
);
|
|
thing.movement = thing.FSM.movePlayerSpringboardDown;
|
|
(<IPlayer>thing).spring = other;
|
|
thing.xvel /= 2.8;
|
|
} else {
|
|
thing.FSM.collideCharacterSolid(thing, other);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a character hitting a WaterBlocker on the top of
|
|
* an underwater area. It simply stops them from moving up.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {WaterBlocker} other
|
|
*/
|
|
collideWaterBlocker(thing: ICharacter, other: ISolid): void {
|
|
thing.FSM.collideCharacterSolid(thing, other);
|
|
}
|
|
|
|
/**
|
|
* Collision callback for the DetectCollision on a flagpole at the end of an
|
|
* EndOutsideCastle. The Flagpole cutscene is started.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {DetectCollision} other
|
|
*/
|
|
collideFlagpole(thing: IPlayer, other: IDetectCollision): void {
|
|
if (thing.bottom > other.bottom) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.ScenePlayer.startCutscene("Flagpole", {
|
|
"player": thing,
|
|
"collider": other
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Collision callback for the player hitting a CastleAxe. The player and
|
|
* screen are paused for 140 steps (other callbacks should be animating
|
|
* the custcene).
|
|
*
|
|
* @param {Player} thing
|
|
* @param {CastleAxe} other
|
|
*/
|
|
collideCastleAxe(thing: IPlayer, other: ICastleAxe): void {
|
|
if (!thing.FSM.MathDecider.compute("canPlayerTouchCastleAxe", thing, other)) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.ScenePlayer.startCutscene("BowserVictory", {
|
|
"player": thing,
|
|
"axe": other
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a player hitting the DetectCollision placed next
|
|
* a CastleDoor in EndOutsideCastle. Things and the current time are added
|
|
* to cutscene settings. Infinite time goes directly to the Fireworks
|
|
* routine, while having non-infinite time goes to the Countdown routine.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {DetectCollision} other
|
|
*/
|
|
collideCastleDoor(thing: IPlayer, other: IDetectCollision): void {
|
|
thing.FSM.killNormal(thing);
|
|
if (!thing.player) {
|
|
return;
|
|
}
|
|
|
|
var time: number = thing.FSM.ItemsHolder.getItem("time");
|
|
|
|
thing.FSM.ScenePlayer.addCutsceneSetting("player", thing);
|
|
thing.FSM.ScenePlayer.addCutsceneSetting("detector", other);
|
|
thing.FSM.ScenePlayer.addCutsceneSetting("time", time);
|
|
|
|
if (time === Infinity) {
|
|
thing.FSM.ScenePlayer.playRoutine("Fireworks");
|
|
} else {
|
|
thing.FSM.ScenePlayer.playRoutine("Countdown");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a player reaching a castle NPC. Things and
|
|
* the NPC's keys are added to cutscene settings, and the Dialog routine
|
|
* is played.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {DetectCollision} other
|
|
*/
|
|
collideCastleNPC(thing: IPlayer, other: IDetectCollision): void {
|
|
var keys: any[] = other.collection.npc.collectionKeys;
|
|
|
|
thing.FSM.ScenePlayer.addCutsceneSetting("keys", keys);
|
|
thing.FSM.ScenePlayer.addCutsceneSetting("player", thing);
|
|
thing.FSM.ScenePlayer.addCutsceneSetting("detector", other);
|
|
|
|
thing.FSM.ScenePlayer.playRoutine("Dialog");
|
|
}
|
|
|
|
/**
|
|
* Collision callback for a player hitting the transportation Platform in
|
|
* cloud worlds. The player collides with it as normal for solids, but if
|
|
* the player is then resting on it, it becomes a normal moving platform
|
|
* with only horizontal momentum.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Solid} other
|
|
*/
|
|
collideTransport(thing: IPlayer, other: ISolid): void {
|
|
if (!thing.player) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.collideCharacterSolid(thing, other);
|
|
if (thing.resting !== other) {
|
|
return;
|
|
}
|
|
|
|
other.xvel = thing.FSM.unitsize / 2;
|
|
other.movement = thing.FSM.movePlatform;
|
|
other.collide = thing.FSM.collideCharacterSolid;
|
|
}
|
|
|
|
/**
|
|
* General collision callback for DetectCollision Things. The real activate
|
|
* callback is only hit if the Thing is a player; otherwise, an optional
|
|
* activateFail may be activated. The DetectCollision is then killed if it
|
|
* doesn't have the noActivateDeath flag.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {DetectCollision} other
|
|
*/
|
|
collideDetector(thing: ICharacter, other: IDetectCollision): void {
|
|
if (!thing.player) {
|
|
if (other.activateFail) {
|
|
other.activateFail(thing);
|
|
}
|
|
return;
|
|
}
|
|
|
|
other.activate(thing, other);
|
|
|
|
if (!other.noActivateDeath) {
|
|
thing.FSM.killNormal(other);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collision callback for level transports (any Thing with a .transport
|
|
* attribute). Depending on the transport, either the map or location are
|
|
* shifted to it.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Thing} other
|
|
*/
|
|
collideLevelTransport(thing: IPlayer, other: ISolid): void {
|
|
var transport: any = other.transport;
|
|
|
|
if (!thing.player) {
|
|
return;
|
|
}
|
|
|
|
if (typeof transport === "undefined") {
|
|
throw new Error("No transport given to collideLevelTransport");
|
|
}
|
|
|
|
if (transport.constructor === String) {
|
|
thing.FSM.setLocation(transport);
|
|
} else if (typeof transport.map !== "undefined") {
|
|
if (typeof transport.location !== "undefined") {
|
|
thing.FSM.setMap(transport.map, transport.location);
|
|
} else {
|
|
thing.FSM.setMap(transport.map);
|
|
}
|
|
} else if (typeof transport.location !== "undefined") {
|
|
thing.FSM.setLocation(transport.location);
|
|
} else {
|
|
throw new Error("Unknown transport type:" + transport);
|
|
}
|
|
}
|
|
|
|
|
|
/* Movement functions
|
|
*/
|
|
|
|
/**
|
|
* Base, generic movement Function for simple characters. The Thing moves
|
|
* at a constant rate in either the x or y direction, and switches direction
|
|
* only if directed by the engine (e.g. when it hits a Solid)
|
|
*
|
|
* @param {Character} thing
|
|
* @remarks thing.speed is the only required member attribute; .direction
|
|
* and .moveleft should be set by the game engine.
|
|
*/
|
|
moveSimple(thing: ICharacter): void {
|
|
// If the thing is looking away from the intended direction, flip it
|
|
if (thing.direction !== thing.moveleft) {
|
|
// thing.moveleft is truthy: it should now be looking to the right
|
|
if (thing.moveleft) {
|
|
thing.xvel = -thing.speed;
|
|
if (!thing.noflip) {
|
|
thing.FSM.unflipHoriz(thing);
|
|
}
|
|
} else {
|
|
// thing.moveleft is falsy: it should now be looking to the left
|
|
thing.xvel = thing.speed;
|
|
if (!thing.noflip) {
|
|
thing.FSM.flipHoriz(thing);
|
|
}
|
|
}
|
|
thing.direction = thing.moveleft;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extension of the moveSimple movement Function for Things that shouldn't
|
|
* fall off the edge of their resting blocks
|
|
*
|
|
* @param {Character} thing
|
|
*/
|
|
moveSmart(thing: ICharacter): void {
|
|
// Start off by calling moveSimple for normal movement
|
|
thing.FSM.moveSimple(thing);
|
|
|
|
// If this isn't resting, it's the same as moveSimple
|
|
if (thing.yvel !== 0) {
|
|
return;
|
|
}
|
|
|
|
if (!thing.resting || !thing.FSM.isCharacterOnResting(thing, thing.resting)) {
|
|
if (thing.moveleft) {
|
|
thing.FSM.shiftHoriz(thing, thing.FSM.unitsize, true);
|
|
} else {
|
|
thing.FSM.shiftHoriz(thing, -thing.FSM.unitsize, true);
|
|
}
|
|
thing.moveleft = !thing.moveleft;
|
|
}
|
|
|
|
|
|
// // Check for being over the edge in the direction of movement
|
|
// if (thing.moveleft) {
|
|
// if (thing.left + thing.FSM.unitsize <= thing.resting.left) {
|
|
// thing.FSM.shiftHoriz(thing, thing.FSM.unitsize);
|
|
// thing.moveleft = false;
|
|
// }
|
|
// } else {
|
|
// if (thing.right - thing.FSM.unitsize >= thing.resting.right) {
|
|
// thing.FSM.shiftHoriz(thing, -thing.FSM.unitsize);
|
|
// thing.moveleft = true;
|
|
// }
|
|
// }
|
|
}
|
|
|
|
/**
|
|
* Extension of the moveSimple movement Function for Things that should
|
|
* jump whenever they start resting.
|
|
*
|
|
* @param {Character} thing
|
|
* @remarks thing.jumpheight is required to know how high to jump
|
|
*/
|
|
moveJumping(thing: ICharacter): void {
|
|
// Start off by calling moveSimple for normal movement
|
|
thing.FSM.moveSimple(thing);
|
|
|
|
// If .resting, jump!
|
|
if (thing.resting) {
|
|
thing.yvel = -Math.abs(thing.jumpheight);
|
|
thing.resting = undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Things that slide back and forth, such as
|
|
* HammerBros and Lakitus.
|
|
*
|
|
* @remarks thing.counter must be a number set elsewhere, such as in a spawn
|
|
* Function.
|
|
*/
|
|
movePacing(thing: ICharacter): void {
|
|
thing.counter += .007;
|
|
thing.xvel = Math.sin(Math.PI * thing.counter) / 2.1;
|
|
}
|
|
|
|
/**
|
|
* Movement Function for HammerBros. They movePacing, look towards the
|
|
* player, and have the nocollidesolid flag if they're jumping up or
|
|
* intentionally falling through a solid.
|
|
*
|
|
* @param {HammerBro} thing
|
|
*/
|
|
moveHammerBro(thing: IHammerBro): void {
|
|
thing.FSM.movePacing(thing);
|
|
thing.FSM.lookTowardsPlayer(thing);
|
|
thing.nocollidesolid = thing.yvel < 0 || thing.falling;
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Bowser. Bowser always faces the player and
|
|
* movePaces if he's to the right of the player, or moves to the right if
|
|
* he's to the left.
|
|
*
|
|
* @param {Bowser} thing
|
|
*/
|
|
moveBowser(thing: IBowser): void {
|
|
// Facing to the right
|
|
if (thing.flipHoriz) {
|
|
// To the left of player: walk to the right
|
|
if (
|
|
thing.FSM.objectToLeft(thing, thing.FSM.player)
|
|
) {
|
|
thing.FSM.moveSimple(thing);
|
|
} else {
|
|
// To the right of player: look to the left and movePacing as normal
|
|
thing.lookleft = thing.moveleft = true;
|
|
thing.FSM.unflipHoriz(thing);
|
|
thing.FSM.movePacing(thing);
|
|
}
|
|
} else {
|
|
// Facing to the left
|
|
// To the left of player: look and walk to the right
|
|
if (
|
|
thing.FSM.objectToLeft(thing, thing.FSM.player)
|
|
) {
|
|
thing.lookleft = thing.moveleft = false;
|
|
thing.FSM.flipHoriz(thing);
|
|
thing.FSM.moveSimple(thing);
|
|
} else {
|
|
// To the right of the player: movePacing as normal
|
|
thing.FSM.movePacing(thing);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Bowser's spewed fire. It has a ylev stored from
|
|
* creation that will tell it when to stop changing its vertical
|
|
* velocity from this Function; otherwise, it shifts its vertical
|
|
* position to move to the ylev.
|
|
*
|
|
* @param {BowserFire} thing
|
|
*/
|
|
moveBowserFire(thing: IBowserFire): void {
|
|
if (Math.round(thing.bottom) === Math.round(thing.ylev)) {
|
|
thing.movement = undefined;
|
|
return;
|
|
}
|
|
|
|
thing.FSM.shiftVert(
|
|
thing,
|
|
Math.min(Math.max(0, thing.ylev - thing.bottom), thing.FSM.unitsize));
|
|
}
|
|
|
|
/**
|
|
* Movement function for Things that float up and down (vertically).
|
|
* If the Thing has reached thing.begin or thing.end, it gradually switches
|
|
* thing.yvel
|
|
*
|
|
* @param {Thing} thing
|
|
* @remarks thing.maxvel is used as the maximum absolute speed vertically
|
|
* @remarks thing.begin and thing.end are used as the vertical endpoints;
|
|
* .begin is the bottom and .end is the top (since begin <= end)
|
|
*/
|
|
moveFloating(thing: IThingFloating): void {
|
|
// If above the endpoint:
|
|
if (thing.top <= thing.end) {
|
|
thing.yvel = Math.min(thing.yvel + thing.FSM.unitsize / 64, thing.maxvel);
|
|
} else if (thing.bottom >= thing.begin) {
|
|
// If below the endpoint:
|
|
thing.yvel = Math.max(thing.yvel - thing.FSM.unitsize / 64, -thing.maxvel);
|
|
}
|
|
|
|
// Deal with velocities and whether the player is resting on this
|
|
thing.FSM.movePlatform(<any>thing);
|
|
}
|
|
|
|
/**
|
|
* Actual movement Function for Things that float sideways (horizontally).
|
|
* If the Thing has reached thing.begin or thing.end, it gradually switches
|
|
* thing.xvel.
|
|
*
|
|
* @param {Thing} thing
|
|
* @remarks thing.maxvel is used as the maximum absolute speed horizontally
|
|
* @remarks thing.begin and thing.end are used as the horizontal endpoints;
|
|
* .begin is the left and .end is the right (since begin <= end)
|
|
*/
|
|
moveSliding(thing: IThingSliding): void {
|
|
// If to the left of the endpoint:
|
|
if (thing.FSM.MapScreener.left + thing.left <= thing.begin) {
|
|
thing.xvel = Math.min(
|
|
thing.xvel + thing.FSM.unitsize / 64, thing.maxvel);
|
|
} else if (thing.FSM.MapScreener.left + thing.right > thing.end) {
|
|
// If to the right of the endpoint:
|
|
thing.xvel = Math.max(
|
|
thing.xvel - thing.FSM.unitsize / 64, -thing.maxvel);
|
|
}
|
|
|
|
// Deal with velocities and whether the player is resting on this
|
|
thing.FSM.movePlatform(<any>thing);
|
|
}
|
|
|
|
/**
|
|
* Ensures thing.begin <= thing.end (so there won't be glitches pertaining
|
|
* to them in functions like moveFloating and moveSliding
|
|
*
|
|
* @param {Thing} thing
|
|
*/
|
|
setMovementEndpoints(thing: IThingFloating | IThingSliding): void {
|
|
if (thing.begin > thing.end) {
|
|
var temp: number = thing.begin;
|
|
thing.begin = thing.end;
|
|
thing.end = temp;
|
|
}
|
|
|
|
thing.begin *= thing.FSM.unitsize;
|
|
thing.end *= thing.FSM.unitsize;
|
|
}
|
|
|
|
/**
|
|
* General movement Function for Platforms. Moves a Platform by its
|
|
* velocities, and checks for whether a Thing is resting on it (if so,
|
|
* the Thing is accordingly).
|
|
*
|
|
* @param {Thing} thing
|
|
*/
|
|
movePlatform(thing: IPlatform): void {
|
|
thing.FSM.shiftHoriz(thing, thing.xvel);
|
|
thing.FSM.shiftVert(thing, thing.yvel);
|
|
|
|
// If the player is resting on this and this is alive, move the player
|
|
if (thing === thing.FSM.player.resting && thing.FSM.player.alive) {
|
|
thing.FSM.setBottom(thing.FSM.player, thing.top);
|
|
thing.FSM.shiftHoriz(thing.FSM.player, thing.xvel);
|
|
|
|
// If the player is too far to the right or left, stop that overlap
|
|
if (thing.FSM.player.right > thing.FSM.MapScreener.width) {
|
|
thing.FSM.setRight(
|
|
thing.FSM.player,
|
|
thing.FSM.MapScreener.width);
|
|
} else if (thing.FSM.player.left < 0) {
|
|
thing.FSM.setLeft(thing.FSM.player, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for platforms that are in a PlatformGenerator. They
|
|
* have the typical movePlatform applied to them, but if they reach the
|
|
* bottom or top of the screen, they are shifted to the opposite side.
|
|
*
|
|
* @param {Platform} thing
|
|
*/
|
|
movePlatformSpawn(thing: IPlatform): void {
|
|
if (thing.bottom < 0) {
|
|
thing.FSM.setTop(thing, thing.FSM.MapScreener.bottomPlatformMax);
|
|
} else if (
|
|
thing.top > thing.FSM.MapScreener.bottomPlatformMax
|
|
) {
|
|
thing.FSM.setBottom(thing, 0);
|
|
} else {
|
|
thing.FSM.movePlatform(thing);
|
|
return;
|
|
}
|
|
|
|
if (
|
|
thing.FSM.player
|
|
&& thing.FSM.player.resting === thing
|
|
) {
|
|
thing.FSM.player.resting = undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Platforms that fall whenever rested upon by a
|
|
* player. Being rested upon means the Platform falls; when it reaches a
|
|
* terminal velocity, it switches to moveFreeFalling forever.
|
|
*
|
|
* @param {Platform} thing
|
|
*/
|
|
moveFalling(thing: IPlatform): void {
|
|
// If the player isn't resting on this thing (any more?), ignore it
|
|
if (thing.FSM.player.resting !== thing) {
|
|
// Since the player might have been on this thing but isn't anymore,
|
|
// set the yvel to 0 just in case
|
|
thing.yvel = 0;
|
|
return;
|
|
}
|
|
|
|
// Since the player is on this thing, start falling more
|
|
thing.FSM.shiftVert(
|
|
thing, thing.yvel += thing.FSM.unitsize / 8
|
|
);
|
|
thing.FSM.setBottom(thing.FSM.player, thing.top);
|
|
|
|
// After a velocity threshold, start always falling
|
|
if (
|
|
thing.yvel >= (
|
|
thing.fallThresholdStart || thing.FSM.unitsize * 2.8
|
|
)
|
|
) {
|
|
thing.freefall = true;
|
|
thing.movement = thing.FSM.moveFreeFalling;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Platforms that have reached terminal velocity in
|
|
* moveFalling and are now destined to die. The Platform will continue to
|
|
* accelerate towards certain death until another velocity threshold,
|
|
* and then switches to movePlatform to remain at that rate.
|
|
*
|
|
* @param {Platform} thing
|
|
*/
|
|
moveFreeFalling(thing: IPlatform): void {
|
|
// Accelerate downwards, increasing the thing's y-velocity
|
|
thing.yvel += thing.acceleration || thing.FSM.unitsize / 16;
|
|
thing.FSM.shiftVert(thing, thing.yvel);
|
|
|
|
// After a velocity threshold, stop accelerating
|
|
if (thing.yvel >= (thing.fallThresholdEnd || thing.FSM.unitsize * 2)) {
|
|
thing.movement = thing.FSM.movePlatform;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Platforms that are a part of a scale. Nothing
|
|
* happens if a Platform isn't being rested and doesn't have a y-velocity.
|
|
* Being rested upon means the y-velocity increases, and not being rested
|
|
* means the y-velocity decreases: either moves the corresponding Platform
|
|
* "partner" in the other vertical direction. When the Platform is too far
|
|
* down (visually has no string left), they both fall.
|
|
*
|
|
* @param {Platform} thing
|
|
* @todo Implement this! See #146.
|
|
*/
|
|
movePlatformScale(thing: IPlatform): void {
|
|
// If the Player is resting on this, fall hard
|
|
if (thing.FSM.player.resting === thing) {
|
|
thing.yvel += thing.FSM.unitsize / 16;
|
|
} else if (thing.yvel > 0) {
|
|
// If this still has velocity from a player, stop or fall less
|
|
if (!thing.partners) {
|
|
thing.yvel = 0;
|
|
} else {
|
|
thing.yvel = Math.max(
|
|
thing.yvel - thing.FSM.unitsize / 16, 0
|
|
);
|
|
}
|
|
} else {
|
|
// Not being rested upon or having a yvel means nothing happens
|
|
return;
|
|
}
|
|
|
|
thing.tension += thing.yvel;
|
|
thing.FSM.shiftVert(thing, thing.yvel);
|
|
|
|
// The rest of the logic is for the platform's partner(s)
|
|
if (!thing.partners) {
|
|
return;
|
|
}
|
|
|
|
thing.partners.partnerPlatform.tension -= thing.yvel;
|
|
|
|
// If the partner has fallen off, everybody falls!
|
|
if (thing.partners.partnerPlatform.tension <= 0) {
|
|
thing.FSM.scoreOn(1000, thing);
|
|
thing.partners.partnerPlatform.yvel = thing.FSM.unitsize / 2;
|
|
thing.collide = thing.partners.partnerPlatform.collide = (
|
|
thing.FSM.collideCharacterSolid);
|
|
thing.movement = thing.partners.partnerPlatform.movement = (
|
|
thing.FSM.moveFreeFalling);
|
|
}
|
|
|
|
// The partner has yvel equal and opposite to this platform's
|
|
thing.FSM.shiftVert(
|
|
thing.partners.partnerPlatform,
|
|
-thing.yvel);
|
|
|
|
// This platform's string grows with its yvel
|
|
thing.FSM.setHeight(
|
|
thing.partners.ownString,
|
|
thing.partners.ownString.height + thing.yvel / thing.FSM.unitsize);
|
|
|
|
// The partner's string shrinks while this platform's string grows
|
|
thing.FSM.setHeight(
|
|
thing.partners.partnerString,
|
|
Math.max(
|
|
thing.partners.partnerString.height - (
|
|
thing.yvel / thing.FSM.unitsize
|
|
),
|
|
0
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Vines. They are constantly growing upward, until
|
|
* some trigger (generally from animateEmergeVine) sets movement to
|
|
* undefined. If there is an attached Thing, it is moved up at the same rate
|
|
* as the Vine.
|
|
*
|
|
* @param {Vine} thing
|
|
*/
|
|
moveVine(thing: IVine): void {
|
|
thing.FSM.increaseHeight(thing, thing.speed);
|
|
thing.FSM.updateSize(thing);
|
|
|
|
if (thing.attachedSolid) {
|
|
thing.FSM.setBottom(thing, thing.attachedSolid.top);
|
|
}
|
|
|
|
if (thing.attachedCharacter) {
|
|
thing.FSM.shiftVert(thing.attachedCharacter, -thing.speed);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Springboards that are "pushing up" during or after
|
|
* being hit by a player. The Springboard changes its height based on its
|
|
* tension. If the player is still on it, then the player is given extra
|
|
* vertical velocity and taken off.
|
|
*
|
|
* @param {Springboard} thing
|
|
*/
|
|
moveSpringboardUp(thing: ISpringboard): void {
|
|
var player: IPlayer = thing.FSM.player;
|
|
|
|
thing.FSM.reduceHeight(thing, -thing.tension, true);
|
|
thing.tension *= 2;
|
|
|
|
// If the spring height is past the normal, it's done moving
|
|
if (thing.height > thing.heightNormal) {
|
|
thing.FSM.reduceHeight(
|
|
thing,
|
|
(thing.height - thing.heightNormal) * thing.FSM.unitsize);
|
|
if (thing === player.spring) {
|
|
player.yvel = thing.FSM.MathDecider.compute("springboardYvelUp", thing);
|
|
player.resting = player.spring = undefined;
|
|
player.movement = thing.FSM.movePlayer;
|
|
}
|
|
thing.tension = 0;
|
|
thing.movement = undefined;
|
|
} else {
|
|
thing.FSM.setBottom(player, thing.top);
|
|
}
|
|
|
|
if (thing === player.spring) {
|
|
if (!thing.FSM.isThingTouchingThing(player, thing)) {
|
|
player.spring = undefined;
|
|
player.movement = FullScreenMario.prototype.movePlayer;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Shells. This actually does nothing for moving
|
|
* Shells (since they only interact unusually on collision). For Shells with
|
|
* no x-velocity, a counting variable is increased. Once it reaches 350, the
|
|
* shell is "peeking" visually; when it reaches 490, the Shell spawns back
|
|
* into its original spawner (typically Koopa or Beetle).
|
|
*
|
|
* @param {Shell} thing
|
|
*/
|
|
moveShell(thing: IShell): void {
|
|
if (thing.xvel !== 0) {
|
|
return;
|
|
}
|
|
|
|
thing.counting += 1;
|
|
|
|
if (thing.counting === 350) {
|
|
thing.peeking = 1;
|
|
thing.height += thing.FSM.unitsize / 8;
|
|
thing.FSM.addClass(thing, "peeking");
|
|
thing.FSM.updateSize(thing);
|
|
} else if (thing.counting === 455) {
|
|
thing.peeking = 2;
|
|
} else if (thing.counting === 490) {
|
|
thing.spawnSettings = {
|
|
"smart": thing.smart
|
|
};
|
|
thing.FSM.killSpawn(thing);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Piranhas. These constantly change their height
|
|
* except when they reach 0 or full height (alternating direction), at which
|
|
* point they switch to movePiranhaLatent to wait to move in the opposite
|
|
* direction.
|
|
*
|
|
* @param {Piranha} thing
|
|
*/
|
|
movePiranha(thing: IPiranha): void {
|
|
var bottom: number = thing.bottom,
|
|
height: number = thing.height + thing.direction,
|
|
atEnd: boolean = false;
|
|
|
|
if (thing.resting && !thing.FSM.isThingAlive(thing.resting)) {
|
|
bottom = thing.constructor.prototype.height * thing.FSM.unitsize + thing.top;
|
|
height = Infinity;
|
|
thing.resting = undefined;
|
|
}
|
|
|
|
if (height <= 0) {
|
|
height = thing.height = 0;
|
|
atEnd = true;
|
|
} else if (height >= thing.constructor.prototype.height) {
|
|
height = thing.height = thing.constructor.prototype.height;
|
|
atEnd = true;
|
|
}
|
|
|
|
thing.FSM.setHeight(thing, height);
|
|
thing.FSM.setBottom(thing, bottom);
|
|
|
|
// Canvas height should be manually reset, as PixelRendr will otherwise
|
|
// store the height as the initial small height from spawnPiranha...
|
|
thing.canvas.height = height * thing.FSM.unitsize;
|
|
thing.FSM.PixelDrawer.setThingSprite(thing);
|
|
|
|
if (atEnd) {
|
|
thing.counter = 0;
|
|
thing.movement = thing.FSM.movePiranhaLatent;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Piranhas that are not changing size. They wait
|
|
* until a counter reaches a point (and then, if their height is 0, for the
|
|
* player to go away) to switch back to movePiranha.
|
|
*
|
|
* @param {Piranha} thing
|
|
*/
|
|
movePiranhaLatent(thing: IPiranha): void {
|
|
var playerX: number = thing.FSM.getMidX(thing.FSM.player);
|
|
|
|
if (
|
|
thing.counter >= thing.countermax
|
|
&& (
|
|
thing.height > 0
|
|
|| playerX < thing.left - thing.FSM.unitsize * 8
|
|
|| playerX > thing.right + thing.FSM.unitsize * 8
|
|
)
|
|
) {
|
|
thing.movement = undefined;
|
|
thing.direction *= -1;
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.movement = thing.FSM.movePiranha;
|
|
},
|
|
7);
|
|
} else {
|
|
thing.counter += 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for the Bubbles that come out of a player's mouth
|
|
* underwater. They die when they reach a top threshold of unitsize * 16.
|
|
*
|
|
* @param {Bubble} thing
|
|
*/
|
|
moveBubble(thing: IThing): void {
|
|
if (
|
|
thing.top < (
|
|
thing.FSM.MapScreener.top
|
|
+ thing.FSM.unitsize * 16
|
|
)
|
|
) {
|
|
thing.FSM.killNormal(thing);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for typical CheepCheeps, which are underwater. They
|
|
* move according to their native velocities except that they cannot travel
|
|
* above the unitsize * 16 top threshold.
|
|
*
|
|
* @param {CheepCheep} thing
|
|
*/
|
|
moveCheepCheep(thing: IThing): void {
|
|
if (thing.top < thing.FSM.unitsize * 16) {
|
|
thing.FSM.setTop(thing, thing.FSM.unitsize * 16);
|
|
thing.yvel *= -1;
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for flying CheepCheeps, like in bridge areas. They
|
|
* lose a movement Function (and therefore just fall) at a unitsize * 28 top
|
|
* threshold.
|
|
*
|
|
* @param {CheepCheep} thing
|
|
*/
|
|
moveCheepCheepFlying(thing: IThing): void {
|
|
if (thing.top < thing.FSM.unitsize * 28) {
|
|
thing.movement = undefined;
|
|
thing.nofall = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Bloopers. These switch between "squeezing" (moving
|
|
* down) and moving up ("unsqueezing"). They always try to unsqueeze if the
|
|
* player is above them.
|
|
*
|
|
* @param {Blooper} thing
|
|
*/
|
|
moveBlooper(thing: IBlooper): void {
|
|
// If the player is dead, just drift aimlessly
|
|
if (!thing.FSM.isThingAlive(thing.FSM.player)) {
|
|
thing.xvel = thing.FSM.unitsize / -4;
|
|
thing.yvel = 0;
|
|
thing.movement = undefined;
|
|
return;
|
|
}
|
|
|
|
switch (thing.counter) {
|
|
case 56:
|
|
thing.squeeze = 1;
|
|
thing.counter += 1;
|
|
break;
|
|
case 63:
|
|
thing.FSM.moveBlooperSqueezing(thing);
|
|
break;
|
|
default:
|
|
thing.counter += 1;
|
|
if (thing.top < thing.FSM.unitsize * 18) {
|
|
thing.FSM.moveBlooperSqueezing(thing);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (thing.squeeze) {
|
|
thing.yvel = Math.max(thing.yvel + .021, .7); // going down
|
|
} else {
|
|
thing.yvel = Math.min(thing.yvel - .035, -.7); // going up
|
|
}
|
|
|
|
if (thing.top > thing.FSM.unitsize * 16) {
|
|
thing.FSM.shiftVert(thing, thing.yvel, true);
|
|
}
|
|
|
|
if (!thing.squeeze) {
|
|
if (
|
|
thing.FSM.player.left
|
|
> thing.right + thing.FSM.unitsize * 8
|
|
) {
|
|
// Go to the right
|
|
thing.xvel = Math.min(
|
|
thing.speed, thing.xvel + thing.FSM.unitsize / 32
|
|
);
|
|
} else if (
|
|
thing.FSM.player.right
|
|
< thing.left - thing.FSM.unitsize * 8
|
|
) {
|
|
// Go to the left
|
|
thing.xvel = Math.max(
|
|
-thing.speed,
|
|
thing.xvel - thing.FSM.unitsize / 32
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Additional movement Function for Bloopers that are "squeezing". Squeezing
|
|
* Bloopers travel downard at a gradual pace until they reach either the
|
|
* player's bottom or a threshold of unitsize * 90.
|
|
*
|
|
* @param {Blooper} thing
|
|
*/
|
|
moveBlooperSqueezing(thing: IBlooper): void {
|
|
if (thing.squeeze !== 2) {
|
|
thing.squeeze = 2;
|
|
thing.FSM.addClass(thing, "squeeze");
|
|
thing.FSM.setHeight(thing, 10, true, true);
|
|
}
|
|
|
|
if (thing.squeeze < 7) {
|
|
thing.xvel /= 1.4;
|
|
} else if (thing.squeeze === 7) {
|
|
thing.xvel = 0;
|
|
}
|
|
|
|
thing.squeeze += 1;
|
|
|
|
if (
|
|
thing.top > thing.FSM.player.bottom
|
|
|| thing.bottom > thing.FSM.unitsize * 91
|
|
) {
|
|
thing.FSM.animateBlooperUnsqueezing(thing);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Podoboos that is only used when they are falling.
|
|
* Podoboo animations trigger this when they reach a certain height, and
|
|
* use this to determine when they should stop accelerating downward, which
|
|
* is their starting location.
|
|
*
|
|
* @param {Podoboo} thing
|
|
*/
|
|
movePodobooFalling(thing: IPodoboo): void {
|
|
if (thing.top >= thing.starty) {
|
|
thing.yvel = 0;
|
|
thing.movement = undefined;
|
|
thing.FSM.unflipVert(thing);
|
|
thing.FSM.setTop(thing, thing.starty);
|
|
return;
|
|
}
|
|
|
|
if (thing.yvel >= thing.speed) {
|
|
thing.yvel = thing.speed;
|
|
return;
|
|
}
|
|
|
|
if (!thing.flipVert && thing.yvel > 0) {
|
|
thing.FSM.flipVert(thing);
|
|
}
|
|
|
|
thing.yvel += thing.acceleration;
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Lakitus that have finished their moveLakituInitial
|
|
* run. This is similar to movePacing in that it makes the Lakitu pace to
|
|
* left and right of the player, and moves with the player rather than the
|
|
* scrolling window.
|
|
*
|
|
* @param {Lakitu} thing
|
|
*/
|
|
moveLakitu(thing: ILakitu): void {
|
|
var player: IPlayer = thing.FSM.player;
|
|
|
|
// If the player is moving quickly to the right, move in front and stay there
|
|
if (
|
|
player.xvel > thing.FSM.unitsize / 8
|
|
&& player.left > thing.FSM.MapScreener.width / 2
|
|
) {
|
|
if (thing.left < player.right + thing.FSM.unitsize * 16) {
|
|
// slide to xloc
|
|
thing.FSM.slideToX(
|
|
thing,
|
|
player.right + player.xvel + thing.FSM.unitsize * 32,
|
|
player.maxspeed * 1.4
|
|
);
|
|
thing.counter = 0;
|
|
}
|
|
} else {
|
|
thing.counter += .007;
|
|
thing.FSM.slideToX(
|
|
thing,
|
|
player.left + player.xvel + Math.sin(Math.PI * thing.counter) * 117,
|
|
player.maxspeed * .7
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initial entry movement Function for Lakitus. They enter by sliding across
|
|
* the top of the screen until they reach the player, and then switch to
|
|
* their standard moveLakitu movement.
|
|
*
|
|
* @param {Lakitu} thing
|
|
*/
|
|
moveLakituInitial(thing: ILakitu): void {
|
|
if (thing.right < thing.FSM.player.left) {
|
|
thing.counter = 0;
|
|
thing.movement = thing.FSM.moveLakitu;
|
|
thing.movement(thing);
|
|
return;
|
|
}
|
|
|
|
thing.FSM.shiftHoriz(thing, -thing.FSM.unitsize);
|
|
}
|
|
|
|
/**
|
|
* Alternate movement Function for Lakitus. This is used when the player
|
|
* reaches the ending flagpole in a level and the Lakitu just flies to the
|
|
* left.
|
|
*
|
|
* @param {Lakitu} thing
|
|
*/
|
|
moveLakituFleeing(thing: ILakitu): void {
|
|
thing.FSM.shiftHoriz(thing, -thing.FSM.unitsize);
|
|
}
|
|
|
|
/**
|
|
* Movement Function for Coins that have been animated. They move based on
|
|
* their yvel, and if they have a parent, die when they go below the parent.
|
|
*
|
|
* @param {Coin} thing
|
|
* @param {Solid} [parent]
|
|
*/
|
|
moveCoinEmerge(thing: ICoin, parent?: ISolid): void {
|
|
thing.FSM.shiftVert(thing, thing.yvel);
|
|
|
|
if (parent && thing.bottom >= thing.blockparent.bottom) {
|
|
thing.FSM.killNormal(thing);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for the player. It reacts to almost all actions that
|
|
* to be done, but is horribly written so that is all the documentation you
|
|
* get here. Sorry! Sections are labeled on the inside.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
movePlayer(thing: IPlayer): void {
|
|
// Not jumping
|
|
if (!thing.keys.up) {
|
|
thing.keys.jump = false;
|
|
} else if (
|
|
// Jumping
|
|
thing.keys.jump
|
|
&& (thing.yvel <= 0 || thing.FSM.MapScreener.underwater)
|
|
) {
|
|
if (thing.FSM.MapScreener.underwater) {
|
|
thing.FSM.animatePlayerPaddling(thing);
|
|
thing.FSM.removeClass(thing, "running");
|
|
}
|
|
|
|
if (thing.resting) {
|
|
if (thing.resting.xvel) {
|
|
thing.xvel += thing.resting.xvel;
|
|
}
|
|
thing.resting = undefined;
|
|
} else {
|
|
// Jumping, not resting
|
|
if (!thing.jumping && !thing.FSM.MapScreener.underwater) {
|
|
thing.FSM.switchClass(thing, "running skidding", "jumping");
|
|
}
|
|
thing.jumping = true;
|
|
|
|
if (thing.power > 1 && thing.crouching) {
|
|
thing.FSM.removeClass(thing, "jumping");
|
|
}
|
|
}
|
|
if (!thing.FSM.MapScreener.underwater) {
|
|
thing.keys.jumplev += 1;
|
|
thing.FSM.MathDecider.compute("decreasePlayerJumpingYvel", thing);
|
|
}
|
|
}
|
|
|
|
// Crouching
|
|
if (thing.keys.crouch && !thing.crouching && thing.resting) {
|
|
if (thing.power > 1) {
|
|
thing.crouching = true;
|
|
thing.FSM.removeClass(thing, "running");
|
|
thing.FSM.addClass(thing, "crouching");
|
|
thing.FSM.setHeight(thing, 11, true, true);
|
|
thing.height = 11;
|
|
thing.tolyOld = thing.toly;
|
|
thing.toly = thing.FSM.unitsize * 4;
|
|
thing.FSM.updateBottom(thing, 0);
|
|
thing.FSM.updateSize(thing);
|
|
}
|
|
// Pipe movement
|
|
if (thing.resting.actionTop) {
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerActionTop", thing, thing.resting);
|
|
thing.resting.actionTop(thing, thing.resting);
|
|
}
|
|
}
|
|
|
|
// Running
|
|
if (thing.FSM.MathDecider.compute("decreasePlayerRunningXvel", thing)) {
|
|
if (thing.skidding) {
|
|
thing.FSM.addClass(thing, "skidding");
|
|
} else {
|
|
thing.FSM.removeClass(thing, "skidding");
|
|
}
|
|
}
|
|
|
|
// Movement mods
|
|
// Slowing down
|
|
if (Math.abs(thing.xvel) < .14) {
|
|
if (thing.running) {
|
|
thing.running = false;
|
|
if (thing.power === 1) {
|
|
thing.FSM.setPlayerSizeSmall(thing);
|
|
}
|
|
thing.FSM.removeClasses(thing, "running skidding one two three");
|
|
thing.FSM.addClass(thing, "still");
|
|
thing.FSM.TimeHandler.cancelClassCycle(thing, "running");
|
|
}
|
|
} else if (!thing.running) {
|
|
// Not moving slowly
|
|
thing.running = true;
|
|
thing.FSM.animatePlayerRunningCycle(thing);
|
|
if (thing.power === 1) {
|
|
thing.FSM.setPlayerSizeSmall(thing);
|
|
}
|
|
}
|
|
if (thing.xvel > 0) {
|
|
thing.xvel = Math.min(thing.xvel, thing.maxspeed);
|
|
if (thing.moveleft && (thing.resting || thing.FSM.MapScreener.underwater)) {
|
|
thing.FSM.unflipHoriz(thing);
|
|
thing.moveleft = false;
|
|
}
|
|
} else if (thing.xvel < 0) {
|
|
thing.xvel = Math.max(thing.xvel, thing.maxspeed * -1);
|
|
if (!thing.moveleft && (thing.resting || thing.FSM.MapScreener.underwater)) {
|
|
thing.moveleft = true;
|
|
thing.FSM.flipHoriz(thing);
|
|
}
|
|
}
|
|
|
|
// Resting stops a bunch of other stuff
|
|
if (thing.resting) {
|
|
// Hopping
|
|
if (thing.hopping) {
|
|
thing.hopping = false;
|
|
thing.FSM.removeClass(thing, "hopping");
|
|
if (thing.xvel) {
|
|
thing.FSM.addClass(thing, "running");
|
|
}
|
|
}
|
|
// Jumping
|
|
thing.keys.jumplev = thing.yvel = thing.jumpcount = 0;
|
|
if (thing.jumping) {
|
|
thing.jumping = false;
|
|
thing.FSM.removeClass(thing, "jumping");
|
|
if (thing.power === 1) {
|
|
thing.FSM.setPlayerSizeSmall(thing);
|
|
}
|
|
thing.FSM.addClass(thing, Math.abs(thing.xvel) < .14 ? "still" : "running");
|
|
}
|
|
// Paddling
|
|
if (thing.paddling) {
|
|
thing.paddling = thing.swimming = false;
|
|
thing.FSM.TimeHandler.cancelClassCycle(thing, "paddling");
|
|
thing.FSM.removeClasses(thing, "paddling swim1 swim2");
|
|
thing.FSM.addClass(thing, "running");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Alternate movement Function for players attached to a Vine. They may
|
|
* climb up or down the Vine, or jump off.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
movePlayerVine(thing: IPlayer): void {
|
|
var attachedSolid: ISolid = thing.attachedSolid,
|
|
animatedClimbing: boolean;
|
|
|
|
if (!attachedSolid) {
|
|
thing.movement = thing.FSM.movePlayer;
|
|
return;
|
|
}
|
|
|
|
if (thing.bottom < thing.attachedSolid.top) {
|
|
thing.FSM.unattachPlayer(thing, thing.attachedSolid);
|
|
return;
|
|
}
|
|
|
|
// Running away from the vine means dropping off
|
|
if (thing.keys.run !== 0 && thing.keys.run === thing.attachedDirection) {
|
|
// Leaving to the left
|
|
if (thing.attachedDirection === -1) {
|
|
thing.FSM.setRight(thing, attachedSolid.left - thing.FSM.unitsize);
|
|
} else if (thing.attachedDirection === 1) {
|
|
// Leaving to the right
|
|
thing.FSM.setLeft(thing, attachedSolid.right + thing.FSM.unitsize);
|
|
}
|
|
|
|
thing.FSM.unattachPlayer(thing, attachedSolid);
|
|
return;
|
|
}
|
|
|
|
// If the player is moving up, simply move up
|
|
if (thing.keys.up) {
|
|
animatedClimbing = true;
|
|
thing.FSM.shiftVert(thing, thing.FSM.unitsize / -4);
|
|
} else if (thing.keys.crouch) {
|
|
// If the thing is moving down, move down and check for unattachment
|
|
animatedClimbing = true;
|
|
thing.FSM.shiftVert(thing, thing.FSM.unitsize / 2);
|
|
if (thing.top > attachedSolid.bottom) {
|
|
thing.FSM.unattachPlayer(thing, thing.attachedSolid);
|
|
}
|
|
return;
|
|
} else {
|
|
animatedClimbing = false;
|
|
}
|
|
|
|
if (animatedClimbing && !thing.animatedClimbing) {
|
|
thing.FSM.addClass(thing, "animated");
|
|
} else if (!animatedClimbing && thing.animatedClimbing) {
|
|
thing.FSM.removeClass(thing, "animated");
|
|
}
|
|
|
|
thing.animatedClimbing = animatedClimbing;
|
|
|
|
if (thing.bottom < thing.FSM.MapScreener.top - thing.FSM.unitsize * 4) {
|
|
thing.FSM.setLocation(thing.attachedSolid.transport);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Movement Function for players pressing down onto a Springboard. This does
|
|
* basically nothing except check for when the player is off the spring or
|
|
* the spring is fully contracted. The former restores the player's movement
|
|
* and the latter clears it (to be restored in moveSpringboardUp).
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
movePlayerSpringboardDown(thing: IPlayer): void {
|
|
var other: ISpringboard = thing.spring;
|
|
|
|
// If the player has moved off the spring, get outta here
|
|
if (!thing.FSM.isThingTouchingThing(thing, other)) {
|
|
thing.movement = thing.FSM.movePlayer;
|
|
other.movement = thing.FSM.moveSpringboardUp;
|
|
thing.spring = undefined;
|
|
return;
|
|
}
|
|
|
|
// If the spring is fully contracted, go back up
|
|
if (
|
|
other.height < thing.FSM.unitsize * 2.5
|
|
|| other.tension < thing.FSM.unitsize / 32
|
|
) {
|
|
thing.movement = undefined;
|
|
other.movement = thing.FSM.moveSpringboardUp;
|
|
return;
|
|
}
|
|
|
|
// Make sure it's hard to slide off
|
|
if (
|
|
thing.left < other.left + thing.FSM.unitsize * 2
|
|
|| thing.right > other.right - thing.FSM.unitsize * 2
|
|
) {
|
|
thing.xvel /= 1.4;
|
|
}
|
|
|
|
thing.FSM.reduceHeight(other, other.tension, true);
|
|
other.tension /= 2;
|
|
thing.FSM.setBottom(thing, other.top);
|
|
thing.FSM.updateSize(other);
|
|
}
|
|
|
|
|
|
/* Animations
|
|
*/
|
|
|
|
/**
|
|
* Animates a solid that has just had its bottom "bumped" by a player. It
|
|
* moves with a dx that is initially negative (up) and increases (to down).
|
|
*
|
|
* @param {Solid} thing
|
|
*/
|
|
animateSolidBump(thing: ISolid): void {
|
|
var dx: number = -3;
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
function (thing: ISolid): boolean {
|
|
thing.FSM.shiftVert(thing, dx);
|
|
dx += .5;
|
|
if (dx === 3.5) {
|
|
thing.up = undefined;
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
1,
|
|
Infinity,
|
|
thing);
|
|
}
|
|
|
|
/**
|
|
* Animates a Block to switch from unused to used.
|
|
*
|
|
* @param {Block} thing
|
|
*/
|
|
animateBlockBecomesUsed(thing: IBlock): void {
|
|
thing.used = true;
|
|
thing.FSM.switchClass(thing, "unused", "used");
|
|
}
|
|
|
|
/**
|
|
* Animates a solid to have its contents emerge. A new Thing based on the
|
|
* contents is spawned directly on top of (visually behind) the solid, and
|
|
* has its animate callback triggered.
|
|
*
|
|
* @param {Solid} thing
|
|
* @param {Player} other
|
|
* @remarks If the contents are "Mushroom" and a large player hits the
|
|
* solid, they turn into "FireFlower".
|
|
* @remarks For the level editor, if thing.contents is false, the prototype
|
|
* is tried (so false becomes Coin in Blocks).
|
|
*/
|
|
animateSolidContents(thing: IBrick | IBlock, other: IPlayer): ICharacter {
|
|
var output: ICharacter;
|
|
|
|
if (
|
|
other
|
|
&& other.player
|
|
&& other.power > 1
|
|
&& thing.contents === "Mushroom"
|
|
) {
|
|
thing.contents = "FireFlower";
|
|
}
|
|
|
|
output = <ICharacter>thing.FSM.addThing(
|
|
thing.contents || thing.constructor.prototype.contents);
|
|
thing.FSM.setMidXObj(output, thing);
|
|
thing.FSM.setTop(output, thing.top);
|
|
output.blockparent = thing;
|
|
output.animate(output, thing);
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Animates a Brick turning into four rotating shards flying out of it. The
|
|
* shards have an initial x- and y-velocities, and die after 70 steps.
|
|
*
|
|
* @param {Brick} thing
|
|
*/
|
|
animateBrickShards(thing: IBrick): void {
|
|
var unitsize: number = thing.FSM.unitsize,
|
|
shard: IBrickShard,
|
|
left: number,
|
|
top: number,
|
|
i: number;
|
|
|
|
for (i = 0; i < 4; i += 1) {
|
|
left = thing.left + Number(i < 2) * thing.width * unitsize - unitsize * 2;
|
|
top = thing.top + (i % 2) * thing.height * unitsize - unitsize * 2;
|
|
|
|
shard = <IBrickShard>thing.FSM.addThing("BrickShard", left, top);
|
|
shard.xvel = shard.speed = unitsize / 2 - unitsize * Number(i > 1);
|
|
shard.yvel = unitsize * -1.4 + i % 2;
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.killNormal, 70, shard);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Standard animation Function for Things emerging from a solid as contents.
|
|
* They start at inside the solid, slowly move up, then moveSimple until
|
|
* they're off the solid, at which point they revert to their normal
|
|
* movement.
|
|
*
|
|
* @param {Character} thing
|
|
* @param {Solid} other
|
|
*/
|
|
animateEmerge(thing: ICharacter, other: ISolid): void {
|
|
thing.nomove = thing.nocollide = thing.nofall = thing.alive = true;
|
|
|
|
thing.FSM.flipHoriz(thing);
|
|
thing.FSM.AudioPlayer.play("Powerup Appears");
|
|
thing.FSM.arraySwitch(
|
|
thing,
|
|
<IThing[]>thing.FSM.GroupHolder.getGroup("Character"),
|
|
<IThing[]>thing.FSM.GroupHolder.getGroup("Scenery"));
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
thing.FSM.shiftVert(thing, thing.FSM.unitsize / -8);
|
|
|
|
// Only stop once the bottom has reached the solid's top
|
|
if (thing.bottom > other.top) {
|
|
return false;
|
|
}
|
|
|
|
thing.FSM.setBottom(thing, other.top);
|
|
thing.FSM.GroupHolder.switchMemberGroup(thing, "Scenery", "Character");
|
|
thing.nomove = thing.nocollide = thing.nofall = thing.moveleft = false;
|
|
|
|
if (thing.emergeOut) {
|
|
thing.emergeOut(thing, other);
|
|
}
|
|
|
|
// Wait for movement until moveSimple moves this off the solid
|
|
if (thing.movement) {
|
|
thing.movementOld = thing.movement;
|
|
thing.movement = thing.FSM.moveSimple;
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
if (thing.resting === other) {
|
|
return false;
|
|
}
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.movement = thing.movementOld;
|
|
},
|
|
1);
|
|
|
|
return true;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
return true;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for Coins emerging from (or being hit by) a solid. The
|
|
* Coin switches to the Scenery group, rotates between animation classes,
|
|
* moves up then down then dies, plays the "Coin" sound, and increaes the
|
|
* "coins" and "score" statistics.
|
|
*
|
|
* @param {Coin} thing
|
|
* @param {Solid} other
|
|
*/
|
|
animateEmergeCoin(thing: ICoin, other: ISolid): void {
|
|
thing.nocollide = thing.alive = thing.nofall = true;
|
|
thing.yvel -= thing.FSM.unitsize;
|
|
|
|
thing.FSM.switchClass(thing, "still", "anim");
|
|
thing.FSM.GroupHolder.switchMemberGroup(thing, "Character", "Scenery");
|
|
|
|
thing.FSM.AudioPlayer.play("Coin");
|
|
thing.FSM.ItemsHolder.increase("coins", 1);
|
|
thing.FSM.ItemsHolder.increase("score", 200);
|
|
|
|
thing.FSM.TimeHandler.cancelClassCycle(thing, "0");
|
|
thing.FSM.TimeHandler.addClassCycle(
|
|
thing,
|
|
[
|
|
"anim1", "anim2", "anim3", "anim4", "anim3", "anim2"
|
|
],
|
|
"0",
|
|
5);
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
thing.FSM.moveCoinEmerge(thing, other);
|
|
return !thing.FSM.isThingAlive(thing);
|
|
},
|
|
1,
|
|
Infinity);
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.FSM.killNormal(thing);
|
|
},
|
|
49);
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.yvel *= -1;
|
|
},
|
|
25);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for a Vine emerging from a solid. It continues to grow
|
|
* as normal via moveVine for 700 steps, then has its movement erased to
|
|
* stop.
|
|
*
|
|
* @param {Vine} thing
|
|
* @param {Solid} solid
|
|
*/
|
|
animateEmergeVine(thing: IVine, solid: ISolid): void {
|
|
// This allows the thing's movement to keep it on the solid
|
|
thing.attachedSolid = solid;
|
|
|
|
thing.FSM.setHeight(thing, 0);
|
|
thing.FSM.AudioPlayer.play("Vine Emerging");
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.nocollide = false;
|
|
},
|
|
14);
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.movement = undefined;
|
|
},
|
|
700);
|
|
}
|
|
|
|
/**
|
|
* Animates a "flicker" effect on a Thing by repeatedly toggling its hidden
|
|
* flag for a little while.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Number} [cleartime] How long to wait to stop the effect (by
|
|
* default, 49).
|
|
* @param {Number} [interval] How many steps between hidden toggles (by
|
|
* default, 2).
|
|
*/
|
|
animateFlicker(thing: IThing, cleartime?: number, interval?: number): void {
|
|
cleartime = Math.round(cleartime) || 49;
|
|
interval = Math.round(interval) || 2;
|
|
|
|
thing.flickering = true;
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
function (): void {
|
|
thing.hidden = !thing.hidden;
|
|
thing.FSM.PixelDrawer.setThingSprite(thing);
|
|
},
|
|
interval,
|
|
cleartime);
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.flickering = thing.hidden = false;
|
|
thing.FSM.PixelDrawer.setThingSprite(thing);
|
|
},
|
|
cleartime * interval + 1);
|
|
}
|
|
|
|
/**
|
|
* Animate Function for a HammerBro to throw a hammer. The HammerBro
|
|
* switches to the "throwing" class, waits and throws a few repeats, then
|
|
* goes back to normal.
|
|
*
|
|
* @param {HammerBro} thing
|
|
* @param {Number} count How many times left there are to throw a hammer.
|
|
* If equal to 3, a hammer will not be thrown (to
|
|
* mimic the pause in the original game).
|
|
* @remarks This could probably be re-written.
|
|
*/
|
|
animateThrowingHammer(thing: IHammerBro, count: number): boolean {
|
|
if (!thing.FSM.isThingAlive(thing)) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
thing.FSM.isThingAlive(thing.FSM.player)
|
|
&& thing.right >= thing.FSM.unitsize * -32
|
|
&& count !== 3
|
|
) {
|
|
thing.FSM.switchClass(thing, "thrown", "throwing");
|
|
}
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
if (!thing.FSM.isThingAlive(thing)) {
|
|
return;
|
|
}
|
|
|
|
// Schedule the next animateThrowingHammer call
|
|
if (count > 0) {
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.animateThrowingHammer,
|
|
7, thing, count - 1
|
|
);
|
|
} else {
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.animateThrowingHammer,
|
|
70, thing, 7
|
|
);
|
|
thing.FSM.removeClass(thing, "thrown");
|
|
}
|
|
|
|
// Don't throw if the player is dead, or this is too far to the left
|
|
if (!thing.FSM.isThingAlive(thing.FSM.player) || thing.right < thing.FSM.unitsize * -32) {
|
|
thing.FSM.switchClass(thing, "throwing", "thrown");
|
|
return;
|
|
}
|
|
|
|
// Don't throw in the third iteration (makes a gap in the hammers)
|
|
if (count === 3) {
|
|
return;
|
|
}
|
|
|
|
// Throw by creating a hammer and visually updating
|
|
thing.FSM.switchClass(thing, "throwing", "thrown");
|
|
thing.FSM.addThing(
|
|
["Hammer", {
|
|
"xvel": thing.lookleft
|
|
? thing.FSM.unitsize / -1.4
|
|
: thing.FSM.unitsize / 1.4,
|
|
"yvel": thing.FSM.unitsize * -1.4,
|
|
"gravity": thing.FSM.MapScreener.gravity / 2.1
|
|
}],
|
|
thing.left - thing.FSM.unitsize * 2,
|
|
thing.top - thing.FSM.unitsize * 2
|
|
);
|
|
},
|
|
14);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when Bowser jumps. This will only trigger if he is
|
|
* facing left and a player exists. If either Bowser or the player die, it
|
|
* is cancelled. He is given a negative yvel to jump, and the nocollidesolid
|
|
* flag is enabled as long as he is rising.
|
|
*
|
|
* @param {Bowser} thing
|
|
*/
|
|
animateBowserJump(thing: IBowser): boolean {
|
|
if (!thing.lookleft || !thing.FSM.player || !thing.FSM.isThingAlive(thing.FSM.player)) {
|
|
return false;
|
|
}
|
|
|
|
if (!thing.FSM.isThingAlive(thing)) {
|
|
return true;
|
|
}
|
|
|
|
thing.resting = undefined;
|
|
thing.yvel = thing.FSM.unitsize * -1.4;
|
|
|
|
// If there is a platform, don't bump into it
|
|
thing.nocollidesolid = true;
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
if (thing.dead || thing.yvel > thing.FSM.unitsize) {
|
|
thing.nocollidesolid = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
3,
|
|
Infinity);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when Bowser fires. This will only trigger if he is
|
|
* facing left and a player exists. If either Bowser or the player die, it
|
|
* is cancelled. His mouth is closed and an animateBowserFireOpen call is
|
|
* scheduled to complete the animation.
|
|
*
|
|
* @param {Bowser} thing
|
|
*/
|
|
animateBowserFire(thing: IBowser): boolean {
|
|
if (!thing.lookleft || !thing.FSM.player || !thing.FSM.isThingAlive(thing.FSM.player)) {
|
|
return false;
|
|
}
|
|
|
|
if (!thing.FSM.isThingAlive(thing)) {
|
|
return true;
|
|
}
|
|
|
|
// Close the mouth
|
|
thing.FSM.addClass(thing, "firing");
|
|
thing.FSM.AudioPlayer.playLocal("Bowser Fires", thing.left);
|
|
|
|
// After a bit, re-open and fire
|
|
thing.FSM.TimeHandler.addEvent(thing.FSM.animateBowserFireOpen, 14, thing);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when Bowser actually fires. A BowserFire Thing is
|
|
* placed at his mouth, given a (rounded to unitsize * 8) destination y, and
|
|
* sent firing to the player.
|
|
*
|
|
* @param {Bowser} thing
|
|
*/
|
|
animateBowserFireOpen(thing: IBowser): boolean {
|
|
var unitsize: number = thing.FSM.unitsize,
|
|
ylev: number = Math.max(
|
|
-thing.height * unitsize,
|
|
Math.round(thing.FSM.player.bottom / (unitsize * 8))
|
|
* unitsize * 8);
|
|
|
|
if (!thing.FSM.isThingAlive(thing)) {
|
|
return true;
|
|
}
|
|
|
|
thing.FSM.removeClass(thing, "firing");
|
|
thing.FSM.addThing(
|
|
["BowserFire", {
|
|
"ylev": ylev
|
|
}],
|
|
thing.left - thing.FSM.unitsize * 8,
|
|
thing.top + thing.FSM.unitsize * 4
|
|
);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when Bowser throws a Hammer. It's similar to a
|
|
* HammerBro, but the hammer appears on top of Bowser for a few steps
|
|
* before being thrown in the direction Bowser is facing (though it will
|
|
* only be added if facing left).
|
|
*
|
|
* @param {Bowser} thing
|
|
*/
|
|
animateBowserThrow(thing: IBowser): boolean {
|
|
if (!thing.lookleft || !thing.FSM.player || !thing.FSM.isThingAlive(thing.FSM.player)) {
|
|
return false;
|
|
}
|
|
|
|
if (!thing.FSM.isThingAlive(thing)) {
|
|
return true;
|
|
}
|
|
|
|
var hammer: IThing = <IThing>thing.FSM.addThing(
|
|
"Hammer",
|
|
thing.left + thing.FSM.unitsize * 2,
|
|
thing.top - thing.FSM.unitsize * 2);
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
if (!thing.FSM.isThingAlive(thing)) {
|
|
thing.FSM.killNormal(hammer);
|
|
return true;
|
|
}
|
|
|
|
thing.FSM.setTop(
|
|
hammer, thing.top - thing.FSM.unitsize * 2
|
|
);
|
|
if (thing.lookleft) {
|
|
thing.FSM.setLeft(
|
|
hammer,
|
|
thing.left + thing.FSM.unitsize * 2
|
|
);
|
|
} else {
|
|
thing.FSM.setLeft(
|
|
hammer,
|
|
thing.right - thing.FSM.unitsize * 2
|
|
);
|
|
}
|
|
|
|
return true;
|
|
},
|
|
1,
|
|
14);
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
hammer.xvel = thing.FSM.unitsize * 1.17;
|
|
hammer.yvel = thing.FSM.unitsize * -2.1;
|
|
// hammer.gravity = thing.FSM.MapScreener.gravity / 1.4;
|
|
if (thing.lookleft) {
|
|
hammer.xvel *= -1;
|
|
}
|
|
},
|
|
14);
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when Bowser freezes upon the player hitting a
|
|
* CastleAxe. Velocity and movement are paused, and the Bowser is added to
|
|
* the current cutscene's settings.
|
|
*
|
|
* @param {Bowser} thing
|
|
* @remarks This is triggered as Bowser's killonend property.
|
|
*/
|
|
animateBowserFreeze(thing: IBowser): void {
|
|
thing.nofall = true;
|
|
thing.nothrow = true;
|
|
thing.movement = undefined;
|
|
thing.dead = true;
|
|
thing.FSM.thingPauseVelocity(thing);
|
|
|
|
thing.FSM.ScenePlayer.addCutsceneSetting("bowser", thing);
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.nofall = false;
|
|
},
|
|
70);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for a standard jump, such as what HammerBros do. The
|
|
* jump may be in either up or down, chosen at random by the NumberMaker.
|
|
* Steps are taken to ensure the Thing does not collide at improper points
|
|
* during the jump.
|
|
*
|
|
* @param {Thing} thing
|
|
*/
|
|
animateJump(thing: IHammerBro): boolean {
|
|
// Finish
|
|
if (!thing.FSM.isThingAlive(thing) || !thing.FSM.isThingAlive(thing.FSM.player)) {
|
|
return true;
|
|
}
|
|
|
|
// Skip
|
|
if (!thing.resting) {
|
|
return false;
|
|
}
|
|
|
|
// Jump up?
|
|
if (
|
|
thing.FSM.MapScreener.floor - (
|
|
thing.bottom / thing.FSM.unitsize
|
|
) >= 30
|
|
&& thing.resting.title !== "Floor"
|
|
&& thing.FSM.NumberMaker.randomBoolean()
|
|
) {
|
|
thing.falling = true;
|
|
thing.yvel = thing.FSM.unitsize * -.7;
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.falling = false;
|
|
},
|
|
42);
|
|
} else {
|
|
// Jump down
|
|
thing.nocollidesolid = true;
|
|
thing.yvel = thing.FSM.unitsize * -2.1;
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.nocollidesolid = false;
|
|
},
|
|
42);
|
|
}
|
|
|
|
thing.resting = undefined;
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Animation Function for Bloopers starting to "unsqueeze". The "squeeze"
|
|
* class is removed, their height is reset to 12, and their counter reset.
|
|
*
|
|
* @param {Blooper} thing
|
|
*/
|
|
animateBlooperUnsqueezing(thing: IBlooper): void {
|
|
thing.counter = 0;
|
|
thing.squeeze = 0;
|
|
|
|
thing.FSM.removeClass(thing, "squeeze");
|
|
thing.FSM.setHeight(thing, 12, true, true);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for Podoboos jumping up. Their top is recorded and a
|
|
* large negative yvel is given; after the jumpheight number of steps, they
|
|
* fall back down.
|
|
*
|
|
* @param {Podoboo} thing
|
|
*/
|
|
animatePodobooJumpUp(thing: IPodoboo): void {
|
|
thing.starty = thing.top;
|
|
thing.yvel = thing.speed * -1;
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.animatePodobooJumpDown,
|
|
thing.jumpHeight,
|
|
thing);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when a Podoboo needs to stop jumping. It obtains
|
|
* the movePodobooFalling movement to track its descent.
|
|
*
|
|
* @param {Podoboo} thing
|
|
*/
|
|
animatePodobooJumpDown(thing: IPodoboo): void {
|
|
thing.movement = thing.FSM.movePodobooFalling;
|
|
}
|
|
|
|
/**
|
|
* Animation Function for a Lakitu throwing a SpinyEgg. The Lakitu hides
|
|
* behind its cloud ("hiding" class), waits 21 steps, then throws an egg up
|
|
* and comes out of "hiding".
|
|
*
|
|
* @param {Lakitu} thing
|
|
*/
|
|
animateLakituThrowingSpiny(thing: ILakitu): boolean {
|
|
if (thing.fleeing || !thing.FSM.isThingAlive(thing)) {
|
|
return true;
|
|
}
|
|
|
|
thing.FSM.switchClass(thing, "out", "hiding");
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
if (thing.dead) {
|
|
return;
|
|
}
|
|
|
|
var spawn: ISpinyEgg = <ISpinyEgg>thing.FSM.addThing("SpinyEgg", thing.left, thing.top);
|
|
spawn.yvel = thing.FSM.unitsize * -2.1;
|
|
thing.FSM.switchClass(thing, "hiding", "out");
|
|
},
|
|
21);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when a SpinyEgg hits the ground. The SpinyEgg is
|
|
* killed and a Spiny is put in its place, moving towards the player.
|
|
*
|
|
* @param {SpinyEgg} thing
|
|
*/
|
|
animateSpinyEggHatching(thing: ISpinyEgg): void {
|
|
if (!thing.FSM.isThingAlive(thing)) {
|
|
return;
|
|
}
|
|
|
|
var spawn: ISpiny = <ISpiny>thing.FSM.addThing("Spiny", thing.left, thing.top - thing.yvel);
|
|
spawn.moveleft = thing.FSM.objectToLeft(thing.FSM.player, spawn);
|
|
thing.FSM.killNormal(thing);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when a Fireball emerges from a player. All that
|
|
* happens is the "Fireball" sound plays.
|
|
*
|
|
* @param {Fireball} thing
|
|
*/
|
|
animateFireballEmerge(thing: IThing): void {
|
|
thing.FSM.AudioPlayer.play("Fireball");
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when a Fireball explodes. It is deleted and,
|
|
* unless big is === 2 (as this is used as a kill Function), a Firework is
|
|
* put in its place.
|
|
*
|
|
* @param {Fireball} thing
|
|
* @param {Number} [big] The "level" of death this is (a 2 implies this is
|
|
* a sudden death, without animations).
|
|
*/
|
|
animateFireballExplode(thing: IFireball, big?: number): void {
|
|
thing.nocollide = true;
|
|
thing.FSM.killNormal(thing);
|
|
if (big === 2) {
|
|
return;
|
|
}
|
|
|
|
var output: IFirework = <IFirework>thing.FSM.addThing("Firework");
|
|
thing.FSM.setMidXObj(output, thing);
|
|
thing.FSM.setMidYObj(output, thing);
|
|
output.animate(output);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for a Firework, triggered immediately upon spawning.
|
|
* The Firework cycles between "n1" through "n3", then dies.
|
|
*
|
|
* @param {Firework} thing
|
|
*/
|
|
animateFirework(thing: IFirework): void {
|
|
var name: string = thing.className + " n",
|
|
i: number;
|
|
|
|
for (i = 0; i < 3; i += 1) {
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (i: number): void {
|
|
thing.FSM.setClass(thing, name + String(i + 1));
|
|
},
|
|
i * 7,
|
|
i);
|
|
}
|
|
|
|
thing.FSM.AudioPlayer.play("Firework");
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.FSM.killNormal(thing);
|
|
},
|
|
i * 7);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for a Cannon outputting a BulletBill. This will only
|
|
* happen if the Cannon isn't within 8 units of the player. The spawn flies
|
|
* at a constant rate towards the player.
|
|
*
|
|
* @param {Cannon} thing
|
|
*/
|
|
animateCannonFiring(thing: ICannon): void {
|
|
if (!thing.FSM.isThingAlive(thing)) {
|
|
return;
|
|
}
|
|
|
|
// Don't fire if Player is too close
|
|
if (
|
|
thing.FSM.player.right > (
|
|
thing.left - thing.FSM.unitsize * 8
|
|
)
|
|
&& thing.FSM.player.left < (
|
|
thing.right + thing.FSM.unitsize * 8
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
var spawn: IBulletBill = thing.FSM.ObjectMaker.make("BulletBill");
|
|
|
|
if (thing.FSM.objectToLeft(thing.FSM.player, thing)) {
|
|
spawn.direction = spawn.moveleft = true;
|
|
spawn.xvel *= -1;
|
|
thing.FSM.flipHoriz(spawn);
|
|
thing.FSM.addThing(spawn, thing.left, thing.top);
|
|
} else {
|
|
thing.FSM.addThing(spawn, thing.left + thing.width, thing.top);
|
|
}
|
|
|
|
thing.FSM.AudioPlayer.playLocal("Bump", thing.right);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for a fiery player throwing a Fireball. The player may
|
|
* only do so if fewer than 2 other thrown Fireballs exist. A new Fireball
|
|
* is created in front of where the player is facing and are sent bouncing
|
|
* away.
|
|
*
|
|
* @param {Player} thing
|
|
* @remarks Yes, it's called numballs.
|
|
*/
|
|
animatePlayerFire(thing: IPlayer): void {
|
|
if (thing.numballs >= 2) {
|
|
return;
|
|
}
|
|
|
|
var xloc: number = thing.moveleft
|
|
? (thing.left - thing.FSM.unitsize / 4)
|
|
: (thing.right + thing.FSM.unitsize / 4),
|
|
ball: IFireball = thing.FSM.ObjectMaker.make("Fireball", {
|
|
"moveleft": thing.moveleft,
|
|
"speed": thing.FSM.unitsize * 1.75,
|
|
"jumpheight": thing.FSM.unitsize * 1.56,
|
|
"gravity": thing.FSM.MapScreener.gravity * 1.56,
|
|
"yvel": thing.FSM.unitsize,
|
|
"movement": thing.FSM.moveJumping
|
|
});
|
|
|
|
thing.FSM.addThing(ball, xloc, thing.top + thing.FSM.unitsize * 8);
|
|
ball.animate(ball);
|
|
ball.onDelete = function (): void {
|
|
thing.numballs -= 1;
|
|
};
|
|
|
|
thing.numballs += 1;
|
|
thing.FSM.addClass(thing, "firing");
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.FSM.removeClass(thing, "firing");
|
|
},
|
|
7);
|
|
}
|
|
|
|
/**
|
|
* Animation Function that regularly spings CastleFireballs around their
|
|
* parent CastleBlock. The CastleBlock's location and angle determine the
|
|
* location of each CastleFireball, and its dt and direction determine how
|
|
* the angle is changed for the next call.
|
|
*
|
|
* @param {CastleBlock} thing
|
|
* @param {CastleFireball[]} balls
|
|
*/
|
|
animateCastleBlock(thing: ICastleBlock, balls: ICastleFireball[]): void {
|
|
var midx: number = thing.EightBitter.getMidX(thing),
|
|
midy: number = thing.EightBitter.getMidY(thing),
|
|
ax: number = Math.cos(thing.angle * Math.PI) * thing.FSM.unitsize * 4,
|
|
ay: number = Math.sin(thing.angle * Math.PI) * thing.FSM.unitsize * 4,
|
|
i: number;
|
|
|
|
for (i = 0; i < balls.length; i += 1) {
|
|
thing.FSM.setMidX(balls[i], midx + ax * i);
|
|
thing.FSM.setMidY(balls[i], midy + ay * i);
|
|
}
|
|
|
|
thing.angle += thing.dt * thing.direction;
|
|
}
|
|
|
|
/**
|
|
* Animation Function to close a CastleBridge when the player triggers its
|
|
* killonend after hitting the CastleAxe in EndInsideCastle. Its width is
|
|
* reduced repeatedly on an interval until it's 0.
|
|
*
|
|
* @param {CastleBridge} thing
|
|
* @remarks This is triggered as the killonend property of the bridge.
|
|
*/
|
|
animateCastleBridgeOpen(thing: ISolid): void {
|
|
thing.FSM.ScenePlayer.playRoutine("CastleBridgeOpen", thing);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when a CastleChain opens, which just delays a
|
|
* killNormal call for 7 steps.
|
|
*
|
|
* @param {CastleChain} thing
|
|
* @remarks This is triggered as the killonend property of the chain.
|
|
*/
|
|
animateCastleChainOpen(thing: ISolid): void {
|
|
thing.FSM.TimeHandler.addEvent(thing.FSM.killNormal, 3, thing);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when the player paddles underwater. Any previous
|
|
* Any previous paddling classes and cycle are removed, and a new one is
|
|
* added that, when it finishes, remnoves the player's paddlingCycle as
|
|
* well.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
animatePlayerPaddling(thing: IPlayer): void {
|
|
if (!thing.paddlingCycle) {
|
|
thing.FSM.removeClasses(
|
|
thing, "skidding paddle1 paddle2 paddle3 paddle4 paddle5"
|
|
);
|
|
thing.FSM.addClass(thing, "paddling");
|
|
thing.FSM.TimeHandler.cancelClassCycle(
|
|
thing, "paddlingCycle"
|
|
);
|
|
thing.FSM.TimeHandler.addClassCycle(
|
|
thing,
|
|
[
|
|
"paddle1", "paddle2", "paddle3", "paddle2", "paddle1",
|
|
function (): boolean {
|
|
return thing.paddlingCycle = false;
|
|
},
|
|
],
|
|
"paddlingCycle",
|
|
7
|
|
);
|
|
}
|
|
thing.paddling = thing.paddlingCycle = thing.swimming = true;
|
|
thing.yvel = thing.FSM.unitsize * -.84;
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when a player lands to reset size and remove
|
|
* hopping (and if underwater, paddling) classes. The mod event is fired.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
animatePlayerLanding(thing: IPlayer): void {
|
|
if (thing.crouching && thing.power > 1) {
|
|
thing.FSM.setHeight(thing, 11, true, true);
|
|
}
|
|
|
|
if (thing.FSM.hasClass(thing, "hopping")) {
|
|
thing.FSM.switchClass(thing, "hopping", "jumping");
|
|
}
|
|
|
|
if (thing.FSM.MapScreener.underwater) {
|
|
thing.FSM.removeClass(thing, "paddling");
|
|
}
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onPlayerLanding", thing, thing.resting);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when the player moves off a resting solid. It
|
|
* sets resting to undefined, and if underwater, switches the "running" and
|
|
* "paddling" classes.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
animatePlayerRestingOff(thing: IPlayer): void {
|
|
thing.resting = undefined;
|
|
if (thing.FSM.MapScreener.underwater) {
|
|
thing.FSM.switchClass(thing, "running", "paddling");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when a player breathes a underwater. This creates
|
|
* a Bubble, which slowly rises to the top of the screen.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
animatePlayerBubbling(thing: IPlayer): void {
|
|
thing.FSM.addThing("Bubble", thing.right, thing.top);
|
|
}
|
|
|
|
/**
|
|
* Animation Function to give the player a cycle of running classes. The
|
|
* cycle auto-updates its time as a function of how fast the player is
|
|
* moving relative to its maximum speed.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
animatePlayerRunningCycle(thing: IPlayer): void {
|
|
thing.FSM.switchClass(thing, "still", "running");
|
|
|
|
(<any>thing).running = thing.FSM.TimeHandler.addClassCycle(
|
|
thing,
|
|
[
|
|
"one", "two", "three", "two"
|
|
],
|
|
"running",
|
|
function (event: any): void {
|
|
event.timeout = 5 + Math.ceil(thing.maxspeedsave - Math.abs(thing.xvel));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when a player hops on an enemy. Resting is set to
|
|
* undefined, and a small vertical yvel is given.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
animateCharacterHop(thing: ICharacter): void {
|
|
thing.resting = undefined;
|
|
thing.yvel = thing.FSM.unitsize * -1.4;
|
|
}
|
|
|
|
/**
|
|
* Animation Function to start a player transferring through a Pipe. This is
|
|
* generic for entrances and exists horizontally and vertically: movement
|
|
* and velocities are frozen, size is reset, and the piping flag enabled.
|
|
* The player is also moved into the Scenery group to be behind the Pipe.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
animatePlayerPipingStart(thing: IPlayer): void {
|
|
thing.nocollide = thing.nofall = thing.piping = true;
|
|
thing.xvel = thing.yvel = 0;
|
|
thing.movementOld = thing.movement;
|
|
thing.movement = undefined;
|
|
|
|
if (thing.power > 1) {
|
|
thing.FSM.animatePlayerRemoveCrouch(thing);
|
|
thing.FSM.setPlayerSizeLarge(thing);
|
|
} else {
|
|
thing.FSM.setPlayerSizeSmall(thing);
|
|
}
|
|
thing.FSM.removeClasses(thing, "jumping running crouching");
|
|
|
|
thing.FSM.AudioPlayer.clearTheme();
|
|
thing.FSM.TimeHandler.cancelAllCycles(thing);
|
|
thing.FSM.GroupHolder.switchMemberGroup(thing, "Character", "Scenery");
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when a player is done passing through a Pipe. This
|
|
* is abstracted for exits both horizontally and vertically, typically after
|
|
* an area has just been entered.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
animatePlayerPipingEnd(thing: IPlayer): void {
|
|
thing.movement = thing.movementOld;
|
|
thing.nocollide = thing.nofall = thing.piping = false;
|
|
|
|
thing.FSM.AudioPlayer.resumeTheme();
|
|
thing.FSM.GroupHolder.switchMemberGroup(thing, "Scenery", "Character");
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when a player is hopping off a pole. It hops off
|
|
* and faces the opposite direction.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Boolean} [doRun] Whether the player should have a running cycle
|
|
* added immediately, such as during cutscenes
|
|
* (by default, false).
|
|
*/
|
|
animatePlayerOffPole(thing: IPlayer, doRun?: boolean): void {
|
|
thing.FSM.removeClasses(thing, "climbing running");
|
|
thing.FSM.addClass(thing, "jumping");
|
|
|
|
thing.xvel = 1.4;
|
|
thing.yvel = -.7;
|
|
thing.nocollide = thing.nofall = false;
|
|
thing.gravity = thing.FSM.MapScreener.gravity / 14;
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.movement = thing.FSM.movePlayer;
|
|
thing.gravity = thing.FSM.MapScreener.gravity;
|
|
|
|
thing.FSM.unflipHoriz(thing);
|
|
|
|
if (doRun) {
|
|
thing.FSM.animatePlayerRunningCycle(thing);
|
|
}
|
|
},
|
|
21);
|
|
}
|
|
|
|
/**
|
|
* Animation Function for when a player must hop off a Vine during an area's
|
|
* opening cutscene. The player switches sides, waits 14 steps, then calls
|
|
* animatePlayerOffPole.
|
|
*
|
|
* @param {Player} thing
|
|
*/
|
|
animatePlayerOffVine(thing: IPlayer): void {
|
|
thing.FSM.flipHoriz(thing);
|
|
thing.FSM.shiftHoriz(
|
|
thing,
|
|
(thing.width - 1) * thing.FSM.unitsize
|
|
);
|
|
|
|
thing.FSM.TimeHandler.addEvent(thing.FSM.animatePlayerOffPole, 14, thing);
|
|
}
|
|
|
|
|
|
/* Appearance utilities
|
|
*/
|
|
|
|
/**
|
|
* Makes one Thing look towards another, chainging lookleft and moveleft in
|
|
* the process.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Thing} other
|
|
*/
|
|
lookTowardsThing(thing: ICharacter, other: IThing): void {
|
|
// Case: other is to the left
|
|
if (other.right <= thing.left) {
|
|
thing.lookleft = true;
|
|
thing.moveleft = true;
|
|
thing.FSM.unflipHoriz(thing);
|
|
} else if (other.left >= thing.right) {
|
|
// Case: other is to the right
|
|
thing.lookleft = false;
|
|
thing.moveleft = false;
|
|
thing.FSM.flipHoriz(thing);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Makes one Thing look towards the player, chainging lookleft and moveleft
|
|
* in the process.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Boolean} [big] Whether to always change lookleft and moveleft,
|
|
* even if lookleft is already accurate (by
|
|
* default, false).
|
|
*/
|
|
lookTowardsPlayer(thing: ICharacter, big?: boolean): void {
|
|
// Case: Player is to the left
|
|
if (thing.FSM.player.right <= thing.left) {
|
|
if (!thing.lookleft || big) {
|
|
thing.lookleft = true;
|
|
thing.moveleft = false;
|
|
thing.FSM.unflipHoriz(thing);
|
|
}
|
|
} else if (thing.FSM.player.left >= thing.right) {
|
|
// Case: Player is to the right
|
|
if (thing.lookleft || big) {
|
|
thing.lookleft = false;
|
|
thing.moveleft = true;
|
|
thing.FSM.flipHoriz(thing);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* Death functions
|
|
*/
|
|
|
|
/**
|
|
* Standard Function to kill a Thing, which means marking it as dead and
|
|
* clearing its numquads, resting, movement, and cycles. It will later be
|
|
* marked as gone by its maintain* Function (Solids or Characters).
|
|
*
|
|
* @param {Thing} thing
|
|
*/
|
|
killNormal(thing: IThing): void {
|
|
if (!thing) {
|
|
return;
|
|
}
|
|
|
|
thing.hidden = thing.dead = true;
|
|
thing.alive = false;
|
|
thing.numquads = 0;
|
|
thing.movement = undefined;
|
|
|
|
if (this.hasOwnProperty("resting")) {
|
|
(<ICharacter>thing).resting = undefined;
|
|
}
|
|
|
|
if (thing.FSM) {
|
|
thing.FSM.TimeHandler.cancelAllCycles(thing);
|
|
}
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onKillNormal", thing);
|
|
}
|
|
|
|
/**
|
|
* Death Function commonly called on characters to animate a small flip
|
|
* before killNormal is called.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Number} [extra] How much time to wait beyond the standard 70
|
|
* steps before calling killNormal (by default,
|
|
* 0).
|
|
*/
|
|
killFlip(thing: ICharacter, extra: number = 0): void {
|
|
thing.FSM.flipVert(thing);
|
|
|
|
if ((<any>thing).bottomBump) {
|
|
(<any>thing).bottomBump = undefined;
|
|
}
|
|
|
|
thing.nocollide = thing.dead = true;
|
|
thing.speed = thing.xvel = 0;
|
|
thing.nofall = false;
|
|
thing.resting = thing.movement = undefined;
|
|
thing.yvel = -thing.FSM.unitsize;
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.killNormal, 70 + extra, thing);
|
|
}
|
|
|
|
/**
|
|
* Kill Function to replace a Thing with a spawned Thing, determined by the
|
|
* thing's spawnType, in the same location.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Boolean} [big] Whether this should skip creating the spawn (by
|
|
* default, false).
|
|
*/
|
|
killSpawn(thing: ICharacter, big?: boolean): IThing {
|
|
if (big) {
|
|
thing.FSM.killNormal(thing);
|
|
return;
|
|
}
|
|
|
|
if (!thing.spawnType) {
|
|
throw new Error("Thing " + thing.title + " has no .spawnType.");
|
|
}
|
|
|
|
var spawn: IThing = thing.FSM.ObjectMaker.make(
|
|
thing.spawnType,
|
|
thing.spawnSettings || {});
|
|
thing.FSM.addThing(spawn);
|
|
thing.FSM.setBottom(spawn, thing.bottom);
|
|
thing.FSM.setMidXObj(spawn, thing);
|
|
|
|
thing.FSM.killNormal(thing);
|
|
|
|
return spawn;
|
|
}
|
|
|
|
/**
|
|
* A kill Function similar to killSpawn but more configurable. A spawned
|
|
* Thing is created with the given attributes and copies over any specified
|
|
* attributes from the original Thing.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {String} title The type of new Thing to create, such as "Goomba".
|
|
* @param {Object} [attributes] An optional object to pass in to the
|
|
* ObjectMaker.make call (by default, {}).
|
|
* @param {String[]} [attributesCopied] An optional listing of attributes
|
|
* to copy from the original Thing
|
|
* (by default, none).
|
|
*/
|
|
killReplace(thing: IThing, title: string, attributes: any, attributesCopied?: string[]): IThing {
|
|
var spawn: IThing,
|
|
i: number;
|
|
|
|
if (typeof attributes === "undefined") {
|
|
attributes = {};
|
|
}
|
|
|
|
if (typeof attributesCopied !== "undefined") {
|
|
for (i = 0; i < attributesCopied.length; i += 1) {
|
|
attributes[attributesCopied[i]] = thing[attributesCopied[i]];
|
|
}
|
|
}
|
|
|
|
spawn = thing.FSM.ObjectMaker.make(title, attributes);
|
|
|
|
if (thing.flipHoriz) {
|
|
thing.FSM.flipHoriz(spawn);
|
|
}
|
|
|
|
if (thing.flipVert) {
|
|
thing.FSM.flipVert(spawn);
|
|
}
|
|
|
|
thing.FSM.addThing(spawn, thing.left, thing.top);
|
|
thing.FSM.killNormal(thing);
|
|
|
|
return spawn;
|
|
}
|
|
|
|
/**
|
|
* Kill Function for Goombas. If big isn't specified, it replaces the
|
|
* killed Goomba with a DeadGoomba via killSpawn.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Boolean} [big] Whether to call killFlip on the Thing instead of
|
|
* killSpawn, such as when a Shell hits it.
|
|
*/
|
|
killGoomba(thing: IGoomba, big?: boolean): void {
|
|
if (big) {
|
|
thing.FSM.killFlip(thing);
|
|
return;
|
|
}
|
|
|
|
thing.FSM.killSpawn(thing);
|
|
}
|
|
|
|
/**
|
|
* Kill Function for Koopas. Jumping and floating Koopas are replacing with
|
|
* an equivalent Koopa that's just walking, while walking Koopas become
|
|
* Shells.
|
|
*
|
|
* @param {Koopa} thing
|
|
* @param {Boolean} [big] Whether shells should be immediately killed.
|
|
* @remarks This isn't called when a Shell hits a Koopa.
|
|
*/
|
|
killKoopa(thing: IKoopa, big?: boolean): ICharacter {
|
|
var spawn: ICharacter;
|
|
|
|
if (thing.jumping || thing.floating) {
|
|
spawn = <ICharacter>thing.FSM.killReplace(
|
|
thing, "Koopa", undefined, ["smart", "direction", "moveleft"]
|
|
);
|
|
spawn.xvel = spawn.moveleft ? -spawn.speed : spawn.speed;
|
|
} else {
|
|
spawn = thing.FSM.killToShell(thing, Number(big));
|
|
}
|
|
|
|
return spawn;
|
|
}
|
|
|
|
/**
|
|
* Kill Function for Lakitus. If this is the last Lakitu in Characters,
|
|
* a new one is scheduled to be spawned at the same y-position.
|
|
*
|
|
* @param {Lakitu} thing
|
|
*/
|
|
killLakitu(thing: ILakitu): void {
|
|
var characters: ICharacter[] = <ICharacter[]>thing.FSM.GroupHolder.getGroup("Character"),
|
|
i: number;
|
|
|
|
thing.FSM.killFlip(thing);
|
|
thing.FSM.MapScreener.lakitu = undefined;
|
|
|
|
// If any other Lakitu exists after killNormal, killLakitu is done
|
|
for (i = 0; i < characters.length; i += 1) {
|
|
if (characters[i].title === "Lakitu") {
|
|
thing.FSM.MapScreener.lakitu = <ILakitu>characters[i];
|
|
return;
|
|
}
|
|
}
|
|
|
|
// The next Lakitu is spawned ~5 seconds later, give or take
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.addThing.bind(thing.FSM),
|
|
350,
|
|
"Lakitu",
|
|
thing.FSM.MapScreener.right,
|
|
thing.top);
|
|
}
|
|
|
|
/**
|
|
* Kill Function for Bowsers. In reality this is only called when the player
|
|
* Fireballs him or all NPCs are to be killed. It takes five Fireballs to
|
|
* killFlip a Bowser, which scores 5000 points.
|
|
*
|
|
* @param {Bowser} thing
|
|
* @param {Boolean} [big] Whether this should default to killFlip, as in
|
|
* an EndInsideCastle cutscene.
|
|
*/
|
|
killBowser(thing: IBowser, big?: boolean): void {
|
|
if (big) {
|
|
thing.nofall = false;
|
|
thing.movement = undefined;
|
|
thing.FSM.killFlip(<any>thing.FSM.killSpawn(thing));
|
|
return;
|
|
}
|
|
|
|
thing.deathcount += 1;
|
|
if (thing.deathcount === 5) {
|
|
thing.yvel = thing.speed = 0;
|
|
thing.movement = undefined;
|
|
thing.FSM.killFlip(<any>thing.FSM.killSpawn(thing), 350);
|
|
thing.FSM.scoreOn(5000, thing);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Kills a Thing by replacing it with another Thing, typically a Shell or
|
|
* BeetleShell (determined by thing.shelltype). The spawn inherits smartness
|
|
* and location from its parent, and is temporarily given nocollidechar to
|
|
* stop double collision detections.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Number} [big] Whether the spawned Shell should be killed
|
|
* immediately (by default, false).
|
|
*/
|
|
killToShell(thing: ICharacter, big?: number): IShell {
|
|
var spawn: IShell,
|
|
nocollidecharold: boolean,
|
|
nocollideplayerold: boolean;
|
|
|
|
thing.spawnSettings = {
|
|
"smart": thing.smart
|
|
};
|
|
|
|
if (big && big !== 2) {
|
|
thing.spawnType = thing.title;
|
|
} else {
|
|
thing.spawnType = thing.shelltype || "Shell";
|
|
}
|
|
|
|
thing.spawnSettings = {
|
|
"smart": thing.smart
|
|
};
|
|
|
|
spawn = <IShell>thing.FSM.killSpawn(thing);
|
|
nocollidecharold = spawn.nocollidechar;
|
|
nocollideplayerold = spawn.nocollideplayer;
|
|
spawn.nocollidechar = true;
|
|
spawn.nocollideplayer = true;
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
spawn.nocollidechar = nocollidecharold;
|
|
spawn.nocollideplayer = nocollideplayerold;
|
|
},
|
|
7);
|
|
|
|
thing.FSM.killNormal(thing);
|
|
|
|
if (big === 2) {
|
|
thing.FSM.killFlip(spawn);
|
|
}
|
|
|
|
return spawn;
|
|
}
|
|
|
|
/**
|
|
* Wipes the screen of any characters or solids that should be gone during
|
|
* an important cutscene, such as hitting an end-of-level flag.
|
|
* For characters, they're deleted if .nokillonend isn't truthy. If they
|
|
* have a .killonend function, that's called on them.
|
|
* Solids are only deleted if their .killonend is true.
|
|
*
|
|
* @remarks If thing.killonend is a Function, it is called on the Thing.
|
|
* @todo Rename .killonend to be more accurate
|
|
*/
|
|
killNPCs(): void {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this),
|
|
group: IThing[],
|
|
character: ICharacter,
|
|
solid: ISolid,
|
|
i: number;
|
|
|
|
// Characters: they must opt out of being killed with .nokillonend, and
|
|
// may opt into having a function called instead (such as Lakitus).
|
|
group = <ICharacter[]>FSM.GroupHolder.getGroup("Character");
|
|
for (i = group.length - 1; i >= 0; --i) {
|
|
character = <ICharacter>group[i];
|
|
|
|
if (!character.nokillend) {
|
|
character.FSM.killNormal(character);
|
|
character.FSM.arrayDeleteThing(character, group, i);
|
|
} else if (character.killonend) {
|
|
character.killonend(character);
|
|
}
|
|
}
|
|
|
|
// Solids: they may opt into being deleted
|
|
group = <ISolid[]>FSM.GroupHolder.getGroup("Solid");
|
|
for (i = group.length - 1; i >= 0; --i) {
|
|
solid = <ISolid>group[i];
|
|
|
|
if (solid.killonend) {
|
|
if (solid.killonend.constructor === Function) {
|
|
(<any>solid.killonend)(solid, group, i);
|
|
} else {
|
|
solid.FSM.arrayDeleteThing(solid, group, i);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Kill Function for Bricks. The Brick is killed an an animateBrickShards
|
|
* animation is timed. If other is provided, it's also marked as the Brick's
|
|
* up, which will kill colliding characters: this works because
|
|
* maintainSolids happens before maintainCharacters, so the killNormal won't
|
|
* come into play until after the next maintainCharacters call.
|
|
*
|
|
* @param {Brick} thing
|
|
* @param {Thing} [other] An optional Thing to mark as the cause of the
|
|
* Brick's death (its up attribute).
|
|
*/
|
|
killBrick(thing: IBrick, other?: ICharacter): void {
|
|
thing.FSM.AudioPlayer.play("Break Block");
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.animateBrickShards, 1, thing
|
|
);
|
|
thing.FSM.killNormal(thing);
|
|
|
|
if (
|
|
other instanceof thing.FSM.ObjectMaker.getFunction("Thing")
|
|
) {
|
|
thing.up = other;
|
|
} else {
|
|
thing.up = undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Kill Function for the player. It's big and complicated, but in general...
|
|
* 1. If big === 2, just kill it altogether
|
|
* 2. If the player is large and big isn't true, just power down the player.
|
|
* 3. The player can't survive this, so animate the "shrug" class and an
|
|
* up-then-down movement.
|
|
* At the end of 1. and 3., decrease the "lives" and "power" statistics and
|
|
* call the equivalent onPlayerDeath or onGameOver callbacks, depending on
|
|
* how many lives are left. The mod event is also fired.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Number} [big] The severity of this death: 0 for normal, 1 for
|
|
* not survivable, 2 for immediate death.
|
|
*/
|
|
killPlayer(thing: IPlayer, big?: number): void {
|
|
if (!thing.alive || thing.flickering || thing.dying) {
|
|
return;
|
|
}
|
|
|
|
var FSM: FullScreenMario = thing.FSM,
|
|
area: IArea = <IArea>thing.FSM.MapsHandler.getArea();
|
|
|
|
// Large big: real, no-animation death
|
|
if (big === 2) {
|
|
thing.dead = thing.dying = true;
|
|
thing.alive = false;
|
|
FSM.MapScreener.notime = true;
|
|
} else {
|
|
// Regular big: regular (enemy, time, etc.) kill
|
|
// If the player can survive this, just power down
|
|
if (!big && thing.power > 1) {
|
|
thing.power = 1;
|
|
FSM.ItemsHolder.setItem("power", 1);
|
|
FSM.AudioPlayer.play("Power Down");
|
|
FSM.playerGetsSmall(thing);
|
|
return;
|
|
} else {
|
|
// The player can't survive this: animate a death
|
|
thing.dying = true;
|
|
|
|
FSM.setSize(thing, 7.5, 7, true);
|
|
FSM.updateSize(thing);
|
|
FSM.setClass(thing, "character player dead");
|
|
FSM.thingPauseVelocity(thing);
|
|
FSM.arrayToEnd(
|
|
thing, <any[]>FSM.GroupHolder.getGroup(thing.groupType));
|
|
|
|
FSM.MapScreener.notime = true;
|
|
FSM.MapScreener.nokeys = true;
|
|
|
|
FSM.TimeHandler.cancelAllCycles(thing);
|
|
FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
FSM.thingResumeVelocity(thing, true);
|
|
thing.nocollide = true;
|
|
thing.movement = thing.resting = undefined;
|
|
thing.gravity = FSM.MapScreener.gravity / 2.1;
|
|
thing.yvel = FullScreenMario.unitsize * -1.4;
|
|
},
|
|
7);
|
|
}
|
|
}
|
|
|
|
thing.nocollide = thing.nomove = thing.dead = true;
|
|
FSM.MapScreener.nokeys = true;
|
|
FSM.AudioPlayer.clearAll();
|
|
FSM.AudioPlayer.play("Player Dies");
|
|
FSM.ItemsHolder.decrease("lives");
|
|
FSM.ItemsHolder.setItem("power", 1);
|
|
|
|
if (FSM.ItemsHolder.getItem("lives") > 0) {
|
|
FSM.TimeHandler.addEvent(
|
|
area.onPlayerDeath.bind(FSM),
|
|
area.onPlayerDeathTimeout,
|
|
FSM
|
|
);
|
|
} else {
|
|
FSM.TimeHandler.addEvent(
|
|
area.onGameOver.bind(FSM),
|
|
area.onGameOverTimeout,
|
|
FSM);
|
|
}
|
|
}
|
|
|
|
|
|
/* Scoring
|
|
*/
|
|
|
|
/**
|
|
* @this {EightBittr}
|
|
* @param {Number} level What number call this is in a chain of scoring
|
|
* events, such as a Shell or hopping spree.
|
|
* @return {Number} How many points should be gained (if 0, that means the
|
|
* maximum points were passed and gainLife was called).
|
|
*/
|
|
findScore(level: number): number {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this);
|
|
|
|
if (level < FSM.pointLevels.length) {
|
|
return FSM.pointLevels[level];
|
|
}
|
|
|
|
FSM.gainLife(1);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Driver function to score some number of points for the player and show
|
|
* the gains via an animation.
|
|
*
|
|
* @this {EightBittr}
|
|
* @param {Number} value How many points the player is receiving.
|
|
* @param {Boolean} [continuation] Whether the game shouldn't increase the
|
|
* score amount in the ItemsHoldr (this will
|
|
* only be false on the first score() call).
|
|
* @remarks For point gains that should not have a visual animation,
|
|
* directly call ItemsHolder.increase("score", value).
|
|
* @remarks The calling chain will be:
|
|
* score -> scoreOn -> scoreAnimateOn -> scoreAnimate
|
|
*/
|
|
score(value: number, continuation?: boolean): void {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this);
|
|
|
|
FSM.scoreOn(value, FSM.player, true);
|
|
|
|
if (!continuation) {
|
|
this.ItemsHolder.increase("score", value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scores a given number of points for the player, and shows the gains via
|
|
* an animation centered at the top of a thing.
|
|
*
|
|
* @param {Number} value How many points the player is receiving.
|
|
* @param {Thing} thing An in-game Thing to place the visual score text
|
|
* on top of and centered.
|
|
* @param {Boolean} [continuation] Whether the game shouldn't increase the
|
|
* score amount in the ItemsHoldr (this will
|
|
* only be false on the first score() call).
|
|
* @remarks The calling chain will be:
|
|
* scoreOn -> scoreAnimateOn -> scoreAnimate
|
|
*/
|
|
scoreOn(value: number, thing: IThing, continuation?: boolean): void {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
var text: IText = <IText>thing.FSM.addThing("Text" + value);
|
|
|
|
thing.FSM.scoreAnimateOn(<IText>text, thing);
|
|
|
|
if (!continuation) {
|
|
this.ItemsHolder.increase("score", value);
|
|
}
|
|
|
|
thing.FSM.ModAttacher.fireEvent("onScoreOn", value, thing, continuation);
|
|
}
|
|
|
|
/**
|
|
* Centers a text associated with some points gain on the top of a Thing,
|
|
* and animates it updward, setting an event for it to die.
|
|
*
|
|
* @param {Thing} text The text whose position is being manipulated.
|
|
* @param {Thing} thing An in-game Thing to place the visual score text
|
|
* on top of and centered.
|
|
* @remarks The calling chain will be:
|
|
* scoreAnimateOn -> scoreAnimate
|
|
*/
|
|
scoreAnimateOn(text: IText, thing: IThing): void {
|
|
thing.FSM.setMidXObj(text, thing);
|
|
thing.FSM.setBottom(text, thing.top);
|
|
thing.FSM.scoreAnimate(text);
|
|
}
|
|
|
|
/**
|
|
* Animates a score on top of a Thing.
|
|
*
|
|
* @param {Thing} thing
|
|
* @param {Number} [timeout] How many game ticks to wait before killing
|
|
* the text (defaults to 28).
|
|
* @remarks This is the last function in the score() calling chain:
|
|
* scoreAnimate <- scoreAnimateOn <- scoreOn <- score
|
|
*/
|
|
scoreAnimate(thing: IThing, timeout: number = 28): void {
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
thing.FSM.shiftVert,
|
|
1,
|
|
timeout,
|
|
thing,
|
|
-thing.FSM.unitsize / 6
|
|
);
|
|
thing.FSM.TimeHandler.addEvent(
|
|
thing.FSM.killNormal, timeout, thing
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Inelegant catch-all Function for when the player has hit a shell and
|
|
* needs points to be scored. This takes into account player star status and
|
|
* Shell resting and peeking. With none of those modifiers, it defaults to
|
|
* scoreOn with 400.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Shell} other
|
|
* @remarks See http://themushroomkingdom.net/smb_breakdown.shtml
|
|
*/
|
|
scorePlayerShell(thing: IPlayer, other: IShell): void {
|
|
// Star player: 200 points
|
|
if (thing.star) {
|
|
thing.FSM.scoreOn(200, other);
|
|
return;
|
|
}
|
|
|
|
// Shells in the air: 8000 points (see guide)
|
|
if (!other.resting) {
|
|
thing.FSM.scoreOn(8000, other);
|
|
return;
|
|
}
|
|
|
|
// Peeking shells: 1000 points
|
|
if (other.peeking) {
|
|
thing.FSM.scoreOn(1000, other);
|
|
return;
|
|
}
|
|
|
|
// Already hopping: 500 points
|
|
if (thing.jumpcount) {
|
|
thing.FSM.scoreOn(500, other);
|
|
return;
|
|
}
|
|
|
|
// All other cases: the shell's default
|
|
thing.FSM.scoreOn(400, other);
|
|
}
|
|
|
|
/**
|
|
* Determines the amount a player should score upon hitting a flag, based on
|
|
* the player's y-position.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Number} difference How far up the pole the collision happened,
|
|
* by absolute amount (not multiplied by
|
|
* unitsize).
|
|
* @return {Number}
|
|
* @remarks See http://themushroomkingdom.net/smb_breakdown.shtml
|
|
*/
|
|
scorePlayerFlag(thing: IThing, difference: number): number {
|
|
var amount: number;
|
|
|
|
if (difference < 28) {
|
|
amount = difference < 8 ? 100 : 400;
|
|
} else if (difference < 40) {
|
|
amount = 800;
|
|
} else {
|
|
amount = difference < 62 ? 2000 : 5000;
|
|
}
|
|
|
|
return amount;
|
|
}
|
|
|
|
|
|
/* Audio
|
|
*/
|
|
|
|
/**
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Number} xloc The x-location of the sound's source.
|
|
* @return {Number} How loud the sound should be at that position, in [0,1].
|
|
* This is louder closer to the player, and nothing to
|
|
* the right of the visible screen.
|
|
*/
|
|
getVolumeLocal(FSM: FullScreenMario, xloc: number): number {
|
|
if (xloc > FSM.MapScreener.right) {
|
|
return 0;
|
|
}
|
|
|
|
return Math.max(
|
|
.14,
|
|
Math.min(
|
|
.84,
|
|
(
|
|
FSM.MapScreener.width - Math.abs(
|
|
xloc - FSM.player.left
|
|
)
|
|
) / FSM.MapScreener.width
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param {FullScreenMario} FSM
|
|
* @return {String} The name of the default audio for the current area,
|
|
* which is the first word in the area's setting (split on
|
|
* spaces).
|
|
*/
|
|
getAudioThemeDefault(FSM: FullScreenMario): string {
|
|
return FSM.MapsHandler.getArea().setting.split(" ")[0];
|
|
}
|
|
|
|
|
|
/* Map sets
|
|
*/
|
|
|
|
/**
|
|
* Sets the game state to a new map, resetting all Things and inputs in the
|
|
* process. The mod events are fired.
|
|
*
|
|
* @param {String} [name] The name of the map (by default, the currently
|
|
* played one).
|
|
* @param {Mixed} [location] The name of the location within the map (by
|
|
* default 0 for the first in Array form).
|
|
* @remarks Most of the work here is done by setLocation.
|
|
*/
|
|
setMap(name?: string | IFullScreenMario, location?: string | number): void {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this),
|
|
time: number,
|
|
map: IMap;
|
|
|
|
if (typeof name === "undefined" || name.constructor === FullScreenMario) {
|
|
name = FSM.MapsHandler.getMapName();
|
|
}
|
|
|
|
map = FSM.MapsHandler.setMap(<string>name);
|
|
|
|
FSM.ModAttacher.fireEvent("onPreSetMap", map);
|
|
|
|
if (map.seed) {
|
|
FSM.NumberMaker.resetFromSeed(map.seed);
|
|
}
|
|
|
|
FSM.ItemsHolder.setItem("world", name);
|
|
FSM.InputWriter.restartHistory();
|
|
|
|
FSM.ModAttacher.fireEvent("onSetMap", map);
|
|
|
|
FSM.setLocation(location || map.locationDefault || FSM.settings.maps.locationDefault);
|
|
|
|
time = (<IArea>FSM.MapsHandler.getArea()).time || (<IMap>FSM.MapsHandler.getMap()).time;
|
|
FSM.ItemsHolder.setItem("time", Number(time));
|
|
}
|
|
|
|
/**
|
|
* Sets the game state to a location within the current map, resetting all
|
|
* Things, inputs, the current Area, PixelRender, and MapScreener in the
|
|
* process. The location's entry Function is called to bring a new Player
|
|
* into the game. The mod events are fired.
|
|
*
|
|
* @param {Mixed} [name] The name of the location within the map (by
|
|
* default 0 for the first in Array form).
|
|
*/
|
|
setLocation(name: string | number = 0): void {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this),
|
|
location: ILocation;
|
|
|
|
(<IMapScreenr>FSM.MapScreener).nokeys = false;
|
|
(<IMapScreenr>FSM.MapScreener).notime = false;
|
|
(<IMapScreenr>FSM.MapScreener).canscroll = true;
|
|
FSM.MapScreener.clearScreen();
|
|
FSM.GroupHolder.clearArrays();
|
|
FSM.TimeHandler.cancelAllEvents();
|
|
|
|
FSM.MapsHandler.setLocation((name || 0).toString());
|
|
FSM.MapScreener.setVariables();
|
|
location = <ILocation>FSM.MapsHandler.getLocation((name || 0).toString());
|
|
|
|
FSM.ModAttacher.fireEvent("onPreSetLocation", location);
|
|
|
|
FSM.PixelDrawer.setBackground((<IArea>FSM.MapsHandler.getArea()).background);
|
|
|
|
FSM.TimeHandler.addEventInterval(FSM.maintainTime, 25, Infinity, FSM);
|
|
FSM.TimeHandler.addEventInterval(FSM.maintainScenery, 350, Infinity, FSM);
|
|
|
|
FSM.AudioPlayer.clearAll();
|
|
FSM.AudioPlayer.playTheme();
|
|
|
|
FSM.QuadsKeeper.resetQuadrants();
|
|
|
|
location.entry(FSM, location);
|
|
|
|
FSM.ModAttacher.fireEvent("onSetLocation", location);
|
|
|
|
FSM.GamesRunner.play();
|
|
}
|
|
|
|
/* Map entrances
|
|
*/
|
|
|
|
/**
|
|
* Standard map entrance Function for dropping from the ceiling. A new
|
|
* player is placed 16x16 units away from the top-left corner, with
|
|
* location.xloc scrolling applied if necessary.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Location} [location] The calling Location entering into (by
|
|
* default, not used).
|
|
*/
|
|
mapEntranceNormal(FSM: FullScreenMario, location?: ILocation): void {
|
|
if (location && location.xloc) {
|
|
FSM.scrollWindow(location.xloc * FSM.unitsize);
|
|
}
|
|
|
|
FSM.addPlayer(FSM.unitsize * 16, FSM.unitsize * 16);
|
|
}
|
|
|
|
/**
|
|
* Standard map entrance Function for starting on the ground. A new player
|
|
* is placed 16x16 units away from the top-left corner, with location.xloc
|
|
* scrolling applied if necessary.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Location} [location] The calling Location entering into (by
|
|
* default, not used).
|
|
*/
|
|
mapEntrancePlain(FSM: FullScreenMario, location?: ILocation): void {
|
|
if (location && location.xloc) {
|
|
FSM.scrollWindow(location.xloc * FSM.unitsize);
|
|
}
|
|
|
|
FSM.addPlayer(FSM.unitsize * 16, FSM.MapScreener.floor * FSM.unitsize);
|
|
|
|
}
|
|
|
|
/**
|
|
* Map entrance Function for starting on the ground and immediately walking
|
|
* as if in a cutscene. mapEntrancePlain is immediately called, and the
|
|
* player has movement forced to be walking, with nokeys and notime set to
|
|
* true.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Location} [location] The calling Location entering into (by
|
|
* default, not used).
|
|
*/
|
|
mapEntranceWalking(FSM: FullScreenMario, location?: ILocation): void {
|
|
FSM.mapEntrancePlain(FSM, location);
|
|
|
|
FSM.player.keys.run = 1;
|
|
FSM.player.maxspeed = FSM.player.walkspeed;
|
|
|
|
FSM.MapScreener.nokeys = true;
|
|
FSM.MapScreener.notime = true;
|
|
}
|
|
|
|
/**
|
|
* Map entrance Function for entering a castle area. The player is simply
|
|
* added at 2 x 56.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
*/
|
|
mapEntranceCastle(FSM: FullScreenMario): void {
|
|
FSM.addPlayer(FSM.unitsize * 2, FSM.unitsize * 56);
|
|
}
|
|
|
|
/**
|
|
* Map entrance Function for entering an area climbing a Vine. The Vine
|
|
* enters first by growing, then the player climbs it and hops off. The
|
|
* player's actions are done via mapEntranceVinePlayer and are triggered
|
|
* when the Vine's top reaches its threshold.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
*/
|
|
mapEntranceVine(FSM: FullScreenMario): void {
|
|
var threshold: number = (
|
|
FSM.MapScreener.bottom - FSM.unitsize * 40
|
|
),
|
|
vine: IVine = <IVine>FSM.addThing(
|
|
"Vine",
|
|
FSM.unitsize * 32,
|
|
FSM.MapScreener.bottom + FSM.unitsize * 8
|
|
);
|
|
|
|
FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
if (vine.top >= threshold) {
|
|
return false;
|
|
}
|
|
|
|
vine.movement = undefined;
|
|
FSM.mapEntranceVinePlayer(FSM, vine);
|
|
return true;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
/**
|
|
* Continuation of mapEntranceVine for the player's actions. The player
|
|
* climbs up the Vine; once it reaches the threshold, it hops off using
|
|
* animatePlayerOffVine.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Vine} vine
|
|
*/
|
|
mapEntranceVinePlayer(FSM: FullScreenMario, vine: IVine): void {
|
|
var threshold: number = (
|
|
FSM.MapScreener.bottom - FSM.unitsize * 24
|
|
),
|
|
speed: number = FSM.unitsize / -4,
|
|
player: IPlayer = FSM.addPlayer(
|
|
FSM.unitsize * 29,
|
|
FSM.MapScreener.bottom - FSM.unitsize * 4
|
|
);
|
|
|
|
FSM.shiftVert(player, player.height * FSM.unitsize);
|
|
|
|
FSM.collideVine(player, vine);
|
|
|
|
FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
FSM.shiftVert(player, speed);
|
|
if (player.top < threshold) {
|
|
FSM.TimeHandler.addEvent(
|
|
FSM.animatePlayerOffVine, 49, player
|
|
);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
/**
|
|
* Map entrance Function for coming in through a vertical Pipe. The player
|
|
* is added just below the top of the Pipe, and is animated to rise up
|
|
* through it like an Italian chestburster.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Location} [location] The calling Location entering into (by
|
|
* default, not used).
|
|
*/
|
|
mapEntrancePipeVertical(FSM: FullScreenMario, location?: ILocation): void {
|
|
if (location && location.xloc) {
|
|
FSM.scrollWindow(location.xloc * FSM.unitsize);
|
|
}
|
|
|
|
FSM.addPlayer(
|
|
location.entrance.left + FSM.player.width * FSM.unitsize / 2,
|
|
location.entrance.top + FSM.player.height * FSM.unitsize);
|
|
|
|
FSM.animatePlayerPipingStart(FSM.player);
|
|
FSM.AudioPlayer.play("Pipe");
|
|
FSM.AudioPlayer.addEventListener(
|
|
"Pipe",
|
|
"ended",
|
|
function (): void {
|
|
FSM.AudioPlayer.playTheme();
|
|
});
|
|
|
|
FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
FSM.shiftVert(FSM.player, FSM.unitsize / -4);
|
|
|
|
if (FSM.player.bottom <= location.entrance.top) {
|
|
FSM.animatePlayerPipingEnd(FSM.player);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
/**
|
|
* Map entrance Function for coming in through a horizontal Pipe. The player
|
|
* is added just to the left of the entrance, and is animated to pass
|
|
* through it like an Italian chestburster.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Location} [location] The calling Location entering into (by
|
|
* default, not used).
|
|
*/
|
|
mapEntrancePipeHorizontal(FSM: FullScreenMario, location?: ILocation): void {
|
|
throw new Error("mapEntrancePipeHorizontal is not yet implemented.");
|
|
}
|
|
|
|
/**
|
|
* Map entrance Function for the player reincarnating into a level,
|
|
* typically from a random map. The player is placed at 16 x 0 and a
|
|
* Resting Stone placed some spaces below via playerAddRestingStone.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
*/
|
|
mapEntranceRespawn(FSM: FullScreenMario): void {
|
|
FSM.MapScreener.nokeys = false;
|
|
FSM.MapScreener.notime = false;
|
|
FSM.MapScreener.canscroll = true;
|
|
|
|
FSM.addPlayer(FSM.unitsize * 16, 0);
|
|
FSM.animateFlicker(FSM.player);
|
|
|
|
if (!FSM.MapScreener.underwater) {
|
|
FSM.playerAddRestingStone(FSM.player);
|
|
}
|
|
|
|
FSM.ModAttacher.fireEvent("onPlayerRespawn");
|
|
}
|
|
|
|
|
|
/* Map exits
|
|
*/
|
|
|
|
/**
|
|
* Map exit Function for leaving through a vertical Pipe. The player is
|
|
* animated to pass through it and then transfer locations.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Pipe} other
|
|
*/
|
|
mapExitPipeVertical(thing: IPlayer, other: IPipe): void {
|
|
if (!thing.resting || typeof (other.transport) === "undefined"
|
|
|| thing.right + thing.FSM.unitsize * 2 > other.right
|
|
|| thing.left - thing.FSM.unitsize * 2 < other.left) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.animatePlayerPipingStart(thing);
|
|
thing.FSM.AudioPlayer.play("Pipe");
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
thing.FSM.shiftVert(thing, thing.FSM.unitsize / 4);
|
|
|
|
if (thing.top <= other.top) {
|
|
return false;
|
|
}
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
if (other.transport.constructor === Object) {
|
|
thing.FSM.setMap(other.transport.map);
|
|
} else {
|
|
thing.FSM.setLocation(other.transport);
|
|
}
|
|
},
|
|
42);
|
|
|
|
return true;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
/**
|
|
* Map exit Function for leaving through a horiontal Pipe. The player is
|
|
* animated to pass through it and then transfer locations.
|
|
*
|
|
* @param {Player} thing
|
|
* @param {Pipe} other
|
|
* @param {Boolean} [shouldTransport] Whether not resting and not paddling
|
|
* does not imply the player cannot
|
|
* pass through the Pipe (by default,
|
|
* false, as this is normal).
|
|
* @remarks The shouldTransport argument was added because the "Bouncy
|
|
* Bounce!" mod rendered some areas unenterable without it.
|
|
*/
|
|
mapExitPipeHorizontal(thing: IPlayer, other: IPipe, shouldTransport?: boolean): void {
|
|
if (!shouldTransport && !thing.resting && !thing.paddling) {
|
|
return;
|
|
}
|
|
|
|
if (thing.top < other.top || thing.bottom > other.bottom) {
|
|
return;
|
|
}
|
|
|
|
if (!thing.keys.run) {
|
|
return;
|
|
}
|
|
|
|
thing.FSM.animatePlayerPipingStart(thing);
|
|
thing.FSM.AudioPlayer.play("Pipe");
|
|
|
|
thing.FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
thing.FSM.shiftHoriz(thing, thing.FSM.unitsize / 4);
|
|
|
|
if (thing.left <= other.left) {
|
|
return false;
|
|
}
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.FSM.setLocation(other.transport);
|
|
},
|
|
42);
|
|
|
|
return true;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
|
|
/* Map creation
|
|
*/
|
|
|
|
/**
|
|
* The onMake callback for Areas. Attributes are copied as specified in the
|
|
* prototype, and the background is set based on the setting.
|
|
*
|
|
* @this {Area}
|
|
*/
|
|
initializeArea(): void {
|
|
var scope: IArea = <IArea><any>this,
|
|
i: string;
|
|
|
|
// Copy all attributes, if they exist
|
|
if (scope.attributes) {
|
|
for (i in scope.attributes) {
|
|
if (scope.hasOwnProperty(i) && scope[i]) {
|
|
FullScreenMario.prototype.proliferate(scope, scope.attributes[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
scope.setBackground(scope);
|
|
}
|
|
|
|
/**
|
|
* Sets an area's background as a function of its setting.
|
|
*
|
|
* @param {Area} area
|
|
* @remarks In the future, it might be more elegant to make Areas inherit
|
|
* from base Area types (Overworld, etc.) so this inelegant switch
|
|
* statement doesn't have to be used.
|
|
*/
|
|
setAreaBackground(area: IArea): void {
|
|
// Non-underwater Underworld, Castle, and all Nights: black background
|
|
if (
|
|
area.setting.indexOf("Underwater") === -1
|
|
&& (
|
|
area.setting.indexOf("Underworld") !== -1
|
|
|| area.setting.indexOf("Castle") !== -1
|
|
|| area.setting.indexOf("Night") !== -1
|
|
)
|
|
) {
|
|
area.background = "#000000";
|
|
} else {
|
|
// Default (typically Overworld): sky blue background
|
|
area.background = "#5c94fc";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Number} yloc A height to find the distance to the floor from.
|
|
* @param {Boolean} [correctUnitsize] Whether the yloc accounts for
|
|
* unitsize expansion (e.g. 48 rather
|
|
* than 12, for unitsize=4).
|
|
* @return {Number} The distance from the absolute base (bottom of the
|
|
* user's viewport) to a specific height above the floor
|
|
* (in the form given by map functions, distance from the
|
|
* floor).
|
|
*/
|
|
getAbsoluteHeight(yloc: number, correctUnitsize?: boolean): number {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this),
|
|
height: number = yloc + FSM.MapScreener.height;
|
|
|
|
if (!correctUnitsize) {
|
|
height *= FSM.unitsize;
|
|
}
|
|
|
|
return height;
|
|
}
|
|
|
|
/**
|
|
* Adds a PreThing to the map and stretches it to fit a width equal to the
|
|
* current map's outermost boundaries.
|
|
*
|
|
* @this {EightBittr}
|
|
* @param {PreThing} prethingRaw
|
|
* @return {Thing} A strethed Thing, newly added via addThing.
|
|
*/
|
|
mapAddStretched(prethingRaw: string | IPreThingSettings): IThing {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this),
|
|
boundaries: any = FSM.MapsHandler.getArea().boundaries,
|
|
prething: IPreThingSettings = prethingRaw instanceof String
|
|
? {
|
|
"thing": prething
|
|
}
|
|
: <IPreThingSettings>prethingRaw,
|
|
y: number = (
|
|
((<IMapScreenr>FSM.MapScreener).floor - prething.y)
|
|
* FSM.unitsize
|
|
),
|
|
// It is assumed the PreThing does have a .thing if it's a stretch
|
|
thing: IThing = FSM.ObjectMaker.make((<any>prething).thing, {
|
|
"width": boundaries.right - boundaries.left,
|
|
"height": prething.height || FSM.getAbsoluteHeight(prething.y)
|
|
});
|
|
|
|
return <IThing>FSM.addThing(thing, boundaries.left, y);
|
|
}
|
|
|
|
/**
|
|
* Analyzes a PreThing to be placed to the right of the current map's
|
|
* boundaries (after everything else).
|
|
*
|
|
* @this {EightBittr}
|
|
* @param {PreThing} prethingRaw
|
|
*/
|
|
mapAddAfter(prethingRaw: string | IPreThingSettings): void {
|
|
var FSM: FullScreenMario = FullScreenMario.prototype.ensureCorrectCaller(this),
|
|
MapsCreator: MapsCreatr.IMapsCreatr = FSM.MapsCreator,
|
|
MapsHandler: MapsHandlr.IMapsHandlr = FSM.MapsHandler,
|
|
prethings: { [i: string]: IPreThing[] } = <{ [i: string]: IPreThing[] }>MapsHandler.getPreThings(),
|
|
prething: IPreThingSettings = prethingRaw instanceof String
|
|
? {
|
|
"thing": prething
|
|
}
|
|
: <IPreThingSettings>prethingRaw,
|
|
area: IArea = <IArea>MapsHandler.getArea(),
|
|
map: MapsCreatr.IMapsCreatrMap = MapsHandler.getMap(),
|
|
boundaries: any = FSM.MapsHandler.getArea().boundaries;
|
|
|
|
prething.x = boundaries.right;
|
|
MapsCreator.analyzePreSwitch(prething, prethings, area, map);
|
|
}
|
|
|
|
|
|
/* Cutscenes
|
|
*/
|
|
|
|
/**
|
|
* First cutscene for the Flagpole routine. The player becomes invincible and
|
|
* starts sliding down the flagpole, while all other Things are killed.
|
|
* A score calculated by scorePlayerFlag is shown at the base of the pole and
|
|
* works its way up. The collideFlagBottom callback will be fired when the player
|
|
* reaches the bottom.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} settings Storage for the cutscene from ScenePlayr.
|
|
*/
|
|
cutsceneFlagpoleStartSlidingDown(FSM: FullScreenMario, settings: any): void {
|
|
var thing: IPlayer = settings.player,
|
|
other: IDetectCollision = settings.collider,
|
|
height: number = (other.bottom - thing.bottom) | 0,
|
|
scoreAmount: number = FSM.scorePlayerFlag(
|
|
thing, height / FSM.unitsize),
|
|
scoreThing: IText = FSM.ObjectMaker.make("Text" + scoreAmount);
|
|
|
|
// This is a cutscene. No movement, no deaths, no scrolling.
|
|
thing.star = 1;
|
|
thing.nocollidechar = true;
|
|
FSM.MapScreener.nokeys = true;
|
|
FSM.MapScreener.notime = true;
|
|
FSM.MapScreener.canscroll = false;
|
|
|
|
// Kill all other characters and pause the player next to the pole
|
|
FSM.killNPCs();
|
|
FSM.thingPauseVelocity(thing);
|
|
FSM.setRight(thing, other.left + FSM.unitsize * 3);
|
|
FSM.killNormal(other);
|
|
|
|
// The player is now climbing down the pole
|
|
FSM.removeClasses(thing, "running jumping skidding");
|
|
FSM.addClass(thing, "climbing animated");
|
|
FSM.TimeHandler.addClassCycle(
|
|
thing, ["one", "two"], "climbing", 0);
|
|
|
|
// Animate the Flag to the base of the pole
|
|
FSM.TimeHandler.addEventInterval(
|
|
FSM.shiftVert,
|
|
1,
|
|
64,
|
|
other.collection.Flag,
|
|
FSM.unitsize);
|
|
|
|
// Add a ScoreText element at the bottom of the flag and animate it up
|
|
FSM.addThing(scoreThing, other.right, other.bottom);
|
|
FSM.TimeHandler.addEventInterval(
|
|
FSM.shiftVert,
|
|
1,
|
|
72,
|
|
scoreThing,
|
|
-FSM.unitsize);
|
|
FSM.TimeHandler.addEvent(
|
|
FSM.ItemsHolder.increase.bind(FSM.ItemsHolder),
|
|
72,
|
|
"score",
|
|
scoreAmount);
|
|
|
|
// All audio stops, and the flagpole clip is played
|
|
FSM.AudioPlayer.clearAll();
|
|
FSM.AudioPlayer.clearTheme();
|
|
FSM.AudioPlayer.play("Flagpole");
|
|
|
|
FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
// While the player hasn't reached the bottom yet, slide down
|
|
if (thing.bottom < other.bottom) {
|
|
FSM.shiftVert(thing, FSM.unitsize);
|
|
return false;
|
|
}
|
|
|
|
// If the flag hasn't reached it but the player has, don't move yet
|
|
if ((other.collection.Flag.bottom | 0) < (other.bottom | 0)) {
|
|
return false;
|
|
}
|
|
|
|
// The player is done climbing: trigger the flag bottom collision
|
|
thing.movement = undefined;
|
|
FSM.setBottom(thing, other.bottom);
|
|
FSM.TimeHandler.cancelClassCycle(thing, "climbing");
|
|
FSM.TimeHandler.addEvent(
|
|
FSM.ScenePlayer.bindRoutine("HitBottom"),
|
|
21);
|
|
|
|
return true;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
/**
|
|
* Routine for when a player hits the bottom of a flagpole. It is
|
|
* flipped horizontally, shifted to the other side of the pole, and the
|
|
* animatePlayerOffPole callback is quickly timed.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} settings Storage for the cutscene from ScenePlayr.
|
|
*/
|
|
cutsceneFlagpoleHitBottom(FSM: FullScreenMario, settings: any): void {
|
|
var thing: IPlayer = settings.player;
|
|
|
|
thing.keys.run = 1;
|
|
thing.maxspeed = thing.walkspeed;
|
|
|
|
thing.FSM.flipHoriz(thing);
|
|
thing.FSM.shiftHoriz(
|
|
thing,
|
|
(thing.width + 1) * thing.FSM.unitsize);
|
|
|
|
thing.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
thing.FSM.AudioPlayer.play("Stage Clear");
|
|
thing.FSM.animatePlayerOffPole(thing, true);
|
|
},
|
|
14);
|
|
}
|
|
|
|
/**
|
|
* Routine for counting down time and increasing score at the end of
|
|
* a level. When it's done, it calls the Fireworks routine.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} settings Storage for the cutscene from ScenePlayr.
|
|
*/
|
|
cutsceneFlagpoleCountdown(FSM: FullScreenMario, settings: any): void {
|
|
FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
FSM.ItemsHolder.decrease("time");
|
|
FSM.ItemsHolder.increase("score", 50);
|
|
FSM.AudioPlayer.play("Coin");
|
|
|
|
if (FSM.ItemsHolder.getItem("time") > 0) {
|
|
return false;
|
|
}
|
|
|
|
FSM.TimeHandler.addEvent(FSM.ScenePlayer.bindRoutine("Fireworks"), 35);
|
|
return true;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
/**
|
|
* Animation routine for the fireworks found at the end of EndOutsideCastle.
|
|
* Fireworks are added on a timer (if there should be any), and the level
|
|
* transport is called when any fireworks are done.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} settings Storage for the cutscene from ScenePlayr.
|
|
*/
|
|
cutsceneFlagpoleFireworks(FSM: FullScreenMario, settings: any): void {
|
|
var numFireworks: number = FSM.MathDecider.compute("numberOfFireworks", settings.time),
|
|
player: IPlayer = settings.player,
|
|
detector: IDetectCollision = settings.detector,
|
|
doorRight: number = detector.left,
|
|
doorLeft: number = doorRight - FSM.unitsize * 8,
|
|
doorBottom: number = detector.bottom,
|
|
doorTop: number = doorBottom - FSM.unitsize * 16,
|
|
flag: IThing = FSM.ObjectMaker.make("CastleFlag", {
|
|
"position": "beginning"
|
|
}),
|
|
flagMovements: number = 28,
|
|
fireInterval: number = 28,
|
|
fireworkPositions: number[][] = [
|
|
[0, -48],
|
|
[-8, -40],
|
|
[8, -40],
|
|
[-8, -32],
|
|
[0, -48],
|
|
[-8, -40]
|
|
],
|
|
i: number = 0,
|
|
firework: IFirework,
|
|
position: number[];
|
|
|
|
// Add a flag to the center of the castle, behind everything else
|
|
FSM.addThing(
|
|
flag,
|
|
doorLeft + FSM.unitsize,
|
|
doorTop - FSM.unitsize * 24);
|
|
FSM.arrayToBeginning(flag, <any[]>FSM.GroupHolder.getGroup(flag.groupType));
|
|
|
|
// Animate the flag raising
|
|
FSM.TimeHandler.addEventInterval(
|
|
function (): void {
|
|
FSM.shiftVert(flag, FSM.unitsize * -.25);
|
|
},
|
|
1,
|
|
flagMovements);
|
|
|
|
// If there should be fireworks, add each of them on an interval
|
|
if (numFireworks > 0) {
|
|
FSM.TimeHandler.addEventInterval(
|
|
function (): void {
|
|
position = fireworkPositions[i];
|
|
firework = <IFirework>FSM.addThing(
|
|
"Firework",
|
|
player.left + position[0] * FSM.unitsize,
|
|
player.top + position[1] * FSM.unitsize);
|
|
firework.animate(firework);
|
|
i += 1;
|
|
},
|
|
fireInterval,
|
|
numFireworks);
|
|
}
|
|
|
|
// After everything, activate the detector's transport to leave
|
|
FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
FSM.AudioPlayer.addEventImmediate(
|
|
"Stage Clear", "ended", function (): void {
|
|
FSM.collideLevelTransport(player, detector);
|
|
FSM.ScenePlayer.stopCutscene();
|
|
});
|
|
},
|
|
i * fireInterval + 420);
|
|
}
|
|
|
|
/**
|
|
* Routine for when a player collides with a castle axe. All unimportant NPCs
|
|
* are killed and the player running again is scheduled.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} settings Storage for the cutscene from ScenePlayr.
|
|
*/
|
|
cutsceneBowserVictoryCollideCastleAxe(FSM: FullScreenMario, settings: any): void {
|
|
var player: IPlayer = settings.player,
|
|
axe: ICastleAxe = settings.axe;
|
|
|
|
FSM.thingPauseVelocity(player);
|
|
FSM.killNormal(axe);
|
|
FSM.killNPCs();
|
|
|
|
FSM.AudioPlayer.clearTheme();
|
|
FSM.MapScreener.nokeys = true;
|
|
FSM.MapScreener.notime = true;
|
|
|
|
player.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
player.keys.run = 1;
|
|
player.maxspeed = player.walkspeed;
|
|
FSM.thingResumeVelocity(player);
|
|
|
|
player.yvel = 0;
|
|
FSM.MapScreener.canscroll = true;
|
|
FSM.AudioPlayer.play("World Clear");
|
|
},
|
|
140);
|
|
}
|
|
|
|
/**
|
|
* Routine for a castle bridge opening. Its width is reduced repeatedly on an
|
|
* interval until it's 0, at which point the BowserFalls routine plays.
|
|
*
|
|
* @param {FullScreenMario} FSM
|
|
* @param {Object} settings Storage for the cutscene from ScenePlayr.
|
|
* @remarks The castle bridge's animateCastleBridgeOpen (called via killNPCs
|
|
* as the bridge's .killonend attribute) is what triggers this.
|
|
*/
|
|
cutsceneBowserVictoryCastleBridgeOpen(FSM: FullScreenMario, settings: any): void {
|
|
var bridge: ISolid = settings.routineArguments[0];
|
|
|
|
FSM.TimeHandler.addEventInterval(
|
|
function (): boolean {
|
|
bridge.right -= FSM.unitsize * 2;
|
|
FSM.setWidth(bridge, bridge.width - 2);
|
|
FSM.AudioPlayer.play("Break Block");
|
|
|
|
if (bridge.width <= 0) {
|
|
FSM.ScenePlayer.playRoutine("BowserFalls");
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
1,
|
|
Infinity);
|
|
}
|
|
|
|
/**
|
|
* Routine for Bowser falling after his bridge opens.
|
|
*
|
|
* @param {Object} settings Storage for the cutscene from ScenePlayr.
|
|
* @param {FullScreenMario} FSM
|
|
* @remarks This is called by the CastleBridgeOpen routine, once the bridge
|
|
* has been reduced to no width.
|
|
*/
|
|
cutsceneBowserVictoryBowserFalls(FSM: FullScreenMario, settings: any): void {
|
|
FSM.AudioPlayer.play("Bowser Falls");
|
|
settings.bowser.nofall = true;
|
|
}
|
|
|
|
/**
|
|
* Routine for displaying text above a castle NPC. Each "layer" of text
|
|
* is added in order, after which collideLevelTransport is called.
|
|
*
|
|
* @param {Object} settings Storage for the cutscene from ScenePlayr.
|
|
* @param {FullScreenMario} FSM
|
|
* @remarks This is called by collideCastleNPC.
|
|
*/
|
|
cutsceneBowserVictoryDialog(FSM: FullScreenMario, settings: any): void {
|
|
var player: IPlayer = settings.player,
|
|
detector: IDetectCollision = settings.detector,
|
|
keys: any[] = settings.keys,
|
|
interval: number = 140,
|
|
i: number = 0,
|
|
j: number,
|
|
letters: IThing[];
|
|
|
|
player.keys.run = 0;
|
|
player.FSM.killNormal(detector);
|
|
|
|
player.FSM.TimeHandler.addEventInterval(
|
|
function (): void {
|
|
letters = detector.collection[keys[i]].children;
|
|
|
|
for (j = 0; j < letters.length; j += 1) {
|
|
if (letters[j].title !== "TextSpace") {
|
|
letters[j].hidden = false;
|
|
}
|
|
}
|
|
|
|
i += 1;
|
|
},
|
|
interval,
|
|
keys.length);
|
|
|
|
player.FSM.TimeHandler.addEvent(
|
|
function (): void {
|
|
player.FSM.collideLevelTransport(player, detector);
|
|
},
|
|
280 + interval * keys.length);
|
|
}
|
|
|
|
|
|
/* Map macros
|
|
*/
|
|
|
|
/**
|
|
* Sample macro with no functionality, except to console.log a listing of
|
|
* the arguments provided to each macro function.
|
|
* For all real macros, arguments are listed as the keys given as members of
|
|
* the reference object.
|
|
* They also ignore the "x" and "y" arguments, which
|
|
* are the x-location and y-location of the output (and both default to 0),
|
|
* and the "macro" argument, which is listed as their alias.
|
|
*
|
|
* @alias Example
|
|
* @param {Object} reference A listing of the settings for this macro,
|
|
* from an Area's .creation Array. This should
|
|
* be treated as const!
|
|
* @param {Object[]} prethings The Area's actual .creation Array, which
|
|
* consists of a bunch of reference Objects.
|
|
* @param {Area} area The area currently being generated.
|
|
* @param {Map} map The map containing the area currently being generated.
|
|
*/
|
|
macroExample(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
console.log("This is a macro that may be called by a map creation.");
|
|
console.log("The arguments are:\n");
|
|
console.log("Reference (the listing from area.creation): ", reference);
|
|
console.log("Prethings (the area's listing of prethings): ", prethings);
|
|
console.log("Area (the currently generated area): ", area);
|
|
console.log("Map (the map containing the area): ", map);
|
|
console.log("Scope (the custom scope container): ", scope);
|
|
}
|
|
|
|
/**
|
|
* Macro to place a single type of Thing multiple times, drawing from a
|
|
* bottom/left corner to a top/right corner.
|
|
*
|
|
* @alias Fill
|
|
* @param {String} thing The name of the Thing to fill (e.g. "Brick").
|
|
* @param {Number} xnum How many times to repeat the Thing horizontally
|
|
* to the right (defaults to 1)
|
|
* @param {Number} ynum How many times to repeat the Thing vertically
|
|
* upwards (defaults to 1)
|
|
* @param {Number} xwidth How many units are between the left edges of
|
|
* placed Things horizontally (defaults to 0)
|
|
* @param {Number} yheight How many units are between the top edges of
|
|
* placed Things vertically (defaults to 0)
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @example { "macro": "Fill", "thing": "Brick",
|
|
* "x": 644, "y": 64, "xnum": 5, "xwidth": 8 }
|
|
* @return {Object[]}
|
|
*/
|
|
macroFillPreThings(
|
|
reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
var defaults: any = scope.ObjectMaker.getFullPropertiesOf(reference.thing),
|
|
xnum: number = reference.xnum || 1,
|
|
ynum: number = reference.ynum || 1,
|
|
xwidth: number = reference.xwidth || defaults.width,
|
|
yheight: number = reference.yheight || defaults.height,
|
|
x: number = reference.x || 0,
|
|
yref: number = reference.y || 0,
|
|
outputs: any[] = [],
|
|
output: any,
|
|
o: number = 0,
|
|
y: number,
|
|
i: number,
|
|
j: number;
|
|
|
|
for (i = 0; i < xnum; ++i) {
|
|
y = yref;
|
|
for (j = 0; j < ynum; ++j) {
|
|
output = {
|
|
"x": x,
|
|
"y": y,
|
|
"macro": undefined
|
|
};
|
|
outputs.push(FullScreenMario.prototype.proliferate(output, reference, true));
|
|
o += 1;
|
|
y += yheight;
|
|
}
|
|
x += xwidth;
|
|
}
|
|
|
|
return outputs;
|
|
}
|
|
|
|
/**
|
|
* Macro to continuously place a listing of Things multiple times, from left
|
|
* to right. This is commonly used for repeating background scenery.
|
|
*
|
|
* @alias Pattern
|
|
* @param {String} pattern The name of the pattern to print, from the
|
|
* listing in scope.settings.maps.patterns.
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Number} [repeat] How many times to repeat the overall pattern
|
|
* (by default, 1).
|
|
* @param {Number[]} [skips] Which numbered items to skip, if any.
|
|
* @return {Object[]}
|
|
*/
|
|
macroFillPrePattern(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
// Make sure the pattern exists before doing anything
|
|
if (!scope.settings.maps.patterns[reference.pattern]) {
|
|
console.warn("An unknown pattern is referenced: " + reference);
|
|
return;
|
|
}
|
|
var pattern: any = scope.settings.maps.patterns[reference.pattern],
|
|
length: number = pattern.length,
|
|
defaults: any = scope.ObjectMaker.getFullProperties(),
|
|
repeats: number = reference.repeat || 1,
|
|
xpos: number = reference.x || 0,
|
|
ypos: number = reference.y || 0,
|
|
outputs: any[] = [],
|
|
o: number = 0,
|
|
skips: any = {},
|
|
prething: any,
|
|
output: any,
|
|
i: number,
|
|
j: number;
|
|
|
|
// If skips are given, record them in an Object for quick access
|
|
if (typeof reference.skips !== "undefined") {
|
|
for (i = 0; i < reference.skips.length; i += 1) {
|
|
skips[reference.skips[i]] = true;
|
|
}
|
|
}
|
|
|
|
// For each time the pattern should be repeated:
|
|
for (i = 0; i < repeats; i += 1) {
|
|
// For each Thing listing in the pattern:
|
|
for (j = 0; j < length; j += 1) {
|
|
// Don't place if marked in skips
|
|
if (skips[j]) {
|
|
continue;
|
|
}
|
|
|
|
prething = pattern[j];
|
|
output = {
|
|
"thing": prething[0],
|
|
"x": xpos + prething[1],
|
|
"y": ypos + prething[2]
|
|
};
|
|
output.y += defaults[prething[0]].height;
|
|
|
|
if (prething[3]) {
|
|
output.width = prething[3];
|
|
}
|
|
|
|
outputs.push(output);
|
|
o += 1;
|
|
}
|
|
xpos += pattern.width;
|
|
}
|
|
|
|
return outputs;
|
|
}
|
|
|
|
/**
|
|
* Macro to place a Floor Thing with infinite height. All settings are
|
|
* passed in except "macro", which becomes undefined.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Number} [width] How wide the Floor should be (by default, 8).
|
|
* @return {Object}
|
|
*/
|
|
macroFloor(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
var x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
floor: any = FullScreenMario.prototype.proliferate(
|
|
{
|
|
"thing": "Floor",
|
|
"x": x,
|
|
"y": y,
|
|
"width": (reference.width || 8),
|
|
"height": "Infinity"
|
|
},
|
|
reference,
|
|
true);
|
|
|
|
floor.macro = undefined;
|
|
|
|
return floor;
|
|
}
|
|
|
|
/**
|
|
* Macro to place a Pipe, possibly with a pirahna, location hooks, and/or
|
|
* infinite height. All settings are copied to Pipe except for "macro",
|
|
* which becomes undefined.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Mixed} [height] How high the Pipe should be (by default, 8).
|
|
* May be a Number or "Infinity".
|
|
* @param {Boolean} [piranha] Whethere there should be a Piranha spawned
|
|
* with the Pipe (by default, false).
|
|
* @param {Mixed} [transport] What location the Pipe should transport to
|
|
* (by default, none).
|
|
* @param {Mixed} [entrance] What location the Pipe should act as an
|
|
* entrance to (by default, none).
|
|
* @return {Object[]}
|
|
*/
|
|
macroPipe(reference: any, prethings: any[], area: IArea, map: IMap, scope: any): any {
|
|
var x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
height: number | string = reference.height || 16,
|
|
pipe: any = FullScreenMario.prototype.proliferate(
|
|
{
|
|
"thing": "Pipe",
|
|
"x": x,
|
|
"y": y,
|
|
"width": 16,
|
|
"height": reference.height === Infinity
|
|
? "Infinity"
|
|
: reference.height || 8
|
|
},
|
|
reference,
|
|
true),
|
|
output: any[] = [pipe];
|
|
|
|
pipe.macro = undefined;
|
|
|
|
if (height === "Infinity" || height === Infinity) {
|
|
pipe.height = scope.MapScreener.height;
|
|
} else {
|
|
pipe.y += height;
|
|
}
|
|
|
|
if (reference.piranha) {
|
|
output.push({
|
|
"thing": "Piranha",
|
|
"x": x + 4,
|
|
"y": pipe.y + 12,
|
|
"onPipe": true
|
|
});
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place a horizontal Pipe with a vertical one, likely with
|
|
* location hooks.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Mixed} [height] How high the Pipe should be (by default, 8).
|
|
* May be a Number or "Infinity".
|
|
* @param {Mixed} [transport] What location the Pipe should transport to
|
|
* (by default, none).
|
|
* @param {Boolean} [scrollEnabler] Whether there should be a
|
|
* ScrollEnabler placed on top of the
|
|
* PipeVertical (by default, false).
|
|
* @param {Boolean} [scrollBlocker] Whether there should be a
|
|
* ScrollBlocker placed to the right of
|
|
* the PipeVertical (by default, false).
|
|
* @return {Object[]}
|
|
* @remarks This could be used in maps like 1-2, but there's no real need to
|
|
* take the time (unless you're a volunteer and want something to
|
|
* do!). It was introduced for WorldSeedr generation.
|
|
*/
|
|
macroPipeCorner(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
var x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
height: number = reference.height || 16,
|
|
output: any[] = [
|
|
{
|
|
"thing": "PipeHorizontal",
|
|
"x": x,
|
|
"y": y,
|
|
"transport": reference.transport || 0
|
|
},
|
|
{
|
|
"thing": "PipeVertical",
|
|
"x": x + 16,
|
|
"y": y + height - 16,
|
|
"height": height
|
|
}
|
|
];
|
|
|
|
if (reference.scrollEnabler) {
|
|
output.push({
|
|
"thing": "ScrollEnabler",
|
|
"x": x + 16,
|
|
"y": y + height + 48,
|
|
"height": 64,
|
|
"width": 16
|
|
});
|
|
}
|
|
|
|
if (reference.scrollBlocker) {
|
|
output.push({
|
|
"thing": "ScrollBlocker",
|
|
"x": x + 32
|
|
});
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place a large Tree.
|
|
*
|
|
* @param {Number} width How wide the Tree should be (preferably a
|
|
* multiple of eight
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Boolean} [solidTrunk] Whether the trunk scenery should be
|
|
* listed in the Solids group instead of
|
|
* Scenery for the sake of overlaps (by
|
|
* default, false).
|
|
* @return {Object[]}
|
|
* @remarks Although the tree trunks in later trees overlap earlier ones,
|
|
* it's ok because the pattern is indistinguishible when placed
|
|
* correctly.
|
|
*/
|
|
macroTree(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
var x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
width: number = reference.width || 24,
|
|
output: any[] = [
|
|
{
|
|
"thing": "TreeTop",
|
|
"x": x,
|
|
"y": y,
|
|
"width": width
|
|
}
|
|
];
|
|
|
|
if (width > 16) {
|
|
output.push({
|
|
"thing": "TreeTrunk",
|
|
"x": x + 8,
|
|
"y": y - 8,
|
|
"width": width - 16,
|
|
"height": "Infinity",
|
|
"groupType": reference.solidTrunk ? "Solid" : "Scenery"
|
|
});
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place a large Shroom (a Tree that looks like a large Mushroom).
|
|
*
|
|
* @param {Number} width How wide the Shroom should be (preferably a
|
|
* multiple of eight).
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Boolean} [solidTrunk] Whether the trunk scenery should be
|
|
* listed in the Solids group instead of
|
|
* Scenery for the sake of overlaps (by
|
|
* default, false).
|
|
* @return {Object[]}
|
|
* @remarks Although the shroom trunks in later shrooms overlap earlier
|
|
* ones, it's ok because the pattern is indistinguishible when
|
|
* placed correctly.
|
|
*/
|
|
macroShroom(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
var x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
width: number = reference.width || 24,
|
|
output: any[] = [
|
|
{
|
|
"thing": "ShroomTop",
|
|
"x": x,
|
|
"y": y,
|
|
"width": width
|
|
}
|
|
];
|
|
|
|
if (width > 16) {
|
|
output.push({
|
|
"thing": "ShroomTrunk",
|
|
"x": x + (width - 8) / 2,
|
|
"y": y - 8,
|
|
"height": Infinity,
|
|
"groupType": reference.solidTrunk ? "Solid" : "Scenery"
|
|
});
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place Water of infinite height. All settings are copied to the
|
|
* Water except for "macro", which becomes undefined.
|
|
*
|
|
* @param {Number} width How wide the Water should be.
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @return {Object}
|
|
*/
|
|
macroWater(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
var x: number = reference.x || 0,
|
|
y: number = (reference.y || 0) + 2, // water is 3.5 x 5.5
|
|
output: any = FullScreenMario.prototype.proliferate(
|
|
{
|
|
"thing": "Water",
|
|
"x": x,
|
|
"y": y,
|
|
"height": "Infinity",
|
|
"macro": undefined
|
|
},
|
|
reference,
|
|
true);
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place a row of Bricks at y = 88.
|
|
*
|
|
* @param {Number} width How wide the ceiling should be (eight times the
|
|
* number of Bricks).
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @return {Object}
|
|
*/
|
|
macroCeiling(reference: any): any {
|
|
return {
|
|
"macro": "Fill",
|
|
"thing": "Brick",
|
|
"x": reference.x,
|
|
"y": 88,
|
|
"xnum": (reference.width / 8) | 0,
|
|
"xwidth": 8
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Macro to place a bridge, possibly with columns at the start and/or end.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Number} [width] How wide the bridge should be (by default, 16).
|
|
* @param {Boolean} [begin] Whether the first 8 units should be taken up
|
|
* by an infinitely high Stone column (by
|
|
* default, false).
|
|
* @param {Boolean} [end] Whether the last 8 units should be taken up by
|
|
* an infinitely high Stone column (by default,
|
|
* false).
|
|
* @return {Object[]}
|
|
*/
|
|
macroBridge(reference: any): any {
|
|
var x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
width: number = Math.max(reference.width || 0, 16),
|
|
output: any[] = [];
|
|
|
|
// A beginning column reduces the width and pushes it forward
|
|
if (reference.begin) {
|
|
width -= 8;
|
|
output.push({
|
|
"thing": "Stone",
|
|
"x": x,
|
|
"y": y,
|
|
"height": "Infinity"
|
|
});
|
|
x += 8;
|
|
}
|
|
|
|
// An ending column just reduces the width
|
|
if (reference.end) {
|
|
width -= 8;
|
|
output.push({
|
|
"thing": "Stone",
|
|
"x": x + width,
|
|
"y": y,
|
|
"height": "Infinity"
|
|
});
|
|
}
|
|
|
|
// Between any columns is a BridgeBase with a Railing on top
|
|
output.push({ "thing": "BridgeBase", "x": x, "y": y, "width": width });
|
|
output.push({ "thing": "Railing", "x": x, "y": y + 4, "width": width });
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place a scale on the map, which is two Platforms seemingly
|
|
* suspended by Strings.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Number} [widthLeft] How wide the left Platform should be (by
|
|
* default, 24).
|
|
* @param {Number} [widthRight] How wide the right Platform should be (by
|
|
* default, 24).
|
|
* @param {Number} [between] How much space there should be between
|
|
* Platforms (by default, 40).
|
|
* @param {Number} [dropLeft] How far down from y the left platform should
|
|
* start (by default, 24).
|
|
* @param {Number} [dropRight] How far down from y the right platform
|
|
* should start (by default, 24).
|
|
* @return {Object[]}
|
|
*/
|
|
macroScale(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
var x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
unitsize: number = scope.unitsize,
|
|
widthLeft: number = reference.widthLeft || 24,
|
|
widthRight: number = reference.widthRight || 24,
|
|
between: number = reference.between || 40,
|
|
dropLeft: number = reference.dropLeft || 24,
|
|
dropRight: number = reference.dropRight || 24,
|
|
collectionName: string = "ScaleCollection--" + [
|
|
x, y, widthLeft, widthRight, dropLeft, dropRight
|
|
].join(","),
|
|
stringLeft: any = {
|
|
"thing": "String",
|
|
"x": x,
|
|
"y": y - 4,
|
|
"height": dropLeft - 4,
|
|
"collectionName": collectionName,
|
|
"collectionKey": "stringLeft"
|
|
},
|
|
stringRight: any = {
|
|
"thing": "String",
|
|
"x": x + between,
|
|
"y": y - 4,
|
|
"height": dropRight - 4,
|
|
"collectionName": collectionName,
|
|
"collectionKey": "stringRight"
|
|
},
|
|
stringMiddle: any = {
|
|
"thing": "String",
|
|
"x": x + 4,
|
|
"y": y,
|
|
"width": between - 7,
|
|
"collectionName": collectionName,
|
|
"collectionKey": "stringMiddle"
|
|
},
|
|
cornerLeft: any = {
|
|
"thing": "StringCornerLeft",
|
|
"x": x,
|
|
"y": y
|
|
},
|
|
cornerRight: any = {
|
|
"thing": "StringCornerRight",
|
|
"x": x + between - 4,
|
|
"y": y
|
|
},
|
|
platformLeft: any = {
|
|
"thing": "Platform",
|
|
"x": x - (widthLeft / 2),
|
|
"y": y - dropLeft,
|
|
"width": widthLeft,
|
|
"inScale": true,
|
|
"tension": (dropLeft - 1.5) * unitsize,
|
|
"onThingAdd": scope.spawnScalePlatform,
|
|
"collectionName": collectionName,
|
|
"collectionKey": "platformLeft"
|
|
},
|
|
platformRight: any = {
|
|
"thing": "Platform",
|
|
"x": x + between - (widthRight / 2),
|
|
"y": y - dropRight,
|
|
"width": widthRight,
|
|
"inScale": true,
|
|
"tension": (dropRight - 1.5) * unitsize,
|
|
"onThingAdd": scope.spawnScalePlatform,
|
|
"collectionName": collectionName,
|
|
"collectionKey": "platformRight"
|
|
};
|
|
|
|
return [
|
|
stringLeft,
|
|
stringRight,
|
|
stringMiddle,
|
|
cornerLeft,
|
|
cornerRight,
|
|
platformLeft,
|
|
platformRight
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Macro to place what appears to be a PlatformGenerator on the map (in
|
|
* actuality, it is multiple Platforms vertically that know how to respawn).
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [direction] What direction to travel (either -1 or 1;
|
|
* defaults to 1).
|
|
* @param {Number} [width] How wide the Platforms should be (by default,
|
|
* 16).
|
|
* @return {Object[]}
|
|
*/
|
|
macroPlatformGenerator(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
var output: any[] = [],
|
|
direction: number = reference.direction || 1,
|
|
levels: number[] = direction > 0 ? [0, 48] : [8, 56],
|
|
width: number = reference.width || 16,
|
|
x: number = reference.x || 0,
|
|
yvel: number = direction * scope.unitsize * .42,
|
|
i: number;
|
|
|
|
for (i = 0; i < levels.length; i += 1) {
|
|
output.push({
|
|
"thing": "Platform",
|
|
"x": x,
|
|
"y": levels[i],
|
|
"width": width,
|
|
"yvel": yvel,
|
|
"movement": scope.movePlatformSpawn
|
|
});
|
|
}
|
|
|
|
output.push({
|
|
"thing": "PlatformString",
|
|
"x": x + (width / 2) - .5,
|
|
"y": scope.MapScreener.floor,
|
|
"width": 1,
|
|
"height": scope.MapScreener.height / scope.unitsize
|
|
});
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place a Warp World group of Pipes, Texts, Piranhas, and
|
|
* detectors.
|
|
*
|
|
* @param {String[]} warps The map names each Pipe should warp to.
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Number} [textHeight] How far above the Piranhas to place the
|
|
* CustomText labels (by default, 8).
|
|
*
|
|
* @return {Object[]}
|
|
*/
|
|
macroWarpWorld(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
var output: any[] = [],
|
|
x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
textHeight: number = reference.hasOwnProperty("textHeight") ? reference.textHeight : 8,
|
|
warps: string[] = reference.warps,
|
|
collectionName: string = "WarpWorldCollection-" + warps.join("."),
|
|
keys: number[] = [],
|
|
i: number;
|
|
|
|
output.push({
|
|
"thing": "CustomText",
|
|
"x": x + 8,
|
|
"y": y + textHeight + 56,
|
|
"texts": [{
|
|
"text": "WELCOME TO WARP WORLD!"
|
|
}],
|
|
"textAttributes": {
|
|
"hidden": true
|
|
},
|
|
"collectionName": collectionName,
|
|
"collectionKey": "Welcomer"
|
|
});
|
|
|
|
output.push({
|
|
"thing": "DetectCollision",
|
|
"x": x + 64,
|
|
"y": y + 174,
|
|
"width": 40,
|
|
"height": 102,
|
|
"activate": scope.activateWarpWorld,
|
|
"collectionName": collectionName,
|
|
"collectionKey": "Detector"
|
|
});
|
|
|
|
for (i = 0; i < warps.length; i += 1) {
|
|
keys.push(i);
|
|
output.push({
|
|
"macro": "Pipe",
|
|
"x": x + 8 + i * 32,
|
|
"height": 24,
|
|
"transport": { "map": warps[i] + "-1" },
|
|
"collectionName": collectionName,
|
|
"collectionKey": i + "-Pipe"
|
|
});
|
|
output.push({
|
|
"thing": "Piranha",
|
|
"x": x + 12 + i * 32,
|
|
"y": y + 36,
|
|
"collectionName": collectionName,
|
|
"collectionKey": i + "-Piranha"
|
|
});
|
|
output.push({
|
|
"thing": "CustomText",
|
|
"x": x + 14 + i * 32,
|
|
"y": y + 32 + textHeight,
|
|
"texts": [{
|
|
"text": String(warps[i])
|
|
}],
|
|
"textAttributes": {
|
|
"hidden": true
|
|
},
|
|
"collectionName": collectionName,
|
|
"collectionKey": i + "-Text"
|
|
});
|
|
}
|
|
|
|
if (warps.length === 1) {
|
|
for (i = 2; i < output.length; i += 1) {
|
|
output[i].x += 32;
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place a DetectCollision that will start the map spawning random
|
|
* CheepCheeps intermittently.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [width] How wide the infinitely tall DetectCollision
|
|
* should be (by default, 8).
|
|
* @return {Object}
|
|
*/
|
|
macroCheepsStart(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
return {
|
|
"thing": "DetectCollision",
|
|
"x": reference.x || 0,
|
|
"y": scope.MapScreener.floor,
|
|
"width": reference.width || 8,
|
|
"height": scope.MapScreener.height / scope.unitsize,
|
|
"activate": scope.activateCheepsStart
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Macro to place a DetectCollision that will stop the map spawning random
|
|
* CheepCheeps intermittently.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [width] How wide the infinitely tall DetectCollision
|
|
* should be (by default, 8).
|
|
* @return {Object}
|
|
*/
|
|
macroCheepsStop(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
return {
|
|
"thing": "DetectCollision",
|
|
"x": reference.x || 0,
|
|
"y": scope.MapScreener.floor,
|
|
"width": reference.width || 8,
|
|
"height": scope.MapScreener.height / scope.unitsize,
|
|
"activate": scope.activateCheepsStop
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Macro to place a DetectCollision that will start the map spawning random
|
|
* BulletBills intermittently.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [width] How wide the infinitely tall DetectCollision
|
|
* should be (by default, 8).
|
|
* @return {Object}
|
|
*/
|
|
macroBulletBillsStart(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
return {
|
|
"thing": "DetectCollision",
|
|
"x": reference.x || 0,
|
|
"y": scope.MapScreener.floor,
|
|
"width": reference.width || 8,
|
|
"height": scope.MapScreener.height / scope.unitsize,
|
|
"activate": scope.activateBulletBillsStart
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Macro to place a DetectCollision that will stop the map spawning random
|
|
* BulletBills intermittently.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [width] How wide the infinitely tall DetectCollision
|
|
* should be (by default, 8).
|
|
* @return {Object}
|
|
*/
|
|
macroBulletBillsStop(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
return {
|
|
"thing": "DetectCollision",
|
|
"x": reference.x || 0,
|
|
"y": scope.MapScreener.floor,
|
|
"width": reference.width || 8,
|
|
"height": scope.MapScreener.height / scope.unitsize,
|
|
"activate": scope.activateBulletBillsStop
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Macro to place a DetectCollision that will tell any current Lakitu to
|
|
* flee the scene.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [width] How wide the infinitely tall DetectCollision
|
|
* should be (by default, 8).
|
|
* @return {Object}
|
|
*/
|
|
macroLakituStop(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
return {
|
|
"thing": "DetectCollision",
|
|
"x": reference.x || 0,
|
|
"y": scope.MapScreener.floor,
|
|
"width": reference.width || 8,
|
|
"height": scope.MapScreener.height / scope.unitsize,
|
|
"activate": scope.activateLakituStop
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Macro to place a small castle, which is really a collection of sceneries.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Mixed} [transport] What map or location to shift to after
|
|
* ending theatrics (collidePlayerTransport).
|
|
* @param {Number} [walls] How many CastleWall Things should be placed to
|
|
* the right of the castle (by default, 2).
|
|
* @return {Object[]}
|
|
*/
|
|
macroCastleSmall(reference: any): any {
|
|
var output: any[] = [],
|
|
x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
i: number,
|
|
j: number;
|
|
|
|
// Base filling left
|
|
for (i = 0; i < 2; i += 1) { // x
|
|
output.push({
|
|
"thing": "BrickHalf",
|
|
"x": x + i * 8,
|
|
"y": y + 4,
|
|
"position": "end"
|
|
});
|
|
|
|
for (j = 1; j < 3; j += 1) { // y
|
|
output.push({
|
|
"thing": "BrickPlain",
|
|
"x": x + i * 8,
|
|
"y": y + 4 + j * 8,
|
|
"position": "end"
|
|
});
|
|
}
|
|
}
|
|
|
|
// Base filling right
|
|
for (i = 0; i < 2; i += 1) { // x
|
|
output.push({
|
|
"thing": "BrickHalf",
|
|
"x": x + 24 + i * 8,
|
|
"y": y + 4,
|
|
"position": "end"
|
|
});
|
|
|
|
for (j = 1; j < 3; j += 1) { // y
|
|
output.push({
|
|
"thing": "BrickPlain",
|
|
"x": x + 24 + i * 8,
|
|
"y": y + 4 + j * 8,
|
|
"position": "end"
|
|
});
|
|
}
|
|
}
|
|
|
|
// Medium railing left
|
|
output.push({
|
|
"thing": "CastleRailing",
|
|
"x": x,
|
|
"y": y + 24,
|
|
"position": "end"
|
|
});
|
|
|
|
// Medium railing center
|
|
for (i = 0; i < 3; i += 1) {
|
|
output.push({
|
|
"thing": "CastleRailingFilled",
|
|
"x": x + (i + 1) * 8,
|
|
"y": y + 24,
|
|
"position": "end"
|
|
});
|
|
}
|
|
|
|
// Medium railing right
|
|
output.push({
|
|
"thing": "CastleRailing",
|
|
"x": x + 32,
|
|
"y": y + 24,
|
|
"position": "end"
|
|
});
|
|
|
|
// Top railing
|
|
for (i = 0; i < 3; i += 1) {
|
|
output.push({
|
|
"thing": "CastleRailing",
|
|
"x": x + (i + 1) * 8,
|
|
"y": y + 40,
|
|
"position": "end"
|
|
});
|
|
}
|
|
|
|
// Top bricking
|
|
for (i = 0; i < 2; i += 1) {
|
|
output.push({
|
|
"thing": "CastleTop",
|
|
"x": x + 8 + i * 12,
|
|
"y": y + 36,
|
|
"position": "end"
|
|
});
|
|
}
|
|
|
|
// Door, and detector if required
|
|
output.push({
|
|
"thing": "CastleDoor",
|
|
"x": x + 16,
|
|
"y": y + 20,
|
|
"position": "end"
|
|
});
|
|
if (reference.transport) {
|
|
output.push({
|
|
"thing": "DetectCollision",
|
|
"x": x + 24,
|
|
"y": y + 16,
|
|
"height": 16,
|
|
"activate": FullScreenMario.prototype.collideCastleDoor,
|
|
"transport": reference.transport,
|
|
"position": "end"
|
|
});
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place a large castle, which is really a collection of sceneries
|
|
* underneath a small castle.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Mixed} [transport] What map or location to shift to after
|
|
* ending theatrics (collidePlayerTransport).
|
|
* @return {Object[]}
|
|
*/
|
|
macroCastleLarge(reference: any): any {
|
|
var output: any[] = [],
|
|
x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
i: number,
|
|
j: number;
|
|
|
|
output.push({
|
|
"macro": "CastleSmall",
|
|
"x": x + 16,
|
|
"y": y + 48
|
|
});
|
|
|
|
// CastleWalls left
|
|
for (i = 0; i < 2; i += 1) { // x
|
|
output.push({
|
|
"thing": "CastleWall",
|
|
"x": x + i * 8,
|
|
"y": y + 48
|
|
});
|
|
}
|
|
|
|
// Bottom doors with bricks on top
|
|
for (i = 0; i < 3; i += 1) { // x
|
|
output.push({
|
|
"thing": "CastleDoor",
|
|
"x": x + 16 + i * 16,
|
|
"y": y + 20,
|
|
"position": "end"
|
|
});
|
|
for (j = 0; j < 2; j += 1) {
|
|
output.push({
|
|
"thing": "BrickPlain",
|
|
"x": x + 16 + i * 16,
|
|
"y": y + 28 + j * 8
|
|
});
|
|
output.push({
|
|
"thing": "BrickHalf",
|
|
"x": x + 16 + i * 16,
|
|
"y": y + 40 + j * 4
|
|
});
|
|
}
|
|
}
|
|
|
|
// Bottom bricks with doors on top
|
|
for (i = 0; i < 2; i += 1) { // x
|
|
for (j = 0; j < 3; j += 1) { // y
|
|
output.push({
|
|
"thing": "BrickPlain",
|
|
"x": x + 24 + i * 16,
|
|
"y": y + 8 + j * 8
|
|
});
|
|
}
|
|
output.push({
|
|
"thing": "CastleDoor",
|
|
"x": x + 24 + i * 16,
|
|
"y": y + 44
|
|
});
|
|
}
|
|
|
|
// Railing (filled)
|
|
for (i = 0; i < 5; i += 1) { // x
|
|
output.push({
|
|
"thing": "CastleRailingFilled",
|
|
"x": x + 16 + i * 8,
|
|
"y": y + 48
|
|
});
|
|
}
|
|
|
|
// CastleWalls right
|
|
j = reference.hasOwnProperty("walls") ? reference.walls : 2;
|
|
for (i = 0; i < j; i += 1) { // x
|
|
output.push({
|
|
"thing": "CastleWall",
|
|
"x": x + 56 + i * 8,
|
|
"y": y + 48,
|
|
"position": "end"
|
|
});
|
|
}
|
|
|
|
if (reference.transport) {
|
|
output.push({
|
|
"thing": "DetectCollision",
|
|
"x": x + 24,
|
|
"y": y + 16,
|
|
"height": 16,
|
|
"activate": FullScreenMario.prototype.collideCastleDoor,
|
|
"transport": reference.transport,
|
|
"position": "end"
|
|
});
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place the typical starting Things for the inside of a castle
|
|
* area.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Number} [width] How wide the entire shebang should be (by
|
|
* default, 40).
|
|
* @return {Object[]}
|
|
*/
|
|
macroStartInsideCastle(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
var x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
width: number = (reference.width || 0) - 40,
|
|
output: any[] = [
|
|
{
|
|
"thing": "Stone",
|
|
"x": x,
|
|
"y": y + 48,
|
|
"width": 24,
|
|
"height": Infinity
|
|
},
|
|
{
|
|
"thing": "Stone",
|
|
"x": x + 24,
|
|
"y": y + 40,
|
|
"width": 8,
|
|
"height": Infinity
|
|
},
|
|
{
|
|
"thing": "Stone",
|
|
"x": x + 32,
|
|
"y": y + 32,
|
|
"width": 8,
|
|
"height": Infinity
|
|
}
|
|
];
|
|
|
|
if (width > 0) {
|
|
output.push({
|
|
"macro": "Floor",
|
|
"x": x + 40,
|
|
"y": y + 24,
|
|
"width": width
|
|
});
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place the typical ending Things for the inside of an outdoor
|
|
* area.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Mixed} [transport] What map or location to shift to after
|
|
* ending theatrics (collidePlayerTransport).
|
|
* @param {Boolean} [large] Whether this should place a large castle
|
|
* instead of a small (by default, false).
|
|
* @param {Number} [castleDistance] How far from the flagpole to the
|
|
* castle (by default, 24 for large
|
|
* castles and 32 for small).
|
|
* @param {Number} [walls] For large castles, how many CastleWall Things
|
|
* should be placed after (by default, 2).
|
|
* @return {Object[]}
|
|
*/
|
|
macroEndOutsideCastle(reference: any): any {
|
|
var x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
collectionName: string = "EndOutsideCastle-" + [
|
|
reference.x, reference.y, reference.large
|
|
].join(","),
|
|
output: any[];
|
|
|
|
// Output starts off with the general flag & collision detection
|
|
output = [
|
|
// Initial collision detector
|
|
{
|
|
"thing": "DetectCollision", x: x, y: y + 108, height: 100,
|
|
"activate": FullScreenMario.prototype.collideFlagpole,
|
|
"activateFail": FullScreenMario.prototype.killNormal,
|
|
"noActivateDeath": true,
|
|
"collectionName": collectionName,
|
|
"collectionKey": "DetectCollision"
|
|
},
|
|
// Flag (scenery)
|
|
{
|
|
"thing": "Flag", "x": x - 4.5, "y": y + 79.5,
|
|
"collectionName": collectionName,
|
|
"collectionKey": "Flag"
|
|
},
|
|
{
|
|
"thing": "FlagTop", "x": x + 1.5, "y": y + 84,
|
|
"collectionName": collectionName,
|
|
"collectionKey": "FlagTop"
|
|
},
|
|
{
|
|
"thing": "FlagPole", "x": x + 3, "y": y + 80,
|
|
"collectionName": collectionName,
|
|
"collectionKey": "FlagPole"
|
|
},
|
|
// Bottom stone
|
|
{
|
|
"thing": "Stone", "x": x, "y": y + 8,
|
|
"collectionName": collectionName,
|
|
"collectionKey": "FlagPole"
|
|
},
|
|
];
|
|
|
|
if (reference.large) {
|
|
output.push({
|
|
"macro": "CastleLarge",
|
|
"x": x + (reference.castleDistance || 24),
|
|
"y": y,
|
|
"transport": reference.transport,
|
|
"walls": reference.walls || 8
|
|
});
|
|
} else {
|
|
output.push({
|
|
"macro": "CastleSmall",
|
|
"x": x + (reference.castleDistance || 32),
|
|
"y": y,
|
|
"transport": reference.transport
|
|
});
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place the typical ending Things for the inside of a castle area.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Mixed} [transport] What map or location to shift to after
|
|
* ending theatrics (collidePlayerTransport).
|
|
* @param {String} [npc] Which NPC to use (either "Toad" or "Peach";
|
|
* "Toad" by default).
|
|
* @param {Boolean} [hard] Whether Bowser should be "hard" (by default,
|
|
* false).
|
|
* @param {String} [spawnType] What the Bowser's spawnType should be for
|
|
* fireball deaths (by default, "Goomba").
|
|
* @param {Boolean} [throwing] Whether the Bowser is also throwing hammers
|
|
* (by default, false).
|
|
* @param {Boolean} [topScrollEnabler] Whether a ScrollEnabler should be
|
|
* added like the ones at the end of
|
|
* large underground PipeCorners (by
|
|
* default, false).
|
|
* @return {Object[]}
|
|
*/
|
|
macroEndInsideCastle(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
var x: number = reference.x || 0,
|
|
y: number = reference.y || 0,
|
|
npc: string = reference.npc || "Toad",
|
|
output: any[],
|
|
texts: any[],
|
|
keys: string[];
|
|
|
|
if (npc === "Toad") {
|
|
keys = ["1", "2"];
|
|
texts = [
|
|
{
|
|
"thing": "CustomText",
|
|
"x": x + 164,
|
|
"y": y + 64,
|
|
"texts": [{
|
|
"text": "THANK YOU MARIO!"
|
|
}],
|
|
"textAttributes": {
|
|
"hidden": true
|
|
},
|
|
"collectionName": "endInsideCastleText",
|
|
"collectionKey": "1"
|
|
}, {
|
|
"thing": "CustomText",
|
|
"x": x + 152,
|
|
"y": y + 48,
|
|
"texts": [
|
|
{
|
|
"text": "BUT OUR PRINCESS IS IN"
|
|
}, {
|
|
"text": "ANOTHER CASTLE!"
|
|
}],
|
|
"textAttributes": {
|
|
"hidden": true
|
|
},
|
|
"collectionName": "endInsideCastleText",
|
|
"collectionKey": "2"
|
|
}];
|
|
} else if (npc === "Peach") {
|
|
keys = ["1", "2", "3"];
|
|
texts = [
|
|
{
|
|
"thing": "CustomText",
|
|
"x": x + 164,
|
|
"y": y + 64,
|
|
"texts": [{
|
|
"text": "THANK YOU MARIO!"
|
|
}],
|
|
"textAttributes": {
|
|
"hidden": true
|
|
},
|
|
"collectionName": "endInsideCastleText",
|
|
"collectionKey": "1"
|
|
}, {
|
|
"thing": "CustomText",
|
|
"x": x + 152,
|
|
"y": y + 48,
|
|
"texts": [
|
|
{
|
|
"text": "YOUR QUEST IS OVER.",
|
|
"offset": 12
|
|
}, {
|
|
"text": "WE PRESENT YOU A NEW QUEST."
|
|
}],
|
|
"textAttributes": {
|
|
"hidden": true
|
|
},
|
|
"collectionName": "endInsideCastleText",
|
|
"collectionKey": "2"
|
|
}, {
|
|
"thing": "CustomText",
|
|
"x": x + 152,
|
|
"y": 32,
|
|
"texts": [
|
|
{
|
|
"text": "PRESS BUTTON B",
|
|
"offset": 8
|
|
}, {
|
|
"text": "TO SELECT A WORLD"
|
|
}],
|
|
"textAttributes": {
|
|
"hidden": true
|
|
},
|
|
"collectionName": "endInsideCastleText",
|
|
"collectionKey": "3"
|
|
}];
|
|
}
|
|
|
|
output = [
|
|
{ "thing": "Stone", "x": x, "y": y + 88, "width": 256 },
|
|
{ "macro": "Water", "x": x, "y": y, "width": 104 },
|
|
// Bridge & Bowser area
|
|
{ "thing": "CastleBridge", "x": x, "y": y + 24, "width": 104 },
|
|
{
|
|
"thing": "Bowser", "x": x + 69, "y": y + 42,
|
|
"hard": reference.hard,
|
|
"spawnType": reference.spawnType || "Goomba",
|
|
"throwing": reference.throwing
|
|
},
|
|
{ "thing": "CastleChain", "x": x + 96, "y": y + 32 },
|
|
// Axe area
|
|
{ "thing": "CastleAxe", "x": x + 104, "y": y + 40 },
|
|
{ "thing": "ScrollBlocker", "x": x + 112 },
|
|
{ "macro": "Floor", "x": x + 104, "y": y, "width": 152 },
|
|
{
|
|
"thing": "Stone", "x": x + 104, "y": y + 32,
|
|
"width": 24, "height": 32
|
|
},
|
|
{
|
|
"thing": "Stone", "x": x + 112, "y": y + 80,
|
|
"width": 16, "height": 24
|
|
},
|
|
// Peach's Magical Happy Chamber of Fantastic Love
|
|
{
|
|
"thing": "DetectCollision", "x": x + 180,
|
|
"activate": scope.collideCastleNPC,
|
|
"transport": reference.transport,
|
|
"collectionName": "endInsideCastleText",
|
|
"collectionKey": "npc",
|
|
"collectionKeys": keys
|
|
},
|
|
{ "thing": npc, "x": x + 200, "y": 13 },
|
|
{ "thing": "ScrollBlocker", "x": x + 256 }
|
|
];
|
|
|
|
if (reference.topScrollEnabler) {
|
|
output.push({
|
|
"thing": "ScrollEnabler",
|
|
"x": x + 96, "y": y + 140,
|
|
"height": 52, "width": 16
|
|
});
|
|
output.push({
|
|
"thing": "ScrollEnabler",
|
|
"x": x + 240, "y": y + 140,
|
|
"height": 52, "width": 16
|
|
});
|
|
}
|
|
|
|
output.push.apply(output, texts);
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Macro to place a DetectSpawn that will call activateSectionBefore to
|
|
* start a stretch section.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Number} [section] Which of the area's sections to spawn (by
|
|
* default, 0).
|
|
* @return {Object}
|
|
*/
|
|
macroSection(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
return {
|
|
"thing": "DetectSpawn",
|
|
"x": reference.x || 0,
|
|
"y": reference.y || 0,
|
|
"activate": scope.activateSectionBefore,
|
|
"section": reference.section || 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Macro to place a DetectCollision to mark the current section as passed.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Number} [width] How wide the DetectCollision should be (by
|
|
* default, 8).
|
|
* @param {Number} [height] How high the DetectCollision should be (by
|
|
* default, 8).
|
|
* @return {Object}
|
|
*/
|
|
macroSectionPass(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
return {
|
|
"thing": "DetectCollision",
|
|
"x": reference.x || 0,
|
|
"y": reference.y || 0,
|
|
"width": reference.width || 8,
|
|
"height": reference.height || 8,
|
|
"activate": function (thing: IThing): void {
|
|
thing.FSM.AudioPlayer.play("Coin");
|
|
thing.FSM.MapScreener.sectionPassed = true;
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Macro to place a DetectCollision to mark the current section as failed.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Number} [width] How wide the DetectCollision should be (by
|
|
* default, 8).
|
|
* @param {Number} [height] How high the DetectCollision should be (by
|
|
* default, 8).
|
|
* @return {Object}
|
|
*/
|
|
macroSectionFail(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
return [
|
|
{
|
|
"thing": "DetectCollision",
|
|
"x": reference.x,
|
|
"y": reference.y,
|
|
"width": reference.width || 8,
|
|
"height": reference.height || 8,
|
|
"activate": function (thing: IThing): void {
|
|
thing.FSM.AudioPlayer.play("Fail");
|
|
thing.FSM.MapScreener.sectionPassed = false;
|
|
}
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Macro to place a DetectSpawn that will spawn a following section based on
|
|
* whether the current one was marked as passed or failed.
|
|
*
|
|
* @param {Number} [x] The x-location (defaults to 0).
|
|
* @param {Number} [y] The y-location (defaults to 0).
|
|
* @param {Number} [pass] Which section to spawn if passed (by default,
|
|
* 0).
|
|
* @param {Number} [fail] Which section to spawn if failed (by default,
|
|
* 0).
|
|
* @return {Object}
|
|
*/
|
|
macroSectionDecider(reference: any,
|
|
prethings: any[],
|
|
area: IArea,
|
|
map: IMap,
|
|
scope: any): any {
|
|
return {
|
|
"thing": "DetectSpawn",
|
|
"x": reference.x || 0,
|
|
"y": reference.y || 0,
|
|
"activate": function (thing: ISectionDetector): void {
|
|
if (thing.FSM.MapScreener.sectionPassed) {
|
|
thing.section = reference.pass || 0;
|
|
} else {
|
|
thing.section = reference.fail || 0;
|
|
}
|
|
thing.FSM.activateSectionBefore(thing);
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
/* Miscellaneous utilities
|
|
*/
|
|
|
|
/**
|
|
* Ensures the current object is a GameStartr by throwing an error if it
|
|
* is not. This should be used for functions in any GameStartr descendants
|
|
* that have to call 'this' to ensure their caller is what the programmer
|
|
* expected it to be.
|
|
*
|
|
* @param {Mixed} current
|
|
*/
|
|
ensureCorrectCaller(current: any): FullScreenMario {
|
|
if (!(current instanceof FullScreenMario)) {
|
|
throw new Error("A function requires the scope ('this') to be the "
|
|
+ "manipulated FullScreenMario object. Unfortunately, 'this' is a "
|
|
+ typeof (this) + ".");
|
|
}
|
|
return current;
|
|
}
|
|
}
|
|
}
|