mirror of
https://github.com/doublespeakgames/adarkroom.git
synced 2026-05-28 00:01:54 +08:00
7ee96fd108
This additional content explores the aftermath of an event in The Ensign. A large dungeon, The Executioner, reveals the fate of one of the Wanderers’ most powerful weapons though multiple wings, each ending with a unique boss encounter. The fabricator allows the player to craft powerful items out of alien alloy, introducing new combat mechanics.
1110 lines
33 KiB
JavaScript
1110 lines
33 KiB
JavaScript
var World = {
|
||
RADIUS: 30,
|
||
VILLAGE_POS: [30, 30],
|
||
TILE: {
|
||
VILLAGE: 'A',
|
||
IRON_MINE: 'I',
|
||
COAL_MINE: 'C',
|
||
SULPHUR_MINE: 'S',
|
||
FOREST: ';',
|
||
FIELD: ',',
|
||
BARRENS: '.',
|
||
ROAD: '#',
|
||
HOUSE: 'H',
|
||
CAVE: 'V',
|
||
TOWN: 'O',
|
||
CITY: 'Y',
|
||
OUTPOST: 'P',
|
||
SHIP: 'W',
|
||
BOREHOLE: 'B',
|
||
BATTLEFIELD: 'F',
|
||
SWAMP: 'M',
|
||
CACHE: 'U',
|
||
EXECUTIONER: 'X'
|
||
},
|
||
TILE_PROBS: {},
|
||
LANDMARKS: {},
|
||
STICKINESS: 0.5, // 0 <= x <= 1
|
||
LIGHT_RADIUS: 2,
|
||
BASE_WATER: 10,
|
||
MOVES_PER_FOOD: 2,
|
||
MOVES_PER_WATER: 1,
|
||
DEATH_COOLDOWN: 120,
|
||
FIGHT_CHANCE: 0, //0.20, TODO UNCOMMENT THIS
|
||
BASE_HEALTH: 10,
|
||
BASE_HIT_CHANCE: 0.8,
|
||
MEAT_HEAL: 8,
|
||
MEDS_HEAL: 20,
|
||
HYPO_HEAL: 30,
|
||
FIGHT_DELAY: 3, // At least three moves between fights
|
||
NORTH: [ 0, -1],
|
||
SOUTH: [ 0, 1],
|
||
WEST: [-1, 0],
|
||
EAST: [ 1, 0],
|
||
|
||
Weapons: {
|
||
'fists': {
|
||
verb: _('punch'),
|
||
type: 'unarmed',
|
||
damage: 1,
|
||
cooldown: 2
|
||
},
|
||
'bone spear': {
|
||
verb: _('stab'),
|
||
type: 'melee',
|
||
damage: 2,
|
||
cooldown: 2
|
||
},
|
||
'iron sword': {
|
||
verb: _('swing'),
|
||
type: 'melee',
|
||
damage: 4,
|
||
cooldown: 2
|
||
},
|
||
'steel sword': {
|
||
verb: _('slash'),
|
||
type: 'melee',
|
||
damage: 6,
|
||
cooldown: 2
|
||
},
|
||
'bayonet': {
|
||
verb: _('thrust'),
|
||
type: 'melee',
|
||
damage: 8,
|
||
cooldown: 2
|
||
},
|
||
'rifle': {
|
||
verb: _('shoot'),
|
||
type: 'ranged',
|
||
damage: 5,
|
||
cooldown: 1,
|
||
cost: { 'bullets': 1 }
|
||
},
|
||
'laser rifle': {
|
||
verb: _('blast'),
|
||
type: 'ranged',
|
||
damage: 8,
|
||
cooldown: 1,
|
||
cost: { 'energy cell': 1 }
|
||
},
|
||
'grenade': {
|
||
verb: _('lob'),
|
||
type: 'ranged',
|
||
damage: 15,
|
||
cooldown: 5,
|
||
cost: { 'grenade': 1 }
|
||
},
|
||
'bolas': {
|
||
verb: _('tangle'),
|
||
type: 'ranged',
|
||
damage: 'stun',
|
||
cooldown: 15,
|
||
cost: { 'bolas': 1 }
|
||
},
|
||
'plasma rifle': {
|
||
verb: _('disintigrate'),
|
||
type: 'ranged',
|
||
damage: 12,
|
||
cooldown: 1,
|
||
cost: { 'energy cell': 1 }
|
||
},
|
||
'energy blade': {
|
||
verb: _('slice'),
|
||
type: 'melee',
|
||
damage: 10,
|
||
cooldown: 2
|
||
},
|
||
'disruptor': {
|
||
verb: _('stun'),
|
||
type: 'ranged',
|
||
damage: 'stun',
|
||
cooldown: 15
|
||
}
|
||
},
|
||
|
||
name: 'World',
|
||
options: {}, // Nothing for now
|
||
init: function(options) {
|
||
this.options = $.extend(
|
||
this.options,
|
||
options
|
||
);
|
||
|
||
// Setup probabilities. Sum must equal 1.
|
||
World.TILE_PROBS[World.TILE.FOREST] = 0.15;
|
||
World.TILE_PROBS[World.TILE.FIELD] = 0.35;
|
||
World.TILE_PROBS[World.TILE.BARRENS] = 0.5;
|
||
|
||
// Setpiece definitions
|
||
World.LANDMARKS[World.TILE.OUTPOST] = { num: 0, minRadius: 0, maxRadius: 0, scene: 'outpost', label: _('An Outpost') };
|
||
World.LANDMARKS[World.TILE.IRON_MINE] = { num: 1, minRadius: 5, maxRadius: 5, scene: 'ironmine', label: _('Iron Mine') };
|
||
World.LANDMARKS[World.TILE.COAL_MINE] = { num: 1, minRadius: 10, maxRadius: 10, scene: 'coalmine', label: _('Coal Mine') };
|
||
World.LANDMARKS[World.TILE.SULPHUR_MINE] = { num: 1, minRadius: 20, maxRadius: 20, scene: 'sulphurmine', label: _('Sulphur Mine') };
|
||
World.LANDMARKS[World.TILE.HOUSE] = { num: 10, minRadius: 0, maxRadius: World.RADIUS * 1.5, scene: 'house', label: _('An Old House') };
|
||
World.LANDMARKS[World.TILE.CAVE] = { num: 5, minRadius: 3, maxRadius: 10, scene: 'cave', label: _('A Damp Cave') };
|
||
World.LANDMARKS[World.TILE.TOWN] = { num: 10, minRadius: 10, maxRadius: 20, scene: 'town', label: _('An Abandoned Town') };
|
||
World.LANDMARKS[World.TILE.CITY] = { num: 20, minRadius: 20, maxRadius: World.RADIUS * 1.5, scene: 'city', label: _('A Ruined City') };
|
||
World.LANDMARKS[World.TILE.SHIP] = { num: 1, minRadius: 28, maxRadius: 28, scene: 'ship', label: _('A Crashed Starship')};
|
||
World.LANDMARKS[World.TILE.BOREHOLE] = { num: 10, minRadius: 15, maxRadius: World.RADIUS * 1.5, scene: 'borehole', label: _('A Borehole')};
|
||
World.LANDMARKS[World.TILE.BATTLEFIELD] = { num: 5, minRadius: 18, maxRadius: World.RADIUS * 1.5, scene: 'battlefield', label: _('A Battlefield')};
|
||
World.LANDMARKS[World.TILE.SWAMP] = { num: 1, minRadius: 15, maxRadius: World.RADIUS * 1.5, scene: 'swamp', label: _('A Murky Swamp')};
|
||
World.LANDMARKS[World.TILE.EXECUTIONER] = { num: 1, minRadius: 28, maxRadius: 28, scene: 'executioner', 'label': _('A Ravaged Battleship')};
|
||
|
||
// Only add the cache if there is prestige data
|
||
if($SM.get('previous.stores')) {
|
||
World.LANDMARKS[World.TILE.CACHE] = { num: 1, minRadius: 10, maxRadius: World.RADIUS * 1.5, scene: 'cache', label: _('A Destroyed Village')};
|
||
}
|
||
|
||
if(typeof $SM.get('features.location.world') == 'undefined') {
|
||
$SM.set('features.location.world', true);
|
||
$SM.set('features.executioner', true);
|
||
$SM.setM('game.world', {
|
||
map: World.generateMap(),
|
||
mask: World.newMask()
|
||
});
|
||
}
|
||
else if (!$SM.get('features.executioner')) {
|
||
// Place the Executioner in previously generated maps that don't have it
|
||
const map = $SM.get('game.world.map');
|
||
const landmark = World.LANDMARKS[World.TILE.EXECUTIONER]
|
||
for(let l = 0; l < landmark.num; l++) {
|
||
World.placeLandmark(landmark.minRadius, landmark.maxRadius, World.TILE.EXECUTIONER, map);
|
||
}
|
||
$SM.set('game.world.map', map);
|
||
$SM.set('features.executioner', true);
|
||
}
|
||
|
||
// Create the World panel
|
||
this.panel = $('<div>').attr('id', "worldPanel").addClass('location').appendTo('#outerSlider');
|
||
|
||
// Create the shrink wrapper
|
||
var outer = $('<div>').attr('id', 'worldOuter').appendTo(this.panel);
|
||
|
||
// Create the bag panel
|
||
$('<div>').attr('id', 'bagspace-world').append($('<div>')).appendTo(outer);
|
||
$('<div>').attr('id', 'backpackTitle').appendTo(outer);
|
||
$('<div>').attr('id', 'backpackSpace').appendTo(outer);
|
||
$('<div>').attr('id', 'healthCounter').appendTo(outer);
|
||
|
||
Engine.updateOuterSlider();
|
||
|
||
// Map the ship and show compass tooltip
|
||
World.ship = World.mapSearch(World.TILE.SHIP,$SM.get('game.world.map'),1);
|
||
World.dir = World.compassDir(World.ship[0]);
|
||
// compass tooltip text
|
||
Room.compassTooltip(World.dir);
|
||
|
||
// Check if everything has been seen
|
||
World.testMap();
|
||
|
||
//subscribe to stateUpdates
|
||
$.Dispatch('stateUpdate').subscribe(World.handleStateUpdates);
|
||
},
|
||
|
||
clearDungeon: function() {
|
||
Engine.event('progress', 'dungeon cleared');
|
||
World.state.map[World.curPos[0]][World.curPos[1]] = World.TILE.OUTPOST;
|
||
World.drawRoad();
|
||
},
|
||
|
||
drawRoad: function() {
|
||
var findClosestRoad = function(startPos) {
|
||
// We'll search in a spiral to find the closest road tile
|
||
// We spiral out along manhattan distance contour
|
||
// lines to ensure we draw the shortest road possible.
|
||
// No attempt is made to reduce the search space for
|
||
// tiles outside the map.
|
||
var searchX, searchY, dtmp,
|
||
x = 0,
|
||
y = 0,
|
||
dx = 1,
|
||
dy = -1;
|
||
for (var i = 0; i < Math.pow(World.getDistance(startPos, World.VILLAGE_POS) + 2, 2); i++) {
|
||
searchX = startPos[0] + x;
|
||
searchY = startPos[1] + y;
|
||
if (0 < searchX && searchX < World.RADIUS * 2 && 0 < searchY && searchY < World.RADIUS * 2) {
|
||
// check for road
|
||
var tile = World.state.map[searchX][searchY];
|
||
if (
|
||
tile === World.TILE.ROAD ||
|
||
(tile === World.TILE.OUTPOST && !(x === 0 && y === 0)) || // outposts are connected to roads
|
||
tile === World.TILE.VILLAGE // all roads lead home
|
||
) {
|
||
return [searchX, searchY];
|
||
}
|
||
}
|
||
if (x === 0 || y === 0) {
|
||
// Turn the corner
|
||
dtmp = dx;
|
||
dx = -dy;
|
||
dy = dtmp;
|
||
}
|
||
if (x === 0 && y <= 0) {
|
||
x++;
|
||
} else {
|
||
x += dx;
|
||
y += dy;
|
||
}
|
||
}
|
||
return World.VILLAGE_POS;
|
||
};
|
||
var closestRoad = findClosestRoad(World.curPos);
|
||
var xDist = World.curPos[0] - closestRoad[0];
|
||
var yDist = World.curPos[1] - closestRoad[1];
|
||
var xDir = Math.abs(xDist)/xDist;
|
||
var yDir = Math.abs(yDist)/yDist;
|
||
var xIntersect, yIntersect;
|
||
if(Math.abs(xDist) > Math.abs(yDist)) {
|
||
xIntersect = closestRoad[0];
|
||
yIntersect = closestRoad[1] + yDist;
|
||
} else {
|
||
xIntersect = closestRoad[0] + xDist;
|
||
yIntersect = closestRoad[1];
|
||
}
|
||
|
||
for(var x = 0; x < Math.abs(xDist); x++) {
|
||
if(World.isTerrain(World.state.map[closestRoad[0] + (xDir*x)][yIntersect])) {
|
||
World.state.map[closestRoad[0] + (xDir*x)][yIntersect] = World.TILE.ROAD;
|
||
}
|
||
}
|
||
for(var y = 0; y < Math.abs(yDist); y++) {
|
||
if(World.isTerrain(World.state.map[xIntersect][closestRoad[1] + (yDir*y)])) {
|
||
World.state.map[xIntersect][closestRoad[1] + (yDir*y)] = World.TILE.ROAD;
|
||
}
|
||
}
|
||
World.drawMap();
|
||
},
|
||
|
||
updateSupplies: function() {
|
||
var supplies = $('div#bagspace-world > div');
|
||
|
||
if(!Path.outfit) {
|
||
Path.outfit = {};
|
||
}
|
||
|
||
// Add water
|
||
var water = $('div#supply_water');
|
||
if(World.water > 0 && water.length === 0) {
|
||
water = World.createItemDiv('water', World.water);
|
||
water.prependTo(supplies);
|
||
} else if(World.water > 0) {
|
||
$('div#supply_water', supplies).text(_('water:{0}' , World.water));
|
||
} else {
|
||
water.remove();
|
||
}
|
||
|
||
var total = 0;
|
||
for(var k in Path.outfit) {
|
||
var item = $('div#supply_' + k.replace(' ', '-'), supplies);
|
||
var num = Path.outfit[k];
|
||
total += num * Path.getWeight(k);
|
||
if(num > 0 && item.length === 0) {
|
||
item = World.createItemDiv(k, num);
|
||
if(k == 'cured meat' && World.water > 0) {
|
||
item.insertAfter(water);
|
||
} else if(k == 'cured meat') {
|
||
item.prependTo(supplies);
|
||
} else {
|
||
item.appendTo(supplies);
|
||
}
|
||
} else if(num > 0) {
|
||
$('div#' + item.attr('id'), supplies).text(_(k) + ':' + num);
|
||
} else {
|
||
item.remove();
|
||
}
|
||
}
|
||
|
||
// Update label
|
||
var t = _('pockets');
|
||
if($SM.get('stores.rucksack', true) > 0) {
|
||
t = _('rucksack');
|
||
}
|
||
$('#backpackTitle').text(t);
|
||
|
||
// Update bagspace
|
||
$('#backpackSpace').text(_('free {0}/{1}', Math.floor(Path.getCapacity() - total) , Path.getCapacity()));
|
||
},
|
||
|
||
setWater: function(w) {
|
||
World.water = w;
|
||
if(World.water > World.getMaxWater()) {
|
||
World.water = World.getMaxWater();
|
||
}
|
||
World.updateSupplies();
|
||
},
|
||
|
||
setHp: function(hp) {
|
||
if(typeof hp == 'number' && !isNaN(hp)) {
|
||
World.health = hp;
|
||
if(World.health > World.getMaxHealth()) {
|
||
World.health = World.getMaxHealth();
|
||
}
|
||
$('#healthCounter').text(_('hp: {0}/{1}', World.health , World.getMaxHealth()));
|
||
}
|
||
},
|
||
|
||
createItemDiv: function(name, num) {
|
||
var div = $('<div>').attr('id', 'supply_' + name.replace(' ', '-'))
|
||
.addClass('supplyItem')
|
||
.text(_('{0}:{1}',_(name), num));
|
||
|
||
return div;
|
||
},
|
||
|
||
moveNorth: function() {
|
||
Engine.log('North');
|
||
if(World.curPos[1] > 0) World.move(World.NORTH);
|
||
},
|
||
|
||
moveSouth: function() {
|
||
Engine.log('South');
|
||
if(World.curPos[1] < World.RADIUS * 2) World.move(World.SOUTH);
|
||
},
|
||
|
||
moveWest: function() {
|
||
Engine.log('West');
|
||
if(World.curPos[0] > 0) World.move(World.WEST);
|
||
},
|
||
|
||
moveEast: function() {
|
||
Engine.log('East');
|
||
if(World.curPos[0] < World.RADIUS * 2) World.move(World.EAST);
|
||
},
|
||
|
||
move: function(direction) {
|
||
var oldTile = World.state.map[World.curPos[0]][World.curPos[1]];
|
||
World.curPos[0] += direction[0];
|
||
World.curPos[1] += direction[1];
|
||
World.narrateMove(oldTile, World.state.map[World.curPos[0]][World.curPos[1]]);
|
||
World.lightMap(World.curPos[0], World.curPos[1], World.state.mask);
|
||
World.drawMap();
|
||
World.doSpace();
|
||
|
||
// play random footstep
|
||
var randomFootstep = Math.floor(Math.random() * 5) + 1;
|
||
AudioEngine.playSound(AudioLibrary['FOOTSTEPS_' + randomFootstep]);
|
||
|
||
if(World.checkDanger()) {
|
||
if(World.danger) {
|
||
Notifications.notify(World, _('dangerous to be this far from the village without proper protection'));
|
||
} else {
|
||
Notifications.notify(World, _('safer here'));
|
||
}
|
||
}
|
||
},
|
||
|
||
keyDown: function(event) {
|
||
switch(event.which) {
|
||
case 38: // Up
|
||
case 87:
|
||
World.moveNorth();
|
||
break;
|
||
case 40: // Down
|
||
case 83:
|
||
World.moveSouth();
|
||
break;
|
||
case 37: // Left
|
||
case 65:
|
||
World.moveWest();
|
||
break;
|
||
case 39: // Right
|
||
case 68:
|
||
World.moveEast();
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
},
|
||
|
||
swipeLeft: function(e) {
|
||
World.moveWest();
|
||
},
|
||
|
||
swipeRight: function(e) {
|
||
World.moveEast();
|
||
},
|
||
|
||
swipeUp: function(e) {
|
||
World.moveNorth();
|
||
},
|
||
|
||
swipeDown: function(e) {
|
||
World.moveSouth();
|
||
},
|
||
|
||
click: function(event) {
|
||
var map = $('#map'),
|
||
// measure clicks relative to the centre of the current location
|
||
centreX = map.offset().left + map.width() * World.curPos[0] / (World.RADIUS * 2),
|
||
centreY = map.offset().top + map.height() * World.curPos[1] / (World.RADIUS * 2),
|
||
clickX = event.pageX - centreX,
|
||
clickY = event.pageY - centreY;
|
||
if (clickX > clickY && clickX < -clickY) {
|
||
World.moveNorth();
|
||
}
|
||
if (clickX < clickY && clickX > -clickY) {
|
||
World.moveSouth();
|
||
}
|
||
if (clickX < clickY && clickX < -clickY) {
|
||
World.moveWest();
|
||
}
|
||
if (clickX > clickY && clickX > -clickY) {
|
||
World.moveEast();
|
||
}
|
||
},
|
||
|
||
checkDanger: function() {
|
||
World.danger = typeof World.danger == 'undefined' ? false: World.danger;
|
||
if(!World.danger) {
|
||
if($SM.get('stores["i armour"]', true) === 0 && World.getDistance() >= 8) {
|
||
World.danger = true;
|
||
return true;
|
||
}
|
||
if($SM.get('stores["s armour"]', true) === 0 && World.getDistance() >= 18) {
|
||
World.danger = true;
|
||
return true;
|
||
}
|
||
} else {
|
||
if(World.getDistance() < 8) {
|
||
World.danger = false;
|
||
return true;
|
||
}
|
||
if(World.getDistance < 18 && $SM.get('stores["i armour"]', true) > 0) {
|
||
World.danger = false;
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
},
|
||
|
||
useSupplies: function() {
|
||
World.foodMove++;
|
||
World.waterMove++;
|
||
// Food
|
||
var movesPerFood = World.MOVES_PER_FOOD;
|
||
movesPerFood *= $SM.hasPerk('slow metabolism') ? 2 : 1;
|
||
if(World.foodMove >= movesPerFood) {
|
||
World.foodMove = 0;
|
||
var num = Path.outfit['cured meat'];
|
||
num--;
|
||
if(num === 0) {
|
||
Notifications.notify(World, _('the meat has run out'));
|
||
} else if(num < 0) {
|
||
// Starvation! Hooray!
|
||
num = 0;
|
||
if(!World.starvation) {
|
||
Notifications.notify(World, _('starvation sets in'));
|
||
World.starvation = true;
|
||
} else {
|
||
$SM.set('character.starved', $SM.get('character.starved', true));
|
||
$SM.add('character.starved', 1);
|
||
if($SM.get('character.starved') >= 10 && !$SM.hasPerk('slow metabolism')) {
|
||
$SM.addPerk('slow metabolism');
|
||
}
|
||
World.die();
|
||
return false;
|
||
}
|
||
} else {
|
||
World.starvation = false;
|
||
World.setHp(World.health + World.meatHeal());
|
||
}
|
||
Path.outfit['cured meat'] = num;
|
||
}
|
||
// Water
|
||
var movesPerWater = World.MOVES_PER_WATER;
|
||
movesPerWater *= $SM.hasPerk('desert rat') ? 2 : 1;
|
||
if(World.waterMove >= movesPerWater) {
|
||
World.waterMove = 0;
|
||
var water = World.water;
|
||
water--;
|
||
if(water === 0) {
|
||
Notifications.notify(World, _('there is no more water'));
|
||
} else if(water < 0) {
|
||
water = 0;
|
||
if(!World.thirst) {
|
||
Notifications.notify(World, _('the thirst becomes unbearable'));
|
||
World.thirst = true;
|
||
} else {
|
||
$SM.set('character.dehydrated', $SM.get('character.dehydrated', true));
|
||
$SM.add('character.dehydrated', 1);
|
||
if($SM.get('character.dehydrated') >= 10 && !$SM.hasPerk('desert rat')) {
|
||
$SM.addPerk('desert rat');
|
||
}
|
||
World.die();
|
||
return false;
|
||
}
|
||
} else {
|
||
World.thirst = false;
|
||
}
|
||
World.setWater(water);
|
||
World.updateSupplies();
|
||
}
|
||
return true;
|
||
},
|
||
|
||
meatHeal: function() {
|
||
return World.MEAT_HEAL * ($SM.hasPerk('gastronome') ? 2 : 1);
|
||
},
|
||
|
||
medsHeal: function() {
|
||
return World.MEDS_HEAL;
|
||
},
|
||
|
||
hypoHeal: () => World.HYPO_HEAL,
|
||
|
||
checkFight: function() {
|
||
World.fightMove = typeof World.fightMove == 'number' ? World.fightMove : 0;
|
||
World.fightMove++;
|
||
if(World.fightMove > World.FIGHT_DELAY) {
|
||
var chance = World.FIGHT_CHANCE;
|
||
chance *= $SM.hasPerk('stealthy') ? 0.5 : 1;
|
||
if(Math.random() < chance) {
|
||
World.fightMove = 0;
|
||
Events.triggerFight();
|
||
}
|
||
}
|
||
},
|
||
|
||
doSpace: function() {
|
||
var curTile = World.state.map[World.curPos[0]][World.curPos[1]];
|
||
|
||
if(curTile == World.TILE.VILLAGE) {
|
||
World.goHome();
|
||
} else if(curTile === World.TILE.EXECUTIONER) {
|
||
const scene = World.state.executioner ? 'executioner-antechamber' : 'executioner-intro';
|
||
const sceneData = Events.Executioner[scene];
|
||
Events.startEvent(sceneData);
|
||
} else if(typeof World.LANDMARKS[curTile] != 'undefined') {
|
||
if(curTile != World.TILE.OUTPOST || !World.outpostUsed()) {
|
||
Events.startEvent(Events.Setpieces[World.LANDMARKS[curTile].scene]);
|
||
}
|
||
} else {
|
||
if(World.useSupplies()) {
|
||
World.checkFight();
|
||
}
|
||
}
|
||
},
|
||
|
||
getDistance: function(from, to) {
|
||
from = from || World.curPos;
|
||
to = to || World.VILLAGE_POS;
|
||
return Math.abs(from[0] - to[0]) + Math.abs(from[1] - to[1]);
|
||
},
|
||
|
||
getTerrain: function() {
|
||
return World.state.map[World.curPos[0]][World.curPos[1]];
|
||
},
|
||
|
||
getDamage: function(thing) {
|
||
return World.Weapons[thing].damage;
|
||
},
|
||
|
||
narrateMove: function(oldTile, newTile) {
|
||
var msg = null;
|
||
switch(oldTile) {
|
||
case World.TILE.FOREST:
|
||
switch(newTile) {
|
||
case World.TILE.FIELD:
|
||
msg = _("the trees yield to dry grass. the yellowed brush rustles in the wind.");
|
||
break;
|
||
case World.TILE.BARRENS:
|
||
msg = _("the trees are gone. parched earth and blowing dust are poor replacements.");
|
||
break;
|
||
}
|
||
break;
|
||
case World.TILE.FIELD:
|
||
switch(newTile) {
|
||
case World.TILE.FOREST:
|
||
msg = _("trees loom on the horizon. grasses gradually yield to a forest floor of dry branches and fallen leaves.");
|
||
break;
|
||
case World.TILE.BARRENS:
|
||
msg = _("the grasses thin. soon, only dust remains.");
|
||
break;
|
||
}
|
||
break;
|
||
case World.TILE.BARRENS:
|
||
switch(newTile) {
|
||
case World.TILE.FIELD:
|
||
msg = _("the barrens break at a sea of dying grass, swaying in the arid breeze.");
|
||
break;
|
||
case World.TILE.FOREST:
|
||
msg = _("a wall of gnarled trees rises from the dust. their branches twist into a skeletal canopy overhead.");
|
||
break;
|
||
}
|
||
break;
|
||
}
|
||
if(msg != null) {
|
||
Notifications.notify(World, msg);
|
||
}
|
||
},
|
||
|
||
newMask: function() {
|
||
var mask = new Array(World.RADIUS * 2 + 1);
|
||
for(var i = 0; i <= World.RADIUS * 2; i++) {
|
||
mask[i] = new Array(World.RADIUS * 2 + 1);
|
||
}
|
||
World.lightMap(World.RADIUS, World.RADIUS, mask);
|
||
return mask;
|
||
},
|
||
|
||
lightMap: function(x, y, mask) {
|
||
var r = World.LIGHT_RADIUS;
|
||
r *= $SM.hasPerk('scout') ? 2 : 1;
|
||
World.uncoverMap(x, y, r, mask);
|
||
return mask;
|
||
},
|
||
|
||
uncoverMap: function(x, y, r, mask) {
|
||
mask[x][y] = true;
|
||
for(var i = -r; i <= r; i++) {
|
||
for(var j = -r + Math.abs(i); j <= r - Math.abs(i); j++) {
|
||
if(y + j >= 0 && y + j <= World.RADIUS * 2 &&
|
||
x + i <= World.RADIUS * 2 &&
|
||
x + i >= 0) {
|
||
mask[x+i][y+j] = true;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
|
||
testMap: function() {
|
||
if(!World.seenAll) {
|
||
var dark;
|
||
var mask = $SM.get('game.world.mask');
|
||
loop:
|
||
for(var i = 0; i < mask.length; i++) {
|
||
for(var j = 0; j < mask[i].length; j++) {
|
||
if(!mask[i][j]) {
|
||
dark = true;
|
||
break loop;
|
||
}
|
||
}
|
||
}
|
||
World.seenAll = !dark;
|
||
}
|
||
},
|
||
|
||
applyMap: function() {
|
||
if(!World.seenAll){
|
||
var x,y,mask = $SM.get('game.world.mask');
|
||
do {
|
||
x = Math.floor(Math.random() * (World.RADIUS * 2 + 1));
|
||
y = Math.floor(Math.random() * (World.RADIUS * 2 + 1));
|
||
} while (mask[x][y]);
|
||
World.uncoverMap(x, y, 5, mask);
|
||
}
|
||
World.testMap();
|
||
},
|
||
|
||
generateMap: function() {
|
||
var map = new Array(World.RADIUS * 2 + 1);
|
||
for(var i = 0; i <= World.RADIUS * 2; i++) {
|
||
map[i] = new Array(World.RADIUS * 2 + 1);
|
||
}
|
||
// The Village is always at the exact center
|
||
// Spiral out from there
|
||
map[World.RADIUS][World.RADIUS] = World.TILE.VILLAGE;
|
||
for(var r = 1; r <= World.RADIUS; r++) {
|
||
for(var t = 0; t < r * 8; t++) {
|
||
var x, y;
|
||
if(t < 2 * r) {
|
||
x = World.RADIUS - r + t;
|
||
y = World.RADIUS - r;
|
||
} else if(t < 4 * r) {
|
||
x = World.RADIUS + r;
|
||
y = World.RADIUS - (3 * r) + t;
|
||
} else if(t < 6 * r) {
|
||
x = World.RADIUS + (5 * r) - t;
|
||
y = World.RADIUS + r;
|
||
} else {
|
||
x = World.RADIUS - r;
|
||
y = World.RADIUS + (7 * r) - t;
|
||
}
|
||
|
||
map[x][y] = World.chooseTile(x, y, map);
|
||
}
|
||
}
|
||
|
||
// Place landmarks
|
||
for(var k in World.LANDMARKS) {
|
||
var landmark = World.LANDMARKS[k];
|
||
for(var l = 0; l < landmark.num; l++) {
|
||
var pos = World.placeLandmark(landmark.minRadius, landmark.maxRadius, k, map);
|
||
}
|
||
}
|
||
|
||
return map;
|
||
},
|
||
|
||
mapSearch: function(target,map,required){
|
||
var max = World.LANDMARKS[target].num;
|
||
if(!max){
|
||
// this restrict the research to numerable landmarks
|
||
return null;
|
||
}
|
||
// restrict research if only a fixed number (usually 1) is required
|
||
max = (required) ? Math.min(required,max) : max;
|
||
var index = 0;
|
||
var targets = [];
|
||
search: // label for coordinate research
|
||
for(var i = 0; i <= World.RADIUS * 2; i++){
|
||
for(var j = 0; j <= World.RADIUS * 2; j++){
|
||
if(map[i][j].charAt(0) === target){
|
||
// search result is stored as an object;
|
||
// items are listed as they appear in the map, tl-br
|
||
// each item has relative coordinates and a compass-type direction
|
||
targets[index] = {
|
||
x : i - World.RADIUS,
|
||
y : j - World.RADIUS,
|
||
};
|
||
index++;
|
||
if(index === max){
|
||
// optimisation: stop the research if maximum number of items has been reached
|
||
break search;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return targets;
|
||
},
|
||
|
||
compassDir: function(pos){
|
||
var dir = '';
|
||
var horz = pos.x < 0 ? 'west' : 'east';
|
||
var vert = pos.y < 0 ? 'north' : 'south';
|
||
if(Math.abs(pos.x) / 2 > Math.abs(pos.y)) {
|
||
dir = horz;
|
||
} else if(Math.abs(pos.y) / 2 > Math.abs(pos.x)){
|
||
dir = vert;
|
||
} else {
|
||
dir = vert + horz;
|
||
}
|
||
return dir;
|
||
},
|
||
|
||
placeLandmark: function(minRadius, maxRadius, landmark, map) {
|
||
|
||
var x = World.RADIUS, y = World.RADIUS;
|
||
while(!World.isTerrain(map[x][y])) {
|
||
var r = Math.floor(Math.random() * (maxRadius - minRadius)) + minRadius;
|
||
var xDist = Math.floor(Math.random() * r);
|
||
var yDist = r - xDist;
|
||
if(Math.random() < 0.5) xDist = -xDist;
|
||
if(Math.random() < 0.5) yDist = -yDist;
|
||
x = World.RADIUS + xDist;
|
||
if(x < 0) x = 0;
|
||
if(x > World.RADIUS * 2) x = World.RADIUS * 2;
|
||
y = World.RADIUS + yDist;
|
||
if(y < 0) y = 0;
|
||
if(y > World.RADIUS * 2) y = World.RADIUS * 2;
|
||
}
|
||
map[x][y] = landmark;
|
||
return [x, y];
|
||
},
|
||
|
||
isTerrain: function(tile) {
|
||
return tile == World.TILE.FOREST || tile == World.TILE.FIELD || tile == World.TILE.BARRENS;
|
||
},
|
||
|
||
chooseTile: function(x, y, map) {
|
||
|
||
var adjacent = [
|
||
y > 0 ? map[x][y-1] : null,
|
||
y < World.RADIUS * 2 ? map[x][y+1] : null,
|
||
x < World.RADIUS * 2 ? map[x+1][y] : null,
|
||
x > 0 ? map[x-1][y] : null
|
||
];
|
||
|
||
var chances = {};
|
||
var nonSticky = 1;
|
||
var cur;
|
||
for(var i in adjacent) {
|
||
if(adjacent[i] == World.TILE.VILLAGE) {
|
||
// Village must be in a forest to maintain thematic consistency, yo.
|
||
return World.TILE.FOREST;
|
||
} else if(typeof adjacent[i] == 'string') {
|
||
cur = chances[adjacent[i]];
|
||
cur = typeof cur == 'number' ? cur : 0;
|
||
chances[adjacent[i]] = cur + World.STICKINESS;
|
||
nonSticky -= World.STICKINESS;
|
||
}
|
||
}
|
||
for(var t in World.TILE) {
|
||
var tile = World.TILE[t];
|
||
if(World.isTerrain(tile)) {
|
||
cur = chances[tile];
|
||
cur = typeof cur == 'number' ? cur : 0;
|
||
cur += World.TILE_PROBS[tile] * nonSticky;
|
||
chances[tile] = cur;
|
||
}
|
||
}
|
||
|
||
var list = [];
|
||
for(var j in chances) {
|
||
list.push(chances[j] + '' + j);
|
||
}
|
||
list.sort(function(a, b) {
|
||
var n1 = parseFloat(a.substring(0, a.length - 1));
|
||
var n2 = parseFloat(b.substring(0, b.length - 1));
|
||
return n2 - n1;
|
||
});
|
||
|
||
var c = 0;
|
||
var r = Math.random();
|
||
for(var l in list) {
|
||
var prob = list[l];
|
||
c += parseFloat(prob.substring(0,prob.length - 1));
|
||
if(r < c) {
|
||
return prob.charAt(prob.length - 1);
|
||
}
|
||
}
|
||
|
||
return World.TILE.BARRENS;
|
||
},
|
||
|
||
markVisited: function(x, y) {
|
||
World.state.map[x][y] = World.state.map[x][y] + '!';
|
||
},
|
||
|
||
drawMap: function() {
|
||
var map = $('#map');
|
||
if(map.length === 0) {
|
||
map = new $('<div>').attr('id', 'map').appendTo('#worldOuter');
|
||
// register click handler
|
||
map.click(World.click);
|
||
}
|
||
var mapString = "";
|
||
for(var j = 0; j <= World.RADIUS * 2; j++) {
|
||
for(var i = 0; i <= World.RADIUS * 2; i++) {
|
||
var ttClass = "";
|
||
if(i > World.RADIUS) {
|
||
ttClass += " left";
|
||
} else {
|
||
ttClass += " right";
|
||
}
|
||
if(j > World.RADIUS) {
|
||
ttClass += " top";
|
||
} else {
|
||
ttClass += " bottom";
|
||
}
|
||
if(World.curPos[0] == i && World.curPos[1] == j) {
|
||
mapString += '<span class="landmark">@<div class="tooltip ' + ttClass + '">'+_('Wanderer')+'</div></span>';
|
||
} else if(World.state.mask[i][j]) {
|
||
var c = World.state.map[i][j];
|
||
switch(c) {
|
||
case World.TILE.VILLAGE:
|
||
mapString += '<span class="landmark">' + c + '<div class="tooltip' + ttClass + '">'+_('The Village')+'</div></span>';
|
||
break;
|
||
default:
|
||
if(typeof World.LANDMARKS[c] != 'undefined' && (c != World.TILE.OUTPOST || !World.outpostUsed(i, j))) {
|
||
mapString += '<span class="landmark">' + c + '<div class="tooltip' + ttClass + '">' + World.LANDMARKS[c].label + '</div></span>';
|
||
} else {
|
||
if(c.length > 1) {
|
||
c = c[0];
|
||
}
|
||
mapString += c;
|
||
}
|
||
break;
|
||
}
|
||
} else {
|
||
mapString += ' ';
|
||
}
|
||
}
|
||
mapString += '<br/>';
|
||
}
|
||
map.html(mapString);
|
||
},
|
||
|
||
die: function() {
|
||
if(!World.dead) {
|
||
World.dead = true;
|
||
Engine.log('player death');
|
||
Engine.event('game event', 'death');
|
||
Engine.keyLock = true;
|
||
// Dead! Discard any world changes and go home
|
||
Notifications.notify(World, _('the world fades'));
|
||
World.state = null;
|
||
Path.outfit = {};
|
||
$SM.remove('outfit');
|
||
AudioEngine.playSound(AudioLibrary.DEATH);
|
||
$('#outerSlider').animate({opacity: '0'}, 600, 'linear', function() {
|
||
$('#outerSlider').css('left', '0px');
|
||
$('#locationSlider').css('left', '0px');
|
||
$('#storesContainer').css({'top': '0px', 'right': '0px'});
|
||
Engine.activeModule = Room;
|
||
$('div.headerButton').removeClass('selected');
|
||
Room.tab.addClass('selected');
|
||
Engine.setTimeout(function(){
|
||
Room.onArrival();
|
||
$('#outerSlider').animate({opacity:'1'}, 600, 'linear');
|
||
Button.cooldown($('#embarkButton'));
|
||
Engine.keyLock = false;
|
||
Engine.tabNavigation = true;
|
||
}, 2000, true);
|
||
});
|
||
}
|
||
},
|
||
|
||
goHome: function() {
|
||
// Home safe! Commit the changes.
|
||
$SM.setM('game.world', World.state);
|
||
World.testMap();
|
||
|
||
if(World.state.sulphurmine && $SM.get('game.buildings["sulphur mine"]', true) === 0) {
|
||
$SM.add('game.buildings["sulphur mine"]', 1);
|
||
Engine.event('progress', 'sulphur mine');
|
||
}
|
||
if(World.state.ironmine && $SM.get('game.buildings["iron mine"]', true) === 0) {
|
||
$SM.add('game.buildings["iron mine"]', 1);
|
||
Engine.event('progress', 'iron mine');
|
||
}
|
||
if(World.state.coalmine && $SM.get('game.buildings["coal mine"]', true) === 0) {
|
||
$SM.add('game.buildings["coal mine"]', 1);
|
||
Engine.event('progress', 'coal mine');
|
||
}
|
||
if(World.state.ship && !$SM.get('features.location.spaceShip')) {
|
||
Ship.init();
|
||
Engine.event('progress', 'ship');
|
||
}
|
||
if (World.state.executioner && !$SM.get('features.location.fabricator')) {
|
||
Fabricator.init();
|
||
Notifications.notify(null, _('builder knows the strange device when she sees it. takes it for herself real quick. doesn’t ask where it came from.'));
|
||
Engine.event('progress', 'fabricator');
|
||
}
|
||
World.redeemBlueprints();
|
||
World.state = null;
|
||
|
||
if(Path.outfit['cured meat'] > 0) {
|
||
Button.setDisabled($('#embarkButton'), false);
|
||
}
|
||
|
||
World.returnOutfit();
|
||
|
||
$('#outerSlider').animate({left: '0px'}, 300);
|
||
Engine.activeModule = Path;
|
||
Path.onArrival();
|
||
Engine.restoreNavigation = true;
|
||
},
|
||
|
||
redeemBlueprints: () => {
|
||
let redeemed = false;
|
||
const redeem = (blueprint, item) => {
|
||
if (Path.outfit[blueprint]) {
|
||
$SM.set(`character.blueprints['${item}']`, true);
|
||
delete Path.outfit[blueprint];
|
||
redeemed = true;
|
||
}
|
||
};
|
||
|
||
redeem('hypo blueprint', 'hypo');
|
||
redeem('kinetic armour blueprint', 'kinetic armour');
|
||
redeem('disruptor blueprint', 'disruptor');
|
||
redeem('plasma rifle blueprint', 'plasma rifle');
|
||
redeem('stim blueprint', 'stim');
|
||
redeem('glowstone blueprint', 'glowstone');
|
||
|
||
if (redeemed) {
|
||
Notifications.notify(null, 'blueprints feed into the fabricator data port. possibilities grow.');
|
||
}
|
||
},
|
||
|
||
returnOutfit: () => {
|
||
for(var k in Path.outfit) {
|
||
$SM.add('stores["'+k+'"]', Path.outfit[k]);
|
||
if(World.leaveItAtHome(k)) {
|
||
Path.outfit[k] = 0;
|
||
}
|
||
}
|
||
},
|
||
|
||
leaveItAtHome: function(thing) {
|
||
return thing != 'cured meat' && thing != 'bullets' && thing != 'energy cell' &&
|
||
thing != 'charm' && thing != 'medicine' && thing != 'stim' && thing != 'hypo' &&
|
||
typeof World.Weapons[thing] == 'undefined' && typeof Room.Craftables[thing] == 'undefined';
|
||
},
|
||
|
||
getMaxHealth: function() {
|
||
if($SM.get('stores["kinetic armour"]', true) > 0) {
|
||
return World.BASE_HEALTH + 75;
|
||
} else if($SM.get('stores["s armour"]', true) > 0) {
|
||
return World.BASE_HEALTH + 35;
|
||
} else if($SM.get('stores["i armour"]', true) > 0) {
|
||
return World.BASE_HEALTH + 15;
|
||
} else if($SM.get('stores["l armour"]', true) > 0) {
|
||
return World.BASE_HEALTH + 5;
|
||
}
|
||
return World.BASE_HEALTH;
|
||
},
|
||
|
||
getHitChance: function() {
|
||
if($SM.hasPerk('precise')) {
|
||
return World.BASE_HIT_CHANCE + 0.1;
|
||
}
|
||
return World.BASE_HIT_CHANCE;
|
||
},
|
||
|
||
getMaxWater: function() {
|
||
|
||
if($SM.get('stores["fluid recycler"]', true) > 0) {
|
||
return World.BASE_WATER + 100;
|
||
} else if($SM.get('stores["water tank"]', true) > 0) {
|
||
return World.BASE_WATER + 50;
|
||
} else if($SM.get('stores.cask', true) > 0) {
|
||
return World.BASE_WATER + 20;
|
||
} else if($SM.get('stores.waterskin', true) > 0) {
|
||
return World.BASE_WATER + 10;
|
||
}
|
||
return World.BASE_WATER;
|
||
},
|
||
|
||
outpostUsed: function(x, y) {
|
||
x = typeof x == 'number' ? x : World.curPos[0];
|
||
y = typeof y == 'number' ? y : World.curPos[1];
|
||
var used = World.usedOutposts[x + ',' + y];
|
||
return typeof used != 'undefined' && used === true;
|
||
},
|
||
|
||
useOutpost: function() {
|
||
Notifications.notify(null, _('water replenished'));
|
||
World.setWater(World.getMaxWater());
|
||
// Mark this outpost as used
|
||
World.usedOutposts[World.curPos[0] + ',' + World.curPos[1]] = true;
|
||
},
|
||
|
||
onArrival: function() {
|
||
Engine.tabNavigation = false;
|
||
// Clear the embark cooldown
|
||
Button.clearCooldown($('#embarkButton'));
|
||
Engine.keyLock = false;
|
||
// Explore in a temporary world-state. We'll commit the changes if you return home safe.
|
||
World.state = $.extend(true, {}, $SM.get('game.world'));
|
||
World.setWater(World.getMaxWater());
|
||
World.setHp(World.getMaxHealth());
|
||
World.foodMove = 0;
|
||
World.waterMove = 0;
|
||
World.starvation = false;
|
||
World.thirst = false;
|
||
World.usedOutposts = {};
|
||
World.curPos = World.copyPos(World.VILLAGE_POS);
|
||
World.drawMap();
|
||
World.setTitle();
|
||
AudioEngine.playBackgroundMusic(AudioLibrary.MUSIC_WORLD);
|
||
World.dead = false;
|
||
$('div#bagspace-world > div').empty();
|
||
World.updateSupplies();
|
||
$('#bagspace-world').width($('#map').width());
|
||
},
|
||
|
||
setTitle: function() {
|
||
document.title = _('A Barren World');
|
||
},
|
||
|
||
copyPos: function(pos) {
|
||
return [pos[0], pos[1]];
|
||
},
|
||
|
||
handleStateUpdates: function(e){
|
||
|
||
}
|
||
};
|