1 import * as otel from './tracing';
2 import { version } from "../../package.json";
3 import { config as dotenv } from 'dotenv';
4 import { join } from 'path';
5 import express, {Request, Response} from 'express';
6 import bodyParser from 'body-parser';
8 import http from 'http';
9 import { Server, Socket } from 'socket.io';
10 import { logger } from './lib/logger';
11 import { loadPlayer, createPlayer, updatePlayer, movePlayer } from './player';
12 import { random, sample } from 'lodash';
13 import {broadcastMessage, Message} from '../shared/message';
14 import {expToLevel, maxHp, Player} from '../shared/player';
15 import {clearFight, createFight, getMonsterList, getRandomMonster, loadMonster, loadMonsterFromFight, loadMonsterWithFaction, saveFightState} from './monster';
16 import {FightRound} from '../shared/fight';
17 import {addInventoryItem, deleteInventoryItem, getEquippedItems, getInventory, getInventoryItem, updateAp} from './inventory';
18 import { getItemFromPlayer, getItemFromShop, getPlayersItems, getShopItems, givePlayerItem, updateItemCount } from './items';
19 import {FightTrigger, Monster, MonsterForFight, MonsterWithFaction} from '../shared/monsters';
20 import {getShopEquipment, getShopItem, listShopItems } from './shopEquipment';
21 import {EquippedItemDetails} from '../shared/equipped';
22 import {ArmourEquipmentSlot, EquipmentSlot} from '../shared/inventory';
23 import { clearTravelPlan, completeTravel, getAllPaths, getAllServices, getCityDetails, getService, getTravelPlan, stepForward, travel } from './map';
24 import { signup, login, authEndpoint } from './auth';
25 import {db} from './lib/db';
26 import { getPlayerSkills, getPlayerSkillsAsObject, updatePlayerSkills } from './skills';
27 import {SkillID, Skills} from '../shared/skills';
29 import { router as healerRouter } from './locations/healer';
31 import * as Alert from './views/alert';
32 import { renderPlayerBar } from './views/player-bar'
33 import { renderEquipmentDetails, renderStore } from './views/stores';
34 import { renderMap } from './views/map';
35 import { renderProfilePage } from './views/profile';
36 import { renderSkills } from './views/skills';
37 import { renderInventoryPage } from './views/inventory';
38 import { renderMonsterSelector } from './views/monster-selector';
39 import { renderFight, renderRoundDetails } from './views/fight';
40 import { renderTravel, travelButton } from './views/travel';
41 import { renderChatMessage } from './views/chat';
44 import { createMonsters } from '../../seeds/monsters';
45 import { createAllCitiesAndLocations } from '../../seeds/cities';
46 import { createShopItems } from '../../seeds/shop_items';
47 import { Item, PlayerItem, ShopItem } from 'shared/items';
48 import { equip, unequip } from './equipment';
49 import { HealthPotionSmall } from '../shared/items/health_potion';
55 const app = express();
56 const server = http.createServer(app);
58 app.use(express.static(join(__dirname, '..', '..', 'public')));
59 app.use(bodyParser.urlencoded({ extended: true }));
60 app.use(express.json());
62 const io = new Server(server);
64 const cache = new Map<string, any>();
65 const chatHistory: Message[] = [];
67 app.use((req, res, next) => {
68 console.log(req.method, req.url);
72 io.on('connection', async socket => {
73 logger.log(`socket ${socket.id} connected, authToken: ${socket.handshake.headers['x-authtoken']}`);
75 let authToken = socket.handshake.headers['x-authtoken'].toString() === 'null' ? null : socket.handshake.headers['x-authtoken'].toString();
79 logger.log(`Attempting to load player with id ${authToken}`);
80 player = await loadPlayer(authToken);
83 logger.log(`Creating player`);
84 player = await createPlayer();
85 authToken = player.id;
86 socket.handshake.headers['x-authtoken'] = authToken;
89 logger.log(`Socket [${socket.id}] auth token: ${player.id}`);
91 // ref to get the socket id for a particular player
92 cache.set(`socket:${player.id}`, socket.id);
93 // ref to get the player object
94 cache.set(`token:${player.id}`, player);
96 socket.emit('authToken', player.id);
98 socket.emit('chat', broadcastMessage('server', `${player.username} just logged in`));
100 socket.on('disconnect', () => {
101 console.log(`Player ${player.username} left`);
104 socket.on('logout', async () => {
105 // clear this player from the cache!
106 const player = cache.get(`token:${socket.handshake.headers['x-authtoken']}`);
108 logger.log(`Player ${player.username} logged out`);
111 logger.log(`Invalid user logout`);
115 // this is a special event to let the client know it can start
117 socket.emit('ready');
120 async function fightRound(player: Player, monster: MonsterWithFaction, data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) {
121 const playerSkills = await getPlayerSkillsAsObject(player.id);
122 const roundData: FightRound = {
125 winner: 'in-progress',
126 fightTrigger: monster.fight_trigger,
134 const equippedItems = await getEquippedItems(player.id);
136 // we only use this if the player successfully defeated the monster
137 // they were fighting, then we load the other monsters in this area
138 // so they can "fight again"
139 let potentialMonsters: MonsterForFight[] = [];
142 * cumulative chance of head/arms/body/legs
146 * we use the factor to decide how many decimal places
150 const monsterTarget = [0.2, 0.4, 0.9, 1];
151 const targets: ArmourEquipmentSlot[] = ['HEAD', 'CHEST', 'ARMS', 'LEGS'];
153 const rand = Math.ceil(Math.random() * factor);
154 let target: ArmourEquipmentSlot = 'CHEST';
155 monsterTarget.forEach((i, idx) => {
156 if (rand > (i * factor)) {
157 target = targets[idx] as ArmourEquipmentSlot;
170 const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
171 const weapons: EquippedItemDetails[] = [];
172 let anyDamageSpells: boolean = false;
173 equippedItems.forEach(item => {
174 if(item.type === 'ARMOUR') {
175 equipment.set(item.equipment_slot, item);
177 else if(item.type === 'WEAPON') {
180 else if(item.type === 'SPELL') {
181 if(item.affectedSkills.includes('destruction_magic')) {
182 anyDamageSpells = true;
187 boost.strength += item.boosts.strength;
188 boost.constitution += item.boosts.constitution;
189 boost.dexterity += item.boosts.dexterity;
190 boost.intelligence += item.boosts.intelligence;
192 if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
193 boost.hp += item.boosts.damage;
196 boost.damage += item.boosts.damage;
200 // if you flee'd, then we want to check your dex vs. the monsters
201 // but we want to give you the item/weapon boosts you need
202 // if not then you're going to get hit.
203 if(data.action === 'flee') {
204 roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
205 roundData.winner = 'monster';
206 await clearFight(player.id);
208 return { roundData, monsters: [], player };
211 const attackType = data.action === 'attack' ? 'physical' : 'magical';
212 const primaryStat = data.action === 'attack' ? player.strength : player.intelligence;
213 const boostStat = data.action === 'attack' ? boost.strength : boost.intelligence;
215 const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
216 const skillsUsed: Record<SkillID | any, number> = {};
217 let hpHealAfterMasteries: number = -1;
218 let playerDamageAfterMasteries: number = 0;
220 weapons.forEach(item => {
221 item.affectedSkills.forEach(id => {
222 if(id === 'restoration_magic') {
223 if(hpHealAfterMasteries < 0) {
224 hpHealAfterMasteries = 0;
226 hpHealAfterMasteries += Skills.get(id).effect(playerSkills.get(id));
229 playerDamageAfterMasteries += playerDamage * Skills.get(id).effect(playerSkills.get(id));
232 if(!skillsUsed[id]) {
239 await updatePlayerSkills(player.id, skillsUsed);
241 const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
242 const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
244 roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
245 let armourKey: string;
246 switch(data.target) {
248 armourKey = 'armsAp';
251 armourKey = 'helmAp';
254 armourKey = 'legsAp';
257 armourKey = 'chestAp';
261 if(monster[armourKey] && monster[armourKey] > 0) {
262 monster[armourKey] -= playerFinalDamage;
264 roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
265 if(monster[armourKey] < 0) {
266 roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
267 roundData.roundDetails.push(`You dealt ${monster[armourKey] * -1} damage to their HP`);
268 monster.hp += monster[armourKey];
269 monster[armourKey] = 0;
273 roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
274 monster.hp -= playerFinalDamage;
277 if(monster.hp <= 0) {
278 roundData.monster.hp = 0;
279 roundData.winner = 'player';
281 roundData.rewards.exp = monster.exp;
282 roundData.rewards.gold = monster.gold;
284 player.gold += monster.gold;
285 player.exp += monster.exp;
287 if(player.exp >= expToLevel(player.level + 1)) {
288 player.exp -= expToLevel(player.level + 1)
290 roundData.rewards.levelIncrease = true;
291 let statPointsGained = 1;
293 if(player.profession !== 'Wanderer') {
294 statPointsGained = 2;
297 player.stat_points += statPointsGained;
299 roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
301 player.hp = maxHp(player.constitution, player.level);
303 // get the monster location if it was an EXPLORED fight
304 if(roundData.fightTrigger === 'explore') {
305 const rawMonster = await loadMonster(monster.ref_id);
306 const monsterList = await getMonsterList(rawMonster.location_id);
307 potentialMonsters = monsterList.map(monster => {
311 level: monster.level,
313 maxHp: monster.maxHp,
314 fight_trigger: 'explore'
319 await clearFight(player.id);
320 await updatePlayer(player);
321 return { roundData, monsters: potentialMonsters, player };
324 roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
325 if(equipment.has(target)) {
326 const item = equipment.get(target);
328 const mitigationPercentage = item.boosts.damage_mitigation || 0;
329 const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
331 item.currentAp -= damageAfterMitigation;
333 if(item.currentAp < 0) {
334 roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
335 roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
336 player.hp += item.currentAp;
338 await deleteInventoryItem(player.id, item.item_id);
341 roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
342 await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
347 roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
348 player.hp -= monster.strength;
351 if(playerFinalHeal > 0) {
352 player.hp += playerFinalHeal;
353 if(player.hp > maxHp(player.constitution, player.level)) {
354 player.hp = maxHp(player.constitution, player.level);
356 roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
359 // update the players inventory for this item!
363 roundData.winner = 'monster';
365 roundData.roundDetails.push(`You were killed by the ${monster.name}`);
367 await clearFight(player.id);
368 await updatePlayer(player);
369 await clearTravelPlan(player.id);
371 return { roundData, monsters: [], player};
374 await updatePlayer(player);
375 await saveFightState(player.id, monster);
377 return { roundData, monsters: [], player};
380 app.use(healerRouter);
383 app.get('/chat/history', authEndpoint, async (req: Request, res: Response) => {
384 const authToken = req.headers['x-authtoken'].toString();
385 const player: Player = await loadPlayer(authToken)
388 logger.log(`Couldnt find player with id ${authToken}`);
389 return res.sendStatus(400);
392 let html = chatHistory.map(renderChatMessage);
394 res.send(html.join("\n"));
397 app.post('/chat', authEndpoint, async (req: Request, res: Response) => {
398 const authToken = req.headers['x-authtoken'].toString();
399 const player: Player = await loadPlayer(authToken)
402 logger.log(`Couldnt find player with id ${authToken}`);
403 return res.sendStatus(400);
406 const msg = req.body.message.trim();
408 if(!msg || !msg.length) {
413 let message: Message;
414 if(msg.startsWith('/server lmnop')) {
415 if(msg === '/server lmnop refresh-monsters') {
416 await createMonsters();
417 message = broadcastMessage('server', 'Monster refresh!');
419 else if(msg === '/server lmnop refresh-cities') {
420 await createAllCitiesAndLocations();
421 message = broadcastMessage('server', 'Cities, Locations, and Paths refreshed!');
423 else if(msg === '/server lmnop refresh-shops') {
424 await createShopItems();
425 message = broadcastMessage('server', 'Refresh shop items');
428 const str = msg.split('/server lmnop ')[1];
430 message = broadcastMessage('server', str);
437 message = broadcastMessage(player.username, msg);
438 chatHistory.push(message);
439 chatHistory.slice(-10);
441 io.emit('chat', message);
446 io.emit('chat', message);
451 app.get('/player', authEndpoint, async (req: Request, res: Response) => {
452 const authToken = req.headers['x-authtoken'].toString();
453 const player: Player = await loadPlayer(authToken)
456 logger.log(`Couldnt find player with id ${authToken}`);
457 return res.sendStatus(400);
460 const inventory = await getEquippedItems(player.id);
462 res.send(renderPlayerBar(player, inventory) + (await renderProfilePage(player)));
465 app.get('/player/skills', authEndpoint, async (req: Request, res: Response) => {
466 const authToken = req.headers['x-authtoken'].toString();
467 const player: Player = await loadPlayer(authToken)
470 logger.log(`Couldnt find player with id ${authToken}`);
471 return res.sendStatus(400);
474 const skills = await getPlayerSkills(player.id);
476 res.send(renderSkills(skills));
479 app.get('/player/inventory', authEndpoint, async (req: Request, res: Response) => {
480 const authToken = req.headers['x-authtoken'].toString();
481 const player: Player = await loadPlayer(authToken)
484 logger.log(`Couldnt find player with id ${authToken}`);
485 return res.sendStatus(400);
488 const [inventory, items] = await Promise.all([
489 getInventory(player.id),
490 getPlayersItems(player.id)
493 res.send(renderInventoryPage(inventory, items));
496 app.post('/player/equip/:item_id/:slot', authEndpoint, async (req: Request, res: Response) => {
497 const authToken = req.headers['x-authtoken'].toString();
498 const player: Player = await loadPlayer(authToken)
501 logger.log(`Couldnt find player with id ${authToken}`);
502 return res.sendStatus(400);
505 const inventoryItem = await getInventoryItem(player.id, req.params.item_id);
506 const equippedItems = await getEquippedItems(player.id);
507 const requestedSlot = req.params.slot;
508 let desiredSlot: EquipmentSlot = inventoryItem.equipment_slot;
511 // handes the situation where you're trying to equip an item
512 // that can be equipped to any hand
513 if(inventoryItem.equipment_slot === 'ANY_HAND') {
514 if(requestedSlot === 'LEFT_HAND' || requestedSlot === 'RIGHT_HAND') {
515 // get the players current equipment in that slot!
516 if(equippedItems.some(v => {
517 return v.equipment_slot === requestedSlot || v.equipment_slot === 'TWO_HANDED';
522 desiredSlot = requestedSlot;
527 if(requestedSlot === 'TWO_HANDED') {
528 if(equippedItems.some(v => {
529 return v.equipment_slot === 'LEFT_HAND' || v.equipment_slot === 'RIGHT_HAND';
536 await equip(player.id, inventoryItem, desiredSlot);
537 const socketId = cache.get(`socket:${player.id}`).toString();
538 io.to(socketId).emit('updatePlayer', player);
539 io.to(socketId).emit('alert', {
541 text: `You equipped your ${inventoryItem.name}`
548 const [inventory, items] = await Promise.all([
549 getInventory(player.id),
550 getPlayersItems(player.id)
553 res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(player, inventory));
556 app.post('/player/unequip/:item_id', authEndpoint, async (req: Request, res: Response) => {
557 const authToken = req.headers['x-authtoken'].toString();
558 const player: Player = await loadPlayer(authToken)
561 logger.log(`Couldnt find player with id ${authToken}`);
562 return res.sendStatus(400);
565 const [item, ] = await Promise.all([
566 getInventoryItem(player.id, req.params.item_id),
567 unequip(player.id, req.params.item_id)
570 const [inventory, items] = await Promise.all([
571 getInventory(player.id),
572 getPlayersItems(player.id)
575 res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(player, inventory));
578 app.get('/player/explore', authEndpoint, async (req: Request, res: Response) => {
579 const authToken = req.headers['x-authtoken'].toString();
580 const player: Player = await loadPlayer(authToken)
583 logger.log(`Couldnt find player with id ${authToken}`);
584 return res.sendStatus(400);
588 const fight = await loadMonsterFromFight(player.id);
589 let closestTown = player.city_id;
592 // ok lets display the fight screen!
593 const data: MonsterForFight = {
599 fight_trigger: fight.fight_trigger
602 res.send(renderFight(data));
605 const travelPlan = await getTravelPlan(player.id);
608 const travelPlan = await getTravelPlan(player.id);
610 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
612 const chanceToSeeMonster = random(0, 100);
613 const things: any[] = [];
614 if(chanceToSeeMonster <= 30) {
615 const monster = await getRandomMonster([closest]);
616 things.push(monster);
620 const nextAction = cache[`step:${player.id}`] || 0;
622 res.send(renderTravel({
625 closestTown: closest,
630 // display the city info!
631 const [city, locations, paths] = await Promise.all([
632 getCityDetails(player.city_id),
633 getAllServices(player.city_id),
634 getAllPaths(player.city_id)
637 res.send(await renderMap({city, locations, paths}, closestTown));
643 // used to purchase equipment from a particular shop
644 app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: Request, res: Response) => {
645 const authToken = req.headers['x-authtoken'].toString();
646 const player: Player = await loadPlayer(authToken)
648 logger.log(`Couldnt find player with id ${authToken}`);
649 return res.sendStatus(400);
652 const item = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
655 logger.log(`Invalid item [${req.params.item_id}]`);
656 return res.sendStatus(400);
659 if(player.gold < item.cost) {
660 res.send(Alert.ErrorAlert(`Sorry, you need at least ${item.cost.toLocaleString()}G to purchase this.`));
664 player.gold -= item.cost;
666 await updatePlayer(player);
667 await addInventoryItem(player.id, item);
669 const equippedItems = await getEquippedItems(player.id);
671 res.send(renderPlayerBar(player, equippedItems) + Alert.SuccessAlert(`You purchased ${item.name}`));
674 // used to purchase items from a particular shop
675 app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: Request, res: Response) => {
676 const authToken = req.headers['x-authtoken'].toString();
677 const player: Player = await loadPlayer(authToken)
679 logger.log(`Couldnt find player with id ${authToken}`);
680 return res.sendStatus(400);
683 const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
686 logger.log(`Invalid item [${req.params.item_id}]`);
687 return res.sendStatus(400);
690 if(player.gold < item.price_per_unit) {
691 res.send(Alert.ErrorAlert(`Sorry, you need at least ${item.price_per_unit.toLocaleString()}G to purchase this.`));
695 player.gold -= item.price_per_unit;
697 await updatePlayer(player);
698 await givePlayerItem(player.id, item.id, 1);
700 const equippedItems = await getEquippedItems(player.id);
702 res.send(renderPlayerBar(player, equippedItems) + Alert.SuccessAlert(`You purchased a ${item.name}`));
705 // used to display equipment modals in a store, validates that
706 // the equipment is actually in this store before displaying
708 app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, async (req: Request, res: Response) => {
709 const authToken = req.headers['x-authtoken'].toString();
710 const player: Player = await loadPlayer(authToken)
712 logger.log(`Couldnt find player with id ${authToken}`);
713 return res.sendStatus(400);
716 const equipment = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
719 logger.log(`Invalid equipment [${req.params.item_id}]`);
720 return res.sendStatus(400);
725 <div class="item-modal-overview">
727 <img src="https://via.placeholder.com/64x64" title="${equipment.name}" alt="${equipment.name}">
730 ${renderEquipmentDetails(equipment, player)}
733 <div class="actions">
734 <button hx-put="/location/${equipment.location_id}/equipment/${equipment.id}" formmethod="dialog" value="cancel">Buy</button>
735 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
743 // used to display item modals in a store, validates that
744 // the item is actually in this store before displaying
746 app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (req: Request, res: Response) => {
747 const authToken = req.headers['x-authtoken'].toString();
748 const player: Player = await loadPlayer(authToken)
750 logger.log(`Couldnt find player with id ${authToken}`);
751 return res.sendStatus(400);
754 const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
757 logger.log(`Invalid item [${req.params.item_id}]`);
758 return res.sendStatus(400);
763 <div class="item-modal-overview">
765 <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}">
768 <h4>${item.name}</h4>
769 <p>${item.description}</p>
772 <div class="actions">
773 <button hx-put="/location/${item.location_id}/items/${item.id}" formmethod="dialog" value="cancel">Buy</button>
774 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
782 app.put('/item/:item_id', authEndpoint, async (req: Request, res: Response) => {
783 const authToken = req.headers['x-authtoken'].toString();
784 const player: Player = await loadPlayer(authToken)
786 logger.log(`Couldnt find player with id ${authToken}`);
787 return res.sendStatus(400);
790 const item: PlayerItem = await getItemFromPlayer(player.id, parseInt(req.params.item_id));
793 console.log(`Can't find item [${req.params.item_id}]`);
797 if(item.amount < 1) {
798 res.send(Alert.ErrorAlert(`You dont have enough ${item.name}`));
804 switch(item.effect_name) {
806 const hpGain = HealthPotionSmall.effect(player);
810 if(player.hp > maxHp(player.constitution, player.level)) {
811 player.hp = maxHp(player.constitution, player.level);
816 await updateItemCount(player.id, item.item_id, -1);
817 await updatePlayer(player);
819 const inventory = await getInventory(player.id);
820 const equippedItems = inventory.filter(i => i.is_equipped);
821 const items = await getPlayersItems(player.id);
825 renderPlayerBar(player, equippedItems),
826 renderInventoryPage(inventory, items, 'ITEMS'),
827 Alert.SuccessAlert(`You used the ${item.name}`)
833 app.get('/modal/items/:item_id', authEndpoint, async (req: Request, res: Response) => {
834 const authToken = req.headers['x-authtoken'].toString();
835 const player: Player = await loadPlayer(authToken)
837 logger.log(`Couldnt find player with id ${authToken}`);
838 return res.sendStatus(400);
841 const item: PlayerItem = await getItemFromPlayer(player.id, parseInt(req.params.item_id));
844 logger.log(`Invalid item [${req.params.item_id}]`);
845 return res.sendStatus(400);
850 <div class="item-modal-overview">
852 <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}">
855 <h4>${item.name}</h4>
856 <p>${item.description}</p>
859 <div class="actions">
860 <button hx-put="/item/${item.item_id}" formmethod="dialog" value="cancel" hx-target="#inventory">Use</button>
861 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
869 app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: Request, res: Response) => {
870 const authToken = req.headers['x-authtoken'].toString();
871 const player: Player = await loadPlayer(authToken)
873 logger.log(`Couldnt find player with id ${authToken}`);
874 return res.sendStatus(400);
877 const location = await getService(parseInt(req.params.location_id));
879 if(!location || location.city_id !== player.city_id) {
880 logger.log(`Invalid location: [${req.params.location_id}]`);
883 const [shopEquipment, shopItems] = await Promise.all([
884 listShopItems({location_id: location.id}),
885 getShopItems(location.id)
888 const html = await renderStore(shopEquipment, shopItems, player);
893 app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: Request, res: Response) => {
894 const authToken = req.headers['x-authtoken'].toString();
895 const player: Player = await loadPlayer(authToken)
897 logger.log(`Couldnt find player with id ${authToken}`);
898 return res.sendStatus(400);
901 const location = await getService(parseInt(req.params.location_id));
902 if(!location || location.city_id !== player.city_id) {
904 logger.log(`Invalid location: [${req.params.location_id}]`);
908 const monsters: Monster[] = await getMonsterList(location.id);
909 res.send(renderMonsterSelector(monsters));
912 app.post('/travel', authEndpoint, async (req: Request, res: Response) => {
913 const authToken = req.headers['x-authtoken'].toString();
914 const player: Player = await loadPlayer(authToken)
915 const destination_id = parseInt(req.body.destination_id);
918 logger.log(`Couldnt find player with id ${authToken}`);
919 return res.sendStatus(400);
922 if(!destination_id || isNaN(destination_id)) {
923 logger.log(`Invalid destination_id [${req.body.destination_id}]`);
924 return res.sendStatus(400);
927 const travelPlan = travel(player, req.body.destination_id);
929 res.json(travelPlan);
932 app.post('/fight/turn', authEndpoint, async (req: Request, res: Response) => {
933 const authToken = req.headers['x-authtoken'].toString();
934 const player: Player = await loadPlayer(authToken)
937 logger.log(`Couldnt find player with id ${authToken}`);
938 return res.sendStatus(400);
941 const monster = await loadMonsterWithFaction(player.id);
944 res.send(Alert.ErrorAlert('Not in a fight'));
948 const fightData = await fightRound(player, monster, {
949 action: req.body.action,
950 target: req.body.fightTarget
953 let html = renderFight(
955 renderRoundDetails(fightData.roundData),
956 fightData.roundData.winner === 'in-progress'
959 if(fightData.monsters.length && monster.fight_trigger === 'explore') {
960 html += renderMonsterSelector(fightData.monsters, monster.ref_id);
963 let travelSection = '';
964 if(monster.fight_trigger === 'travel' && fightData.roundData.winner === 'player') {
965 // you're travellinga dn you won.. display the keep walking!
966 const travelPlan = await getTravelPlan(player.id);
967 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
968 travelSection = travelButton(0);
971 const equippedItems = await getEquippedItems(player.id);
972 const playerBar = renderPlayerBar(fightData.player, equippedItems);
974 res.send(html + travelSection + playerBar);
977 app.post('/fight', authEndpoint, async (req: Request, res: Response) => {
978 const authToken = req.headers['x-authtoken'].toString();
979 const player: Player = await loadPlayer(authToken)
982 logger.log(`Couldnt find player with id ${authToken}`);
983 return res.sendStatus(400);
987 logger.log(`Player didn\'t have enough hp`);
988 return res.sendStatus(400);
991 const monsterId: number = req.body.monsterId;
992 const fightTrigger: FightTrigger = req.body.fightTrigger ?? 'travel';
995 logger.log(`Missing monster Id ${monsterId}`);
996 return res.sendStatus(400);
999 if(!fightTrigger || !['travel', 'explore'].includes(fightTrigger)) {
1000 logger.log(`Invalid fight trigger [${fightTrigger}]`);
1001 return res.sendStatus(400);
1004 const monster = await loadMonster(monsterId);
1007 logger.log(`Couldnt find monster for ${monsterId}`);
1008 return res.sendStatus(400);
1011 const fight = await createFight(player.id, monster, fightTrigger);
1014 const data: MonsterForFight = {
1020 fight_trigger: fight.fight_trigger
1023 res.send(renderFight(data));
1026 app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => {
1027 const authToken = req.headers['x-authtoken'].toString();
1028 const player: Player = await loadPlayer(authToken)
1029 const stepTimerKey = `step:${player.id}`;
1032 logger.log(`Couldnt find player with id ${authToken}`);
1033 return res.sendStatus(400);
1036 const travelPlan = await getTravelPlan(player.id);
1038 res.send(Alert.ErrorAlert('You don\'t have a travel plan'));
1042 if(cache[stepTimerKey]) {
1043 if(cache[stepTimerKey] > Date.now()) {
1044 res.send(Alert.ErrorAlert('Hmm.. travelling too quickly'));
1049 travelPlan.current_position++;
1051 if(travelPlan.current_position >= travelPlan.total_distance) {
1052 const travel = await completeTravel(player.id);
1054 player.city_id = travel.destination_id;
1055 await movePlayer(travel.destination_id, player.id);
1057 const [city, locations, paths] = await Promise.all([
1058 getCityDetails(travel.destination_id),
1059 getAllServices(travel.destination_id),
1060 getAllPaths(travel.destination_id)
1063 delete cache[stepTimerKey];
1064 res.send(await renderMap({city, locations, paths}, player.city_id));
1067 const walkingText: string[] = [
1068 'You take a step forward',
1071 // update existing plan..
1072 // decide if they will run into anything
1073 const travelPlan = await stepForward(player.id);
1075 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
1077 const chanceToSeeMonster = random(0, 100);
1078 const things: any[] = [];
1079 if(chanceToSeeMonster <= 30) {
1080 const monster = await getRandomMonster([closest]);
1081 things.push(monster);
1085 const nextAction = Date.now() + 3000;
1087 cache[stepTimerKey] = nextAction;
1089 res.send(renderTravel({
1092 closestTown: closest,
1093 walkingText: sample(walkingText)
1099 app.post('/travel/:destination_id', authEndpoint, async (req: Request, res: Response) => {
1100 const authToken = req.headers['x-authtoken'].toString();
1101 const player: Player = await loadPlayer(authToken)
1104 logger.log(`Couldnt find player with id ${authToken}`);
1105 return res.sendStatus(400);
1108 if(player.hp <= 0) {
1109 logger.log(`Player didn\'t have enough hp`);
1110 res.send(Alert.ErrorAlert('Sorry, you need some HP to start travelling.'));
1114 const destination = await getCityDetails(parseInt(req.params.destination_id));
1117 res.send(Alert.ErrorAlert(`Thats not a valid desination`));
1121 await travel(player, destination.id);
1123 res.send(renderTravel({
1127 closestTown: player.city_id
1132 app.post('/signup', async (req: Request, res: Response) => {
1133 const {username, password} = req.body;
1134 const authToken = req.headers['x-authtoken'];
1136 if(!username || !password || !authToken) {
1137 res.sendStatus(400);
1143 const player = await loadPlayer(authToken.toString());
1144 logger.log(`Attempted claim for ${player.username}`);
1146 await signup(authToken.toString(), username, password);
1148 await db('players').where({id: player.id}).update({
1149 account_type: 'auth',
1153 logger.log(`Player claimed ${player.username} => ${username}`);
1155 io.emit('chat', broadcastMessage('server', `${player.username} is now ${username}`));
1157 player.username = username;
1158 player.account_type = 'auth';
1166 if(e?.constraint === 'players_username_unique') {
1168 error: 'That username is already taken.'
1172 res.send({error: 'Please try again'}).status(500);
1177 app.post('/login', async (req: Request, res: Response) => {
1178 const {username, password} = req.body;
1180 const player = await login(username, password);
1185 res.json({error: 'That user doesnt exist'}).status(500);
1189 app.get('/status', async (req: Request, res: Response) => {
1191 <div id="server-stats" hx-trigger="load delay:15s" hx-get="/status" hx-swap="outerHTML">
1192 ${io.sockets.sockets.size} Online (v${version})
1197 server.listen(process.env.API_PORT, () => {
1198 logger.log(`Listening on port ${process.env.API_PORT}`);