import { io } from 'socket.io-client';
import $ from 'jquery';
-import {expToLevel, maxHp, Player} from '../shared/player';
+import {expToLevel, maxHp, Player, StatDef, StatDisplay} from '../shared/player';
import { authToken, http } from './http';
import { CustomEventManager } from './events';
-import {MonsterForFight, MonsterForList} from '../shared/monsters';
+import {Fight, FightTrigger, MonsterForFight, MonsterForList} from '../shared/monsters';
import {FightRound} from '../shared/fight';
-import { City, city, Location } from '../shared/map'
+import { City, Location, LocationType, Path } from '../shared/map'
import { v4 as uuid } from 'uuid';
import {EquipmentSlot, ShopItem} from '../shared/inventory';
import { each } from 'lodash';
import {EquippedItemDetails} from '../shared/equipped';
import { Skill, Skills } from '../shared/skills';
import { configureChat } from './chat';
+import { SocketEvent } from './socket-event.client';
import * as EventList from '../events/client';
+import { TimeManager } from '../shared/time';
+import { TravelDTO } from '../events/travel/shared';
+const time = new TimeManager();
const cache = new Map<string, any>();
const events = new CustomEventManager();
const socket = io({
configureChat(socket);
+function setTimeGradient() {
+ const gradientName = time.gradientName();
+ const $body = $('body');
+ $body.removeClass(Array.from($body[0].classList)).addClass(gradientName);
+ $body.addClass(time.getTimePeriod());
+
+ let icon: string;
+ if(time.get24Hour() >= 5 && time.get24Hour() < 9) {
+ icon = 'morning';
+ }
+ else if(time.get24Hour() >= 9 && time.get24Hour() < 17) {
+ icon = 'afternoon';
+ }
+ else if(time.get24Hour() >= 17 && time.get24Hour() < 20) {
+ icon = 'evening'
+ }
+ else {
+ icon = 'night';
+ }
+
+ $('#time-of-day').html(`<img src="/assets/img/icons/time-of-day/${icon}.png"> ${time.getHour()}${time.getAmPm()}`);
+}
+
+setTimeGradient();
+setInterval(setTimeGradient, 60 * 1000);
+
function icon(name: string): string {
return `<img src="/assets/img/${name}.png" class="icon">`;
}
</form></p>`).removeClass('hidden');
}
+ $('#extra-inventory-info').html(renderStatDetails());
}
socket.on('connect', () => {
console.log(`Connected: ${socket.id}`);
});
-socket.on('ready', () => {
- console.log('Server connection verified');
- socket.emit('inventory');
-});
+socket.on('ready', bootstrap);
socket.on('server-stats', (data: {onlinePlayers: number}) => {
$('#server-stats').html(`${data.onlinePlayers} players online`);
each(EventList, event => {
console.log(`Binding Event ${event.eventName}`);
- socket.on(event.eventName, event.handler.bind(null, {
- cache,
- socket
- }))
+ if(event instanceof SocketEvent) {
+ socket.on(event.eventName, event.handler.bind(null, {
+ cache,
+ socket,
+ events
+ }));
+ }
+ else {
+ console.log('Skipped binding', event);
+ }
});
socket.on('calc:ap', (data: {ap: Record<EquipmentSlot, { currentAp: number, maxAp: number}>}) => {
$('#skills').html(html);
});
-events.on('alert', (data: {type: string, text: string}) => {
+function Alert(text: string, type: string = 'error') {
let id = uuid();
- $('#alerts').append(`<div class="alert ${data.type}" id="alert-${id}">${data.text}</div>`);
+ $('#alerts').append(`<div class="alert ${type}" id="alert-${id}">${text}</div>`);
setTimeout(() => {
$(`#alert-${id}`).remove();
}, 3000);
+}
+events.on('alert', (data: {type: string, text: string}) => {
+ Alert(data.text, data.type);
});
socket.on('alert', data => {
events.emit('alert', [data]);
});
+socket.on('init', (data: {version: string}) => {
+ $('#version').html(`v${data.version}`);
+});
+
socket.on('authToken', (authToken: string) => {
console.log(`recv auth token ${authToken}`);
localStorage.setItem('authToken', authToken);
updatePlayer();
});
+async function fetchState() {
+ const res = await http.get('/state');
+ cache.set('state', res);
+}
-$('nav a').on('click', e => {
+$('nav a').on('click', async e => {
e.preventDefault();
e.stopPropagation();
+ const tabsDisabledInFight = [
+ 'skills',
+ 'inventory'
+ ];
+
+ await fetchState();
+ const state = cache.get('state');
+
const $tabContainer = $(`#${$(e.target).data('container')}`);
+ let tabSection = $(e.target).data('section');
+
+ if(tabsDisabledInFight.includes(tabSection) && state?.fight !== null) {
+ Alert('You are currently in a fight', 'error');
+ // we want to force users over to the explore tab
+ // if they are currently in a fight and trying to
+ // access a disabled section
+ tabSection = 'explore';
+ }
$tabContainer.find('.tab').removeClass('active');
- $(`#${$(e.target).data('section')}`).addClass('active');
+ $(`#${tabSection}`).addClass('active');
- $(e.target).closest('nav').find('a').removeClass('active');
- $(e.target).addClass('active');
+ if(e.target.innerHTML !== 'Settings') {
+ $(e.target).closest('nav').find('a').removeClass('active');
+ $(`nav a[data-section=${tabSection}]`).addClass('active');
+ }
- events.emit(`tab:${$(e.target).data('section')}`);
+ events.emit(`tab:${tabSection}`);
});
events.on('tab:inventory', () => {
return html;
}
+function statPointIncreaser(stat: StatDisplay) {
+ return `<button class="increase-stat emit-event" data-event="spend-stat-point" data-args="${stat.id}">+</button>`;
+}
function renderStatDetails() {
const player: Player = cache.get('player');
- const html = `
- <table id="stat-breakdown">
- <tr>
- <th>Strength</th>
- <td class="strength">${player.strength}</td>
- </tr>
- <tr>
- <th>Constitution</th>
- <td class="constitution">${player.constitution}</td>
- </tr>
- <tr>
- <th>Dexterity</th>
- <td class="dexterity">${player.dexterity}</td>
- </tr>
- <tr>
- <th>Intelligence</th>
- <td class="intelligence">${player.intelligence}</td>
- </tr>
- </table>
- `;
+ let html = '<table id="stat-breakdown">';
+
+ StatDef.forEach(stat => {
+ html += `<tr>
+ <th>${stat.display}</th>
+ <td class="${stat.id}">
+ ${player[stat.id]}
+ ${player.stat_points ? statPointIncreaser(stat) : ''}
+ </td>
+ </tr>`;
+ });
+
+ html += `<tr><th>Stat Points</th><td class="stat_points">${player.stat_points}</td></tr>`;
+
+ html += '</table>';
+
return html;
}
socket.emit('unequip', { id: $(e.target).data('id') });
});
-async function renderMap() {
- if(cache.has(`map:${cache.get('player').city_id}`)) {
- $('#map').html(cache.get(`map:${cache.get('player').city_id}`));
+function setMapBackground(city_id: number) {
+ $('#explore').css({
+ 'background-image': `linear-gradient(to left top, rgba(255,255,255,0) 0%,rgb(255,255,255) 100%), linear-gradient(to left, rgba(255, 255, 255, 0) 0%, rgb(255, 255, 255) 100%), url("/assets/img/map/${city_id}.jpeg")`
+ });
+}
+
+events.on('renderMap', async function renderMap(data: { city: City, locations: Location[], paths: Path[]}) {
+ if(!data && cache.has('currentMapHTML')) {
+ $('#map').html(cache.get('currentMapHTML'));
return;
}
- const data: {city: City, locations: Location[]} = await http.get(`/city/${cache.get('player').city_id}`);
+ if(!data) {
+ console.error('oh no.. this got triggered without any city data');
+ }
- let html = `
- <h1>${data.city.name}</h1>
- <table>
- <tr>
- <th>Services</th>
- <th>Stores</th>
- <th>Explore</th>
- </tr>
- <tr>
- <td>
- ${data.locations.filter(l => l.type === 'SERVICES').map(l => {
- return `<a href="#" class="city-action" data-type="services" data-id="${l.id}">${l.name}</a>`
- }).join("<br>")}
- </td>
- <td>
- ${data.locations.filter(l => l.type === 'STORES').map(l => {
- return `<a href="#" class="city-action" data-type="stores" data-id="${l.id}">${l.name}</a>`
- }).join("<br>")}
- </td>
- <td>
- ${data.locations.filter(l => l.type === 'EXPLORE').map(l => {
- return `<a href="#" class="city-action" data-type="explore" data-id="${l.id}">${l.name}</a>`
+ setMapBackground(data.city.id);
+
+ const servicesParsed: Record<LocationType, string[]> = {
+ 'SERVICES': [],
+ 'STORES': [],
+ 'EXPLORE': []
+ };
+
+ data.locations.forEach(l => {
+ servicesParsed[l.type].push(`<a href="#" class="city-emit-event" data-event="${l.event_name}" data-args="${l.id}">${l.name}</a>`);
+ });
+
+ let html = `<h1>${data.city.name}</h1>
+ <div class="city-details">`;
+
+ if(servicesParsed.SERVICES.length) {
+ html += `<div><h3>Services</h3>${servicesParsed.SERVICES.join("<br>")}</div>`
+ }
+ if(servicesParsed.STORES.length) {
+ html += `<div><h3>Stores</h3>${servicesParsed.STORES.join("<br>")}</div>`
+ }
+ if(servicesParsed.EXPLORE.length) {
+ html += `<div><h3>Explore</h3>${servicesParsed.EXPLORE.join("<br>")}</div>`
+ }
+ html += `
+ <div>
+ <h3>Travel</h3>
+ ${data.paths.map(path => {
+ return `<a href="#" class="city-emit-event" data-event="city:travel" data-args="${path.ending_city}">${path.ending_city_name}</a>`
}).join("<br>")}
- </td>
- </tr>
- </table>
+ </div>
+ </div>
`;
- cache.set(`map:${cache.get('player').city_id}`, html);
- $('#map').html(html);
-}
+ cache.set('currentMapHTML', html);
-socket.on('city:service:healer', (data: {text: string}) => {
- $('#map').html(data.text);
+ $('#map').html(html);
});
-function renderRequirement(name: string, val: number | string): string {
- return `<span class="requirement-title">${name}</span>: <span class="requirement-value">${val.toLocaleString()}</span>`;
+function renderRequirement(name: string, val: number | string, currentVal?: number): string {
+ let colorIndicator = '';
+ if(currentVal) {
+ colorIndicator = currentVal >= val ? 'success' : 'error';
+ }
+ return `<span class="requirement-title">${name}</span>: <span class="requirement-value ${colorIndicator}">${val.toLocaleString()}</span>`;
}
function renderStatBoost(name: string, val: number | string): string {
if(typeof val === 'number') {
valSign = val > 0 ? '+' : '-';
}
- return `<span class="requirement-title">${name}</span>: <span class="requirement-value ${typeof val === 'number' && val > 0 ? "success": "error"}">${valSign}${val}</span>`;
+ return `<span class="requirement-title">${name}</span>: <span class="requirement-value ${typeof val === 'number' ? (val > 0 ? "success": "error") : ""}">${valSign}${val}</span>`;
}
function renderInventoryItem(item: EquippedItemDetails , action: (item: EquippedItemDetails) => string): string {
${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''}
${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''}
${item.boosts.damage ? renderStatBoost('DMG', item.boosts.damage) : ''}
+ ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''}
${['WEAPON','SPELL'].includes(item.type) ? '': generateProgressBar(item.currentAp, item.maxAp, '#7be67b')}
</div>
${item.hasOwnProperty('id') ? `<div>${item.cost.toLocaleString()}G</div>` : ''}
}
function renderShopItem(item: ShopItem, action: (item: ShopItem) => string): string {
+ const player: Player = cache.get('player');
return `<div class="store-list">
<div>
<img src="https://via.placeholder.com/64x64">
<div class="details">
<div class="name">${item.name}${item.equipment_slot === 'TWO_HANDED' ? ' (2H)': ''}</div>
<div class="requirements">
- ${item.requirements.level ? renderRequirement('LVL', item.requirements.level): ''}
- ${item.requirements.strength ? renderRequirement('STR', item.requirements.strength): ''}
- ${item.requirements.constitution ? renderRequirement('CON', item.requirements.constitution): ''}
- ${item.requirements.dexterity ? renderRequirement('DEX', item.requirements.dexterity): ''}
- ${item.requirements.intelligence ? renderRequirement('INT', item.requirements.intelligence): ''}
+ ${item.requirements.level ? renderRequirement('LVL', item.requirements.level, player.level): ''}
+ ${item.requirements.strength ? renderRequirement('STR', item.requirements.strength, player.strength): ''}
+ ${item.requirements.constitution ? renderRequirement('CON', item.requirements.constitution, player.constitution): ''}
+ ${item.requirements.dexterity ? renderRequirement('DEX', item.requirements.dexterity, player.dexterity): ''}
+ ${item.requirements.intelligence ? renderRequirement('INT', item.requirements.intelligence, player.intelligence): ''}
${renderRequirement('PRF', item.profession)}
</div>
<div class="stat-mods">
${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''}
${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''}
${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''}
- ${item.boosts.damage ? renderStatBoost('DMG', item.boosts.damage) : ''}
- ${['WEAPON','SPELL'].includes(item.type) ? '' : renderStatBoost('AP', item.maxAp)}
+ ${item.boosts.damage ? renderStatBoost(item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item.boosts.damage) : ''}
+ ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''}
+ ${['WEAPON','SPELL'].includes(item.type) ? '' : renderStatBoost('AP', item.maxAp.toString())}
</div>
${item.hasOwnProperty('id') ? `<div>${item.cost.toLocaleString()}G</div>` : ''}
</div>
}
-socket.on('city:store:blacksmith', (data: ShopItem[]) => {
- let html = `<div class="weapon-listing">
+socket.on('city:stores', (data: ShopItem[]) => {
+ let html = `<div class="shop-inventory-listing">
${data.map(item => {
return renderShopItem(item, i => {
const id = `data-id="${i.id}"`;
`;
$('#map').html(html);
-});
-
-socket.on('city:store:armourer', (data: ShopItem[]) => {
- let html = `<div class="armour-listing">
- ${data.map(item => {
- return renderShopItem(item, i => {
- const id = `data-id="${i.id}"`;
- return `<button type="button" class="purchase-item" ${id} data-type="${i.type}" data-equipment-slot="${i.equipment_slot}" data-cost="${i.cost}">Buy</button>`
- })
- }).join("\n")}
- </div>
- `;
- $('#map').html(html);
});
-socket.on('city:store:mageshop', (data: ShopItem[]) => {
- let html = `<div class="spell-listing">
- ${data.map(item => {
- return renderShopItem(item, i => {
- const id = `data-id="${i.id}"`;
- return `<button type="button" class="purchase-item" ${id} data-type="${i.type}" data-equipment-slot="${i.equipment_slot}" data-cost="${i.cost}">Buy</button>`
- })
- }).join("\n")}
- </div>
- `;
-
- $('#map').html(html);
-});
$('body').on('click', '.purchase-item', e => {
e.preventDefault();
e.stopPropagation();
}
});
-$('body').on('click', '.city-emit-event', e => {
- const eventName = $(e.target).data('event');
- console.log(`Sending event ${eventName}`);
- socket.emit(eventName);
+$('body').on('click', '.emit-event', e => {
+ const $el = $(e.target);
+
+ const eventName = $el.data('event');
+ const args = $el.data('args');
+
+ const rawBlock = $el.data('block');
+
+ if(rawBlock) {
+ const block = parseInt(rawBlock) || 0;
+
+ if(block > Date.now()) {
+ Alert('Sorry, clicked too quick');
+ return;
+ }
+ }
+
+ console.log(`Sending event ${eventName}`, { args });
+ socket.emit(eventName, { args });
});
+$('body').on('click', '.city-emit-event', e => {
+ const $el = $(e.target);
-$('body').on('click', '.city-action', e => {
- const data = {
- type: $(e.target).data('type'),
- id: $(e.target).data('id')
- };
+ const eventName = $el.data('event');
+ const args = $el.data('args');
- const eventName = `city:${data.type}:${data.id}` ;
+ console.log(`Sending event ${eventName}`, { args });
+ socket.emit(eventName, { args });
+});
+
+$('body').on('click', '.emit-event-internal', e => {
+ const $el = $(e.target);
- console.log('Sending event', eventName);
+ const eventName = $el.data('event');
+ const args: string[] = $el.data('args')?.toString().split('|');
- socket.emit(eventName);
+ console.log(`Trigger internal event [${eventName}]`);
+ events.emit(eventName, args);
});
+
socket.on('explore:fights', (monsters: MonsterForList[]) => {
const lastSelectedMonster = cache.get('last-selected-monster');
if(monsters.length) {
let sel = `<form id="fight-selector"><select id="monster-selector">
${monsters.map(monster => {
- return `<option value="${monster.id}" ${lastSelectedMonster === monster.id ? 'selected': ''}>${monster.name}</option>`;
- })}
+ return `<option value="${monster.id}" ${lastSelectedMonster == monster.id ? 'selected': ''}>${monster.name}</option>`;
+ }).join("\n")}
</select> <button type="submit">Fight</button></option>`;
$('#map').html(sel);
});
events.on('tab:explore', async () => {
- const res = await http.get('/fight');
- if(!res) {
- renderMap();
+ const state = cache.get('state');
+ if(cache.get('player').hp <= 0 || (!state.fight && !state.travel)) {
+ // get the details of the city
+ // render the map!
+ let data: {city: City, locations: Location[], paths: Path[]};
+ if(!cache.has(('currentMapHTML')) || cache.get('player').hp <= 0) {
+ data = await http.get(`/city/${cache.get('player').city_id}`);
+ }
+ events.emit('renderMap', [data, state.travel]);
+
}
- else {
- $('#map').html(renderFight(res));
+ else if(state.fight) {
+ setMapBackground(state.closestTown);
+ $('#map').html(renderFight(state.fight));
+ }
+ else if(state.travel) {
+ // render TRAVEL
+ events.emit('renderTravel', [state.travel]);
}
});
-function renderFight(monster: MonsterForFight) {
+function updateStepButton() {
+ const raw = $('#keep-walking').data('block');
+ const time = parseInt(raw) || 0;
+
+ if(time > 0) {
+ const wait = Math.ceil((time - Date.now())/ 1000);
+ if(wait > 0) {
+ $('#keep-walking').prop('disabled', true).html(`Keep walking (${wait}s)`);
+ setTimeout(updateStepButton, 300);
+ return;
+ }
+ }
+
+ $('#keep-walking').prop('disabled', false).html(`Keep walking`);
+}
+
+function renderTravel(data: TravelDTO) {
+ setMapBackground(data.closestTown);
+
+ let promptText = data.walkingText;
+ const blockTime = data.nextAction || 0;
+ let html = `<div id="travelling">`;
+
+ html += '<div id="travelling-actions">';
+ html += `<button class="emit-event" id="keep-walking" data-event="travel:step" data-block="${blockTime}">Keep Walking</button>`;
+
+ if(data.things.length) {
+ // ok you found something, for now we only support
+ // monsters, but eventually that will change
+ promptText = `You see a ${data.things[0].name}`;
+ html += `<button class="emit-event-internal" data-event="startFight" data-args="${data.things[0].id}|travel">Fight</button>`;
+ }
+
+ // end #travelling-actions
+ html += '</div>';
+ html += `<p>${promptText}</p>`;
+
+ // end #travelling div
+ html += '</div>';
+
+ $('#map').html(html);
+
+ if(blockTime) {
+ updateStepButton();
+ }
+}
+
+events.on('renderTravel', renderTravel);
+
+function renderFight(monster: MonsterForFight | Fight) {
const hpPercent = Math.floor((monster.hp / monster.maxHp) * 100);
let html = `<div id="fight-container">
<div class="progress-bar" id="defender-hp-bar" style="background: linear-gradient(to right, red, red ${hpPercent}%, transparent ${hpPercent}%, transparent)" title="${hpPercent}% - ${monster.hp}/${monster.maxHp}">${hpPercent}% - ${monster.hp} / ${monster.maxHp}</div>
</div>
</div>
- <div id="fight-results"></div>
<div id="fight-actions">
<select id="fight-target">
<option value="head">Head</option>
<button type="button" class="fight-action" data-action="cast">Cast</button>
<button type="button" class="fight-action" data-action="flee">Flee</button>
</div>
+ <div id="fight-results"></div>
</div>`;
return html;
}
}
- if(monsters.length) {
- const lastSelectedMonster = cache.get('last-selected-monster');
- html.push('<br><hr><h2>Fight Again</h2>');
- html.push(`<form id="fight-selector"><select id="monster-selector">
- ${monsters.map(monster => {
- return `<option value="${monster.id}" ${lastSelectedMonster === monster.id ? 'selected': ''}>${monster.name}</option>`;
- })}
- </select> <button type="submit">Fight</button></option>
- </select></form>`);
+ if(roundData.player.hp === 0) {
+ // prompt to return to town and don't let them do anything
+ html.push(`<p>You were killed...</p>`);
+ html.push('<p><button class="emit-event-internal" data-event="tab:explore" data-args="">Back to Town</button></p>');
+ }
+ else {
+ switch(roundData.fightTrigger) {
+ case 'explore':
+ if(monsters.length) {
+ const lastSelectedMonster = cache.get('last-selected-monster');
+ // put this above the fight details
+ html.unshift(`<h2>Fight Again</h2><form id="fight-selector"><select id="monster-selector">
+ ${monsters.map(monster => {
+ return `<option value="${monster.id}" ${lastSelectedMonster == monster.id ? 'selected': ''}>${monster.name}</option>`;
+ }).join("\n")}
+ </select> <button type="submit">Fight</button></option>
+ </select></form><hr>`);
+ }
+ break;
+ case 'travel':
+ html.push(`<p><button class="emit-event" data-event="travel:step">Keep Walking</button></p>`);
+ break;
+ default:
+ console.error(`Unknown fight trigger [${roundData.fightTrigger}]`, roundData);
+ break;
+ }
}
+
+
$('#fight-results').html(html.join("\n"));
$('#fight-actions').html('');
$('#fight-results').html(data.roundDetails.map(d => `<div>${d}</div>`).join("\n"));
});
+async function startFight(monsterId: string, fightTrigger: FightTrigger = 'explore') {
+ // check your HP first
+ if(cache.get('player').hp <= 0) {
+ events.emit('alert', [{
+ type: 'error',
+ text: 'You don\'t have enough HP to go looking for a fight'
+ }]);
+ return;
+ }
+
+ cache.set('last-selected-monster', monsterId);
+
+ try {
+ const monster: MonsterForFight = await http.post('/fight', {
+ monsterId,
+ fightTrigger
+ });
+
+ $('#map').html(renderFight(monster));
+ }
+ catch(e) {
+ events.emit('alert', [{
+ type: 'error',
+ text: 'Sorry, you can\'t start that fight'
+ }]);
+ }
+}
+
+events.on('startFight', startFight);
+
$('body').on('submit', '#fight-selector', async e => {
e.preventDefault();
e.stopPropagation();
const monsterId = $('#monster-selector option:selected').val();
- cache.set('last-selected-monster', monsterId);
-
- const monster: MonsterForFight = await http.post('/fight', {
- monsterId,
- });
-
+ if(monsterId) {
+ startFight(monsterId.toString());
+ }
+ else {
+ console.error(`Invalid monster id [${monsterId}]`);
+ }
- $('#map').html(renderFight(monster));
});
$('body').on('click', '.fight-action', e => {
}
}
});
+
+$('body').on('click', '#logout', async e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ let logout = false;
+
+ const player = cache.get('player');
+ if(/^Player[\d]+$/.test(player.username)) {
+ const clear = confirm('Are you sure? You will not be able to retrieve this character unless you set up an email/password');
+ if(clear) {
+ logout = true;
+ }
+ else {
+ logout = false;
+ }
+ }
+ else {
+ logout = true;
+ }
+
+ if(logout) {
+ socket.emit('logout');
+ localStorage.clear();
+ window.location.reload();
+ }
+
+});
+
+function bootstrap() {
+ console.log('Server connection verified');
+ socket.emit('inventory');
+ $('nav a').first().click();
+}