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 } 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, 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, AuthRequest } 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', renderChatMessage(broadcastMessage('server', `${player.username} just logged in`)));
100 socket.on('disconnect', () => {
101 console.log(`Player ${player.username} left`);
104 // this is a special event to let the client know it can start
106 socket.emit('ready');
109 async function fightRound(player: Player, monster: MonsterWithFaction, data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) {
110 const playerSkills = await getPlayerSkillsAsObject(player.id);
111 const roundData: FightRound = {
114 winner: 'in-progress',
115 fightTrigger: monster.fight_trigger,
123 const equippedItems = await getEquippedItems(player.id);
125 // we only use this if the player successfully defeated the monster
126 // they were fighting, then we load the other monsters in this area
127 // so they can "fight again"
128 let potentialMonsters: MonsterForFight[] = [];
131 * cumulative chance of head/arms/body/legs
135 * we use the factor to decide how many decimal places
139 const monsterTarget = [0.2, 0.4, 0.9, 1];
140 const targets: ArmourEquipmentSlot[] = ['HEAD', 'CHEST', 'ARMS', 'LEGS'];
142 const rand = Math.ceil(Math.random() * factor);
143 let target: ArmourEquipmentSlot = 'CHEST';
144 monsterTarget.forEach((i, idx) => {
145 if (rand > (i * factor)) {
146 target = targets[idx] as ArmourEquipmentSlot;
159 const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
160 const weapons: EquippedItemDetails[] = [];
161 let anyDamageSpells: boolean = false;
162 equippedItems.forEach(item => {
163 if(item.type === 'ARMOUR') {
164 equipment.set(item.equipment_slot, item);
166 else if(item.type === 'WEAPON') {
169 else if(item.type === 'SPELL') {
170 if(item.affectedSkills.includes('destruction_magic')) {
171 anyDamageSpells = true;
176 boost.strength += item.boosts.strength;
177 boost.constitution += item.boosts.constitution;
178 boost.dexterity += item.boosts.dexterity;
179 boost.intelligence += item.boosts.intelligence;
181 if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
182 boost.hp += item.boosts.damage;
185 boost.damage += item.boosts.damage;
189 // if you flee'd, then we want to check your dex vs. the monsters
190 // but we want to give you the item/weapon boosts you need
191 // if not then you're going to get hit.
192 if(data.action === 'flee') {
193 roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
194 roundData.winner = 'monster';
195 await clearFight(player.id);
197 return { roundData, monsters: [], player };
200 const attackType = data.action === 'attack' ? 'physical' : 'magical';
201 const primaryStat = data.action === 'attack' ? player.strength : player.intelligence;
202 const boostStat = data.action === 'attack' ? boost.strength : boost.intelligence;
204 const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
205 const skillsUsed: Record<SkillID | any, number> = {};
206 let hpHealAfterMasteries: number = -1;
207 let playerDamageAfterMasteries: number = 0;
209 weapons.forEach(item => {
210 item.affectedSkills.forEach(id => {
211 if(id === 'restoration_magic') {
212 if(hpHealAfterMasteries < 0) {
213 hpHealAfterMasteries = 0;
215 hpHealAfterMasteries += Skills.get(id).effect(playerSkills.get(id));
218 playerDamageAfterMasteries += playerDamage * Skills.get(id).effect(playerSkills.get(id));
221 if(!skillsUsed[id]) {
228 await updatePlayerSkills(player.id, skillsUsed);
230 const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
231 const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
233 roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
234 let armourKey: string;
235 switch(data.target) {
237 armourKey = 'armsAp';
240 armourKey = 'helmAp';
243 armourKey = 'legsAp';
246 armourKey = 'chestAp';
250 if(monster[armourKey] && monster[armourKey] > 0) {
251 monster[armourKey] -= playerFinalDamage;
253 roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
254 if(monster[armourKey] < 0) {
255 roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
256 roundData.roundDetails.push(`You dealt ${monster[armourKey] * -1} damage to their HP`);
257 monster.hp += monster[armourKey];
258 monster[armourKey] = 0;
262 roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
263 monster.hp -= playerFinalDamage;
266 if(monster.hp <= 0) {
267 roundData.monster.hp = 0;
268 roundData.winner = 'player';
270 roundData.rewards.exp = monster.exp;
271 roundData.rewards.gold = monster.gold;
273 player.gold += monster.gold;
274 player.exp += monster.exp;
276 if(player.exp >= expToLevel(player.level + 1)) {
277 player.exp -= expToLevel(player.level + 1)
279 roundData.rewards.levelIncrease = true;
280 let statPointsGained = 1;
282 if(player.profession !== 'Wanderer') {
283 statPointsGained = 2;
286 player.stat_points += statPointsGained;
288 roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
290 player.hp = maxHp(player.constitution, player.level);
292 // get the monster location if it was an EXPLORED fight
293 if(roundData.fightTrigger === 'explore') {
294 const rawMonster = await loadMonster(monster.ref_id);
295 const monsterList = await getMonsterList(rawMonster.location_id);
296 potentialMonsters = monsterList.map(monster => {
300 level: monster.level,
302 maxHp: monster.maxHp,
303 fight_trigger: 'explore'
308 await clearFight(player.id);
309 await updatePlayer(player);
310 return { roundData, monsters: potentialMonsters, player };
313 roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
314 if(equipment.has(target)) {
315 const item = equipment.get(target);
317 const mitigationPercentage = item.boosts.damage_mitigation || 0;
318 const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
320 item.currentAp -= damageAfterMitigation;
322 if(item.currentAp < 0) {
323 roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
324 roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
325 player.hp += item.currentAp;
327 await deleteInventoryItem(player.id, item.item_id);
330 roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
331 await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
336 roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
337 player.hp -= monster.strength;
340 if(playerFinalHeal > 0) {
341 player.hp += playerFinalHeal;
342 if(player.hp > maxHp(player.constitution, player.level)) {
343 player.hp = maxHp(player.constitution, player.level);
345 roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
348 // update the players inventory for this item!
352 roundData.winner = 'monster';
354 roundData.roundDetails.push(`You were killed by the ${monster.name}`);
356 await clearFight(player.id);
357 await updatePlayer(player);
358 await clearTravelPlan(player.id);
360 return { roundData, monsters: [], player};
363 await updatePlayer(player);
364 await saveFightState(player.id, monster);
366 return { roundData, monsters: [], player};
369 app.use(healerRouter);
372 app.get('/chat/history', authEndpoint, async (req: AuthRequest, res: Response) => {
373 let html = chatHistory.map(renderChatMessage);
375 res.send(html.join("\n"));
378 app.post('/chat', authEndpoint, async (req: AuthRequest, res: Response) => {
379 const msg = req.body.message.trim();
381 if(!msg || !msg.length) {
386 let message: Message;
387 if(msg.startsWith('/server lmnop')) {
388 if(msg === '/server lmnop refresh-monsters') {
389 await createMonsters();
390 message = broadcastMessage('server', 'Monster refresh!');
392 else if(msg === '/server lmnop refresh-cities') {
393 await createAllCitiesAndLocations();
394 message = broadcastMessage('server', 'Cities, Locations, and Paths refreshed!');
396 else if(msg === '/server lmnop refresh-shops') {
397 await createShopItems();
398 message = broadcastMessage('server', 'Refresh shop items');
401 const str = msg.split('/server lmnop ')[1];
403 message = broadcastMessage('server', str);
408 message = broadcastMessage(req.player.username, msg);
409 chatHistory.push(message);
410 chatHistory.slice(-10);
414 io.emit('chat', renderChatMessage(message));
419 app.get('/player', authEndpoint, async (req: AuthRequest, res: Response) => {
420 const inventory = await getEquippedItems(req.player.id);
422 res.send(renderPlayerBar(req.player, inventory) + renderProfilePage(req.player));
425 app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Response) => {
426 const stat = req.params.stat;
427 if(!['strength', 'constitution', 'dexterity', 'intelligence'].includes(stat)) {
428 res.send(Alert.ErrorAlert(`Sorry, that's not a valid stat to increase`));
432 if(req.player.stat_points <= 0) {
433 res.send(Alert.ErrorAlert(`Sorry, you don't have enough stat points`));
437 req.player.stat_points -= 1;
440 updatePlayer(req.player);
442 const equippedItems = await getEquippedItems(req.player.id);
443 res.send(renderPlayerBar(req.player, equippedItems) + renderProfilePage(req.player));
446 app.get('/player/skills', authEndpoint, async (req: AuthRequest, res: Response) => {
447 const skills = await getPlayerSkills(req.player.id);
449 res.send(renderSkills(skills));
452 app.get('/player/inventory', authEndpoint, async (req: AuthRequest, res: Response) => {
453 const [inventory, items] = await Promise.all([
454 getInventory(req.player.id),
455 getPlayersItems(req.player.id)
458 res.send(renderInventoryPage(inventory, items));
461 app.post('/player/equip/:item_id/:slot', authEndpoint, async (req: AuthRequest, res: Response) => {
462 const inventoryItem = await getInventoryItem(req.player.id, req.params.item_id);
463 const equippedItems = await getEquippedItems(req.player.id);
464 const requestedSlot = req.params.slot;
465 let desiredSlot: EquipmentSlot = inventoryItem.equipment_slot;
468 // handes the situation where you're trying to equip an item
469 // that can be equipped to any hand
470 if(inventoryItem.equipment_slot === 'ANY_HAND') {
471 if(requestedSlot === 'LEFT_HAND' || requestedSlot === 'RIGHT_HAND') {
472 // get the players current equipment in that slot!
473 if(equippedItems.some(v => {
474 return v.equipment_slot === requestedSlot || v.equipment_slot === 'TWO_HANDED';
479 desiredSlot = requestedSlot;
484 if(requestedSlot === 'TWO_HANDED') {
485 if(equippedItems.some(v => {
486 return v.equipment_slot === 'LEFT_HAND' || v.equipment_slot === 'RIGHT_HAND';
493 await equip(req.player.id, inventoryItem, desiredSlot);
494 const socketId = cache.get(`socket:${req.player.id}`).toString();
495 io.to(socketId).emit('updatePlayer', req.player);
496 io.to(socketId).emit('alert', {
498 text: `You equipped your ${inventoryItem.name}`
505 const [inventory, items] = await Promise.all([
506 getInventory(req.player.id),
507 getPlayersItems(req.player.id)
510 res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(req.player, inventory));
513 app.post('/player/unequip/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
514 const [item, ] = await Promise.all([
515 getInventoryItem(req.player.id, req.params.item_id),
516 unequip(req.player.id, req.params.item_id)
519 const [inventory, items] = await Promise.all([
520 getInventory(req.player.id),
521 getPlayersItems(req.player.id)
524 res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player, inventory));
527 app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) => {
528 const fight = await loadMonsterFromFight(req.player.id);
529 let closestTown = req.player.city_id;
532 // ok lets display the fight screen!
533 const data: MonsterForFight = {
539 fight_trigger: fight.fight_trigger
542 res.send(renderFight(data));
545 const travelPlan = await getTravelPlan(req.player.id);
548 const travelPlan = await getTravelPlan(req.player.id);
550 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
552 const chanceToSeeMonster = random(0, 100);
553 const things: any[] = [];
554 if(chanceToSeeMonster <= 30) {
555 const monster = await getRandomMonster([closest]);
556 things.push(monster);
560 const nextAction = cache[`step:${req.player.id}`] || 0;
562 res.send(renderTravel({
565 closestTown: closest,
570 // display the city info!
571 const [city, locations, paths] = await Promise.all([
572 getCityDetails(req.player.city_id),
573 getAllServices(req.player.city_id),
574 getAllPaths(req.player.city_id)
577 res.send(await renderMap({city, locations, paths}, closestTown));
583 // used to purchase equipment from a particular shop
584 app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
585 const item = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
588 logger.log(`Invalid item [${req.params.item_id}]`);
589 return res.sendStatus(400);
592 if(req.player.gold < item.cost) {
593 res.send(Alert.ErrorAlert(`Sorry, you need at least ${item.cost.toLocaleString()}G to purchase this.`));
597 req.player.gold -= item.cost;
599 await updatePlayer(req.player);
600 await addInventoryItem(req.player.id, item);
602 const equippedItems = await getEquippedItems(req.player.id);
604 res.send(renderPlayerBar(req.player, equippedItems) + Alert.SuccessAlert(`You purchased ${item.name}`));
607 // used to purchase items from a particular shop
608 app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
609 const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
612 logger.log(`Invalid item [${req.params.item_id}]`);
613 return res.sendStatus(400);
616 if(req.player.gold < item.price_per_unit) {
617 res.send(Alert.ErrorAlert(`Sorry, you need at least ${item.price_per_unit.toLocaleString()}G to purchase this.`));
621 req.player.gold -= item.price_per_unit;
623 await updatePlayer(req.player);
624 await givePlayerItem(req.player.id, item.id, 1);
626 const equippedItems = await getEquippedItems(req.player.id);
628 res.send(renderPlayerBar(req.player, equippedItems) + Alert.SuccessAlert(`You purchased a ${item.name}`));
631 // used to display equipment modals in a store, validates that
632 // the equipment is actually in this store before displaying
634 app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => {
635 const equipment = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id));
638 logger.log(`Invalid equipment [${req.params.item_id}]`);
639 return res.sendStatus(400);
644 <div class="item-modal-overview">
646 <img src="${equipment.icon ? `/assets/img/icons/equipment/${equipment.icon}` : 'https://via.placeholder.com/64x64'}" title="${equipment.name}" alt="${equipment.name}">
649 ${renderEquipmentDetails(equipment, req.player)}
652 <div class="actions">
653 <button hx-put="/location/${equipment.location_id}/equipment/${equipment.id}" formmethod="dialog" value="cancel">Buy</button>
654 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
662 // used to display item modals in a store, validates that
663 // the item is actually in this store before displaying
665 app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => {
666 const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id));
669 logger.log(`Invalid item [${req.params.item_id}]`);
670 return res.sendStatus(400);
675 <div class="item-modal-overview">
677 <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}">
680 <h4>${item.name}</h4>
681 <p>${item.description}</p>
684 <div class="actions">
685 <button hx-put="/location/${item.location_id}/items/${item.id}" formmethod="dialog" value="cancel">Buy</button>
686 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
694 app.put('/item/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
695 const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
698 console.log(`Can't find item [${req.params.item_id}]`);
702 if(item.amount < 1) {
703 res.send(Alert.ErrorAlert(`You dont have enough ${item.name}`));
709 switch(item.effect_name) {
711 const hpGain = HealthPotionSmall.effect(req.player);
713 req.player.hp += hpGain;
715 if(req.player.hp > maxHp(req.player.constitution, req.player.level)) {
716 req.player.hp = maxHp(req.player.constitution, req.player.level);
721 await updateItemCount(req.player.id, item.item_id, -1);
722 await updatePlayer(req.player);
724 const inventory = await getInventory(req.player.id);
725 const equippedItems = inventory.filter(i => i.is_equipped);
726 const items = await getPlayersItems(req.player.id);
730 renderPlayerBar(req.player, equippedItems),
731 renderInventoryPage(inventory, items, 'ITEMS'),
732 Alert.SuccessAlert(`You used the ${item.name}`)
738 app.get('/modal/items/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => {
739 const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id));
742 logger.log(`Invalid item [${req.params.item_id}]`);
743 return res.sendStatus(400);
748 <div class="item-modal-overview">
750 <img src="/assets/img/icons/items/${item.icon_name}" title="${item.name}" alt="${item.name}">
753 <h4>${item.name}</h4>
754 <p>${item.description}</p>
757 <div class="actions">
758 <button hx-put="/item/${item.item_id}" formmethod="dialog" value="cancel" hx-target="#inventory">Use</button>
759 <button class="close-modal" formmethod="dialog" value="cancel">Cancel</button>
767 app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => {
768 const location = await getService(parseInt(req.params.location_id));
770 if(!location || location.city_id !== req.player.city_id) {
771 logger.log(`Invalid location: [${req.params.location_id}]`);
774 const [shopEquipment, shopItems] = await Promise.all([
775 listShopItems({location_id: location.id}),
776 getShopItems(location.id)
779 const html = await renderStore(shopEquipment, shopItems, req.player);
784 app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => {
785 const location = await getService(parseInt(req.params.location_id));
786 if(!location || location.city_id !== req.player.city_id) {
788 logger.log(`Invalid location: [${req.params.location_id}]`);
792 const monsters: Monster[] = await getMonsterList(location.id);
793 res.send(renderMonsterSelector(monsters));
796 app.post('/travel', authEndpoint, async (req: AuthRequest, res: Response) => {
797 const destination_id = parseInt(req.body.destination_id);
799 if(!destination_id || isNaN(destination_id)) {
800 logger.log(`Invalid destination_id [${req.body.destination_id}]`);
801 return res.sendStatus(400);
804 const travelPlan = travel(req.player, req.body.destination_id);
806 res.json(travelPlan);
809 app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) => {
810 const monster = await loadMonsterWithFaction(req.player.id);
813 res.send(Alert.ErrorAlert('Not in a fight'));
817 const fightData = await fightRound(req.player, monster, {
818 action: req.body.action,
819 target: req.body.fightTarget
822 let html = renderFight(
824 renderRoundDetails(fightData.roundData),
825 fightData.roundData.winner === 'in-progress'
828 if(fightData.monsters.length && monster.fight_trigger === 'explore') {
829 html += renderMonsterSelector(fightData.monsters, monster.ref_id);
832 let travelSection = '';
833 if(monster.fight_trigger === 'travel' && fightData.roundData.winner === 'player') {
834 // you're travellinga dn you won.. display the keep walking!
835 const travelPlan = await getTravelPlan(req.player.id);
836 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
837 travelSection = travelButton(0);
840 const equippedItems = await getEquippedItems(req.player.id);
841 const playerBar = renderPlayerBar(fightData.player, equippedItems);
843 res.send(html + travelSection + playerBar);
846 app.post('/fight', authEndpoint, async (req: AuthRequest, res: Response) => {
847 if(req.player.hp <= 0) {
848 logger.log(`Player didn\'t have enough hp`);
849 return res.sendStatus(400);
852 const monsterId: number = req.body.monsterId;
853 const fightTrigger: FightTrigger = req.body.fightTrigger ?? 'travel';
856 logger.log(`Missing monster Id ${monsterId}`);
857 return res.sendStatus(400);
860 if(!fightTrigger || !['travel', 'explore'].includes(fightTrigger)) {
861 logger.log(`Invalid fight trigger [${fightTrigger}]`);
862 return res.sendStatus(400);
865 const monster = await loadMonster(monsterId);
868 logger.log(`Couldnt find monster for ${monsterId}`);
869 return res.sendStatus(400);
872 const fight = await createFight(req.player.id, monster, fightTrigger);
875 const data: MonsterForFight = {
881 fight_trigger: fight.fight_trigger
884 res.send(renderFight(data));
887 app.post('/travel/step', authEndpoint, async (req: AuthRequest, res: Response) => {
888 const stepTimerKey = `step:${req.player.id}`;
890 const travelPlan = await getTravelPlan(req.player.id);
892 res.send(Alert.ErrorAlert('You don\'t have a travel plan'));
896 if(cache[stepTimerKey]) {
897 if(cache[stepTimerKey] > Date.now()) {
898 res.send(Alert.ErrorAlert('Hmm.. travelling too quickly'));
903 travelPlan.current_position++;
905 if(travelPlan.current_position >= travelPlan.total_distance) {
906 const travel = await completeTravel(req.player.id);
908 req.player.city_id = travel.destination_id;
909 await movePlayer(travel.destination_id, req.player.id);
911 const [city, locations, paths] = await Promise.all([
912 getCityDetails(travel.destination_id),
913 getAllServices(travel.destination_id),
914 getAllPaths(travel.destination_id)
917 delete cache[stepTimerKey];
918 res.send(await renderMap({city, locations, paths}, req.player.city_id));
921 const walkingText: string[] = [
922 'You take a step forward',
925 // update existing plan..
926 // decide if they will run into anything
927 const travelPlan = await stepForward(req.player.id);
929 const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
931 const chanceToSeeMonster = random(0, 100);
932 const things: any[] = [];
933 if(chanceToSeeMonster <= 30) {
934 const monster = await getRandomMonster([closest]);
935 things.push(monster);
939 const nextAction = Date.now() + 3000;
941 cache[stepTimerKey] = nextAction;
943 res.send(renderTravel({
946 closestTown: closest,
947 walkingText: sample(walkingText)
953 app.post('/travel/:destination_id', authEndpoint, async (req: AuthRequest, res: Response) => {
954 if(req.player.hp <= 0) {
955 logger.log(`Player didn\'t have enough hp`);
956 res.send(Alert.ErrorAlert('Sorry, you need some HP to start travelling.'));
960 const destination = await getCityDetails(parseInt(req.params.destination_id));
963 res.send(Alert.ErrorAlert(`Thats not a valid desination`));
967 await travel(req.player, destination.id);
969 res.send(renderTravel({
973 closestTown: req.player.city_id
977 app.get('/settings', authEndpoint, async (req: AuthRequest, res: Response) => {
980 if(req.player.account_type === 'session') {
981 warning += `<div class="alert error">If you log out without signing up first, this account is lost forever.</div>`;
984 html += '<a href="#" hx-post="/logout">Logout</a>';
985 res.send(warning + html);
988 app.post('/logout', authEndpoint, async (req: AuthRequest, res: Response) => {
989 // ref to get the socket id for a particular player
990 cache.delete(`socket:${req.player.id}`);
991 // ref to get the player object
992 cache.delete(`token:${req.player.id}`);
994 logger.log(`${req.player.username} logged out`);
1000 app.post('/signup', async (req: Request, res: Response) => {
1001 const {username, password} = req.body;
1002 const authToken = req.headers['x-authtoken'];
1004 if(!username || !password || !authToken) {
1005 res.sendStatus(400);
1011 const player = await loadPlayer(authToken.toString());
1012 logger.log(`Attempted claim for ${player.username}`);
1014 await signup(authToken.toString(), username, password);
1016 await db('players').where({id: player.id}).update({
1017 account_type: 'auth',
1021 logger.log(`Player claimed ${player.username} => ${username}`);
1023 io.emit('chat', broadcastMessage('server', `${player.username} is now ${username}`));
1025 player.username = username;
1026 player.account_type = 'auth';
1034 if(e?.constraint === 'players_username_unique') {
1036 error: 'That username is already taken.'
1040 res.send({error: 'Please try again'}).status(500);
1045 app.post('/login', async (req: Request, res: Response) => {
1046 const {username, password} = req.body;
1048 const player = await login(username, password);
1053 res.json({error: 'That user doesnt exist'}).status(500);
1057 app.get('/status', async (req: Request, res: Response) => {
1059 <div id="server-stats" hx-trigger="load delay:15s" hx-get="/status" hx-swap="outerHTML">
1060 ${io.sockets.sockets.size} Online (v${version})
1065 server.listen(process.env.API_PORT, () => {
1066 logger.log(`Listening on port ${process.env.API_PORT}`);