app/controllers/GraphicalViewSharedData.js
import Observable from '../util/Observable.js';
import Dungeon from '../dungeons/Dungeon.js';
import Moves from '../entities/creatures/moves/Moves.js';
import GameEvents from '../events/GameEvents.js';
/**
* An object for decoupling the Dungeon from views and controllers
* Holds the current Dungeon and acts as a proxy for the Dungeon's events.
* Observers are notified whenever the Dungeon fires an event *or* when the
* Dungeon itself switches.
* Additionally, this class stores information that needs to be shared between
* multiple views and controllers, such as the currently hovered tile
* @todo Consider renaming this to `UiSharedData` or similar
*/
export default class GraphicalViewSharedData extends Observable {
/**
* @param {Dungeon} [dungeon] - The initial dungeon for the views to show
*/
constructor(dungeon) {
super();
if(dungeon) {
this.setDungeon(dungeon);
}
this._targettedAbilityIndex = null;
this._targettedItemIndex = null;
this.addObserver((event) => {
// Whenever the player makes a move, cancel
// any UI flows managed by the shared data
if(event instanceof GameEvents.HumanMovingEvent) {
this.unsetAttackMode();
this.unsetTargettedAbility();
this.unsetTargettedItem();
}
});
}
/**
* Changes the contained dungeon. Will notify observers of the change
* @param {Dungeon} dungeon - The dungeon to replace the current dungeon
*/
setDungeon(dungeon) {
if(!(dungeon instanceof Dungeon)) {
throw new Error('Must pass a dungeon');
}
// Clean up old observer
if(typeof this._dungeonObserver === 'function') {
this._dungeon.removeObserver(this._dungeonObserver);
}
// Chain observer so that UI doesn't need to store direct
// reference to dungeon
dungeon.addObserver(this._dungeonObserver = (event)=>this._notifyObservers(event));
this._dungeon = dungeon;
this._notifyObservers(dungeon);
}
/**
* Gets the currently held {@link Dungeon}
* @return {Dungeon}
*/
getDungeon() {
return this._dungeon;
}
/**
* Stores the coordinates of a tile that the user has selected. Useful
* for changing views based on which tile the user is examining/hovering/ect
* @param {number} x - The x-coordinate of the tile
* @param {number} y - The y-coordinate of the tile
*/
setInspectedTile(x, y) {
if(isNaN(x) || isNaN(y)) {
throw new Error('x and y must be numbers');
}
this._inspectedTile = Object.freeze({
x: +x,
y: +y
});
this._notifyObservers();
}
/**
* Gets the tile set by {@link setInspectedTile}
* @return {object} - An object with `x` and `y` properties
*/
getInspectedTile() {
return this._inspectedTile;
}
/**
* Stores a reference to a targetted ability that the user
* has considered using. This enables views to reference the selected
* move while the user considers targets.
* @param {number} index - The position of the chosen ability within
* the player's ability list
*/
setTargettedAbility(index) {
if(!Number.isInteger(+index)) {
throw new Error('index must be an integer');
}
this._targettedAbilityIndex = +index;
const dungeon = this.getDungeon();
const player = dungeon.getPlayableCharacter();
const playerTile = dungeon.getTile(player);
const ability = player.getAbility(index);
if(!ability.isTargetted()) {
throw new Error('Ability must be targetted');
}
const potentialTargets = ability.isTargetCreature() ?
player.getVisibleEnemies(dungeon).map((enemy) => dungeon.getTile(enemy)) :
dungeon.getTiles((tile) => player.canSee(dungeon, tile));
this._abilityTargets = potentialTargets.filter((tile) => {
const move = new Moves.UseAbilityMove(playerTile, +index, tile.getX(), tile.getY());
return !move.getReasonIllegal(dungeon, player, tile);
});
if(this._abilityTargets.length === 0) {
this._abilityTargets = null;
}
this.unsetTargettedItem();
this.unsetAttackMode();
this._notifyObservers();
}
/**
* Forgets which targetted ability the user was considering using
*/
unsetTargettedAbility() {
this._targettedAbilityIndex = null;
this._abilityTargets = null;
this._notifyObservers();
}
/**
* Gets the index of the selected targetted ability, if any
* @return {number} - The index of the targetted ability the user is
* considering, or `null` if none has been selected
*/
getTargettedAbility() {
return this._targettedAbilityIndex;
}
/**
* Gets the currently focused legal ability target
* for the currently selected targetted ability, if any
* @return {Tile} - A tile that is a legal target for the targetted
* ability, or null if no ability is selected
*/
getAbilityTarget() {
return this._abilityTargets && this._abilityTargets[0];
}
/**
* Stores a reference to a targetted item that the user
* has considered using. This enables views to reference the selected
* item while the user considers targets.
* @param {number} index - The position of the chosen item within
* the player's inventory slots.
*/
setTargettedItem(index) {
if(!Number.isInteger(+index)) {
throw new Error('index must be an integer');
}
this._targettedItemIndex = +index;
const dungeon = this.getDungeon();
const player = dungeon.getPlayableCharacter();
const playerTile = dungeon.getTile(player);
const item = player.getInventory().getItem(index);
if(!item.isTargetted()) {
throw new Error('Item must be targetted');
}
const potentialTargets = item.isTargetCreature() ?
player.getVisibleEnemies(dungeon).map((enemy) => dungeon.getTile(enemy)) :
dungeon.getTiles((tile) => player.canSee(dungeon, tile));
this._itemTargets = potentialTargets.filter((tile) => {
const move = new Moves.UseItemMove(playerTile, +index, tile);
return !move.getReasonIllegal(dungeon, player);
});
if(this._itemTargets.length === 0) {
this._itemTargets = null;
}
this.unsetTargettedAbility();
this.unsetAttackMode();
this._notifyObservers();
}
/**
* Forgets which targetted item the user was considering using
*/
unsetTargettedItem() {
this._targettedItemIndex = null;
this._itemTargets = null;
this._notifyObservers();
}
/**
* Gets the index of the selected targetted item, if any
* @return {number} - The index of the targetted item the user is
* considering, or `null` if none has been selected
*/
getTargettedItem() {
return this._targettedItemIndex;
}
/**
* Gets the currently focused legal item target
* for the currently selected targetted item, if any
* @return {Tile} - A tile that is a legal target for the targetted
* item, or null if no item is selected
*/
getItemTarget() {
return this._itemTargets && this._itemTargets[0];
}
/**
* Puts this controller in attack mode so that keyboard users
* can select a target for an attack
*/
setAttackMode() {
const dungeon = this.getDungeon();
const player = dungeon.getPlayableCharacter();
const playerTile = dungeon.getTile(player);
this._attackTargets = player.getVisibleEnemies(dungeon)
.filter((enemy) => {
const tile = dungeon.getTile(enemy);
const move = new Moves.AttackMove(playerTile, tile.getX(), tile.getY());
return !move.getReasonIllegal(dungeon, player);
}).map((enemy) => dungeon.getTile(enemy));
if(this._attackTargets.length === 0) {
this._attackTargets = null;
}
this.unsetTargettedAbility();
this.unsetTargettedItem();
this._notifyObservers();
}
/**
* Returns this controller to the default mode (movement mode)
*/
unsetAttackMode() {
this._attackTargets = null;
this._notifyObservers();
}
/**
* Gets the currently focused legal attack target
* for the currently selected weapon, if any
* @return {Tile} - A tile that is a legal target for the targetted
* weapon, or null if no weapon is selected
*/
getAttackTarget() {
return this._attackTargets && this._attackTargets[0];
}
/**
* Cycles focus to the next ability, attack, or item target depending
* on what the user has selected
*/
cycleTarget(dx, dy) {
const self = this;
let arrayName, array;
['_abilityTargets', '_attackTargets', '_itemTargets'].forEach(function(name) {
if(self[name]) {
arrayName = name;
array = self[name];
}
});
if(typeof dx !== 'undefined') {
const currentTarget = array[0];
const newTarget = array.filter(function(tile) {
return Math.sign(tile.getX() - currentTarget.getX()) === dx &&
Math.sign(tile.getY() - currentTarget.getY()) === dy;
}).sort(function(tileA, tileB) {
return currentTarget.getEuclideanDistance(tileA) - currentTarget.getEuclideanDistance(tileB);
})[0];
if(newTarget) {
const index = array.indexOf(newTarget);
this[arrayName] = array.slice(index).concat(array.slice(0, index));
}
} else {
array.push(array.shift());
}
this._notifyObservers();
}
/**
* Dispatches an event to observers
*/
dispatchUIEvent(event) {
this._notifyObservers(event);
}
}