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 http from 'http';
7 import { Server, Socket } from 'socket.io';
8 import { logger } from './lib/logger';
9 import { loadPlayer, createPlayer, updatePlayer } from './player';
10 import * as _ from 'lodash';
11 import {broadcastMessage, Message} from '../shared/message';
12 import {expToLevel, maxHp, Player} from '../shared/player';
13 import { professionList } from '../shared/profession';
14 import {clearFight, createFight, getMonsterList, loadMonster, loadMonsterFromFight, loadMonsterWithFaction, saveFightState} from './monster';
15 import {FightRound} from '../shared/fight';
16 import {addInventoryItem, deleteInventoryItem, getEquippedItems, getInventory, updateAp} from './inventory';
17 import {MonsterForFight} from '../shared/monsters';
18 import {getShopItem } from './shopItem';
19 import {EquippedItemDetails} from '../shared/equipped';
20 import {ArmourEquipmentSlot, EquipmentSlot} from '../shared/inventory';
21 import { getAllPaths, getAllServices, getCityDetails } from './map';
22 import { signup, login } from './auth';
23 import {db} from './lib/db';
24 import { getPlayerSkills, getPlayerSkillsAsObject, updatePlayerSkills } from './skills';
25 import {SkillID, Skills} from '../shared/skills';
26 import * as EventList from '../events/server';
29 import { createMonsters } from '../../seeds/monsters';
30 import { createAllCitiesAndLocations } from '../../seeds/cities';
31 import { createShopItems } from '../../seeds/shop_items';
37 const app = express();
38 const server = http.createServer(app);
40 app.use(express.static(join(__dirname, '..', '..', 'public')));
41 app.use(express.json());
43 const io = new Server(server);
45 const cache: Record<string, any> = {};
46 const chatHistory: Message[] = [];
48 function calcAp(inventoryItem: EquippedItemDetails[], socket: Socket) {
49 const ap: Record<any | EquipmentSlot, {currentAp: number, maxAp: number}> = {};
50 inventoryItem.forEach(item => {
51 if(item.is_equipped && item.type === 'ARMOUR') {
52 ap[item.equipment_slot] = {
53 currentAp: item.currentAp,
59 socket.emit('calc:ap', {ap});
62 function setServerStats() {
63 io.emit('server-stats', {
64 onlinePlayers: io.sockets.sockets.size
68 setTimeout(setServerStats, 5000);
70 io.on('connection', async socket => {
71 logger.log(`socket ${socket.id} connected, authToken: ${socket.handshake.headers['x-authtoken']}`);
73 const authToken = socket.handshake.headers['x-authtoken'].toString() === 'null' ? null : socket.handshake.headers['x-authtoken'].toString();
77 logger.log(`Attempting to load player with id ${authToken}`);
78 player = await loadPlayer(authToken);
81 logger.log(`Creating player`);
82 player = await createPlayer();
85 cache[`token:${player.id}`] = socket.id;
87 logger.log(`Socket [${socket.id}] auth token: ${player.id}`);
93 socket.emit('authToken', player.id);
94 socket.emit('player', player);
96 const inventory = await getEquippedItems(player.id);
97 calcAp(inventory, socket);
101 io.emit('chathistory', chatHistory);
103 socket.emit('chat', broadcastMessage('server', `${player.username} just logged in`));
105 socket.on('disconnect', () => {
106 console.log(`Player ${player.username} left`);
109 socket.on('chat', async (msg: string) => {
112 let message: Message;
113 if(msg.startsWith('/server lmnop')) {
114 if(msg === '/server lmnop refresh-monsters') {
115 await createMonsters();
116 message = broadcastMessage('server', 'Monster refresh!');
118 else if(msg === '/server lmnop refresh-cities') {
119 await createAllCitiesAndLocations();
120 message = broadcastMessage('server', 'Cities, Locations, and Paths refreshed!');
122 else if(msg === '/server lmnop refresh-shops') {
123 await createShopItems();
124 message = broadcastMessage('server', 'Refresh shop items');
127 const str = msg.split('/server lmnop ')[1];
129 message = broadcastMessage('server', str);
134 message = broadcastMessage(player.username, msg);
138 chatHistory.push(message);
139 chatHistory.slice(-10);
140 io.emit('chat', message);
145 socket.on('purchase', async (data) => {
146 const shopItem = await getShopItem(data.id);
149 if(player.gold < shopItem.cost) {
150 socket.emit('alert', {
152 text: `You dont have enough gold to buy the ${shopItem.name}`
157 player.gold -= shopItem.cost;
158 await updatePlayer(player);
159 await addInventoryItem(player.id, shopItem);
161 socket.emit('alert', {
163 text: `You bought the ${shopItem.name}`
166 socket.emit('updatePlayer', player);
171 _.each(EventList, event => {
172 logger.log(`Bound event listener: ${event.eventName}`);
173 socket.on(event.eventName, event.handler.bind(null, {
181 socket.on('skills', async () => {
182 const skills = await getPlayerSkills(player.id);
183 socket.emit('skills', {skills});
186 socket.on('inventory', async () => {
187 const inventory = await getInventory(player.id);
188 socket.emit('inventory', {
193 socket.on('logout', async () => {
194 // clear this player from the cache!
195 console.log(`Player ${player.username} logged out`);
198 socket.on('fight', async (data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) => {
199 const monster = await loadMonsterWithFaction(player.id);
200 const playerSkills = await getPlayerSkillsAsObject(player.id);
201 const roundData: FightRound = {
204 winner: 'in-progress',
212 const equippedItems = await getEquippedItems(player.id);
214 // we only use this if the player successfully defeated the monster
215 // they were fighting, then we load the other monsters in this area
216 // so they can "fight again"
217 let potentialMonsters: MonsterForFight[] = [];
220 * cumulative chance of head/arms/body/legs
224 * we use the factor to decide how many decimal places
228 const monsterTarget = [0.2, 0.4, 0.9, 1];
229 const targets: ArmourEquipmentSlot[] = ['HEAD', 'CHEST', 'ARMS', 'LEGS'];
231 const rand = Math.ceil(Math.random() * factor);
232 let target: ArmourEquipmentSlot = 'CHEST';
233 monsterTarget.forEach((i, idx) => {
234 if (rand > (i * factor)) {
235 target = targets[idx] as ArmourEquipmentSlot;
248 const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
249 const weapons: EquippedItemDetails[] = [];
250 let anyDamageSpells: boolean = false;
251 equippedItems.forEach(item => {
252 if(item.type === 'ARMOUR') {
253 equipment.set(item.equipment_slot, item);
255 else if(item.type === 'WEAPON') {
258 else if(item.type === 'SPELL') {
259 if(item.affectedSkills.includes('destruction_magic')) {
260 anyDamageSpells = true;
265 boost.strength += item.boosts.strength;
266 boost.constitution += item.boosts.constitution;
267 boost.dexterity += item.boosts.dexterity;
268 boost.intelligence += item.boosts.intelligence;
270 if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
271 boost.hp += item.boosts.damage;
274 boost.damage += item.boosts.damage;
278 // if you flee'd, then we want to check your dex vs. the monsters
279 // but we want to give you the item/weapon boosts you need
280 // if not then you're going to get hit.
281 if(data.action === 'flee') {
282 roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
283 roundData.winner = 'monster';
284 await clearFight(player.id);
286 socket.emit('fight-over', {roundData, monsters: []});
290 const attackType = data.action === 'attack' ? 'physical' : 'magical';
291 const primaryStat = data.action === 'attack' ? player.strength : player.intelligence;
292 const boostStat = data.action === 'attack' ? boost.strength : boost.intelligence;
294 const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
295 const skillsUsed: Record<SkillID | any, number> = {};
296 let hpHealAfterMasteries: number = -1;
297 let playerDamageAfterMasteries: number = 0;
299 weapons.forEach(item => {
300 item.affectedSkills.forEach(id => {
301 if(id === 'restoration_magic') {
302 if(hpHealAfterMasteries < 0) {
303 hpHealAfterMasteries = 0;
305 hpHealAfterMasteries += Skills.get(id).effect(playerSkills.get(id));
308 playerDamageAfterMasteries += playerDamage * Skills.get(id).effect(playerSkills.get(id));
311 if(!skillsUsed[id]) {
318 await updatePlayerSkills(player.id, skillsUsed);
320 const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
321 const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
323 roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
324 if(data.target === 'arms') {
325 if(monster.armsAp > 0) {
326 monster.armsAp -= playerFinalDamage;
328 roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
329 if(monster.armsAp < 0) {
331 roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
332 roundData.roundDetails.push(`You dealt ${monster.armsAp * -1} damage to their HP`);
333 monster.hp += monster.armsAp;
338 roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
339 monster.hp -= playerFinalDamage;
342 else if (data.target === 'head') {
343 if(monster.helmAp > 0) {
344 monster.helmAp -= playerFinalDamage;
346 roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
347 if(monster.helmAp < 0) {
349 roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
350 roundData.roundDetails.push(`You dealt ${monster.armsAp * 1} damage to their HP`);
351 monster.hp += monster.helmAp;
356 roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
357 monster.hp -= playerFinalDamage;
360 else if(data.target === 'legs') {
361 if(monster.legsAp > 0) {
362 monster.legsAp -= playerFinalDamage;
364 roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
365 if(monster.legsAp < 0) {
367 roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
368 roundData.roundDetails.push(`You dealt ${monster.legsAp * 1} damage to their HP`);
369 monster.hp += monster.legsAp;
374 roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
375 monster.hp -= playerFinalDamage;
379 if(monster.chestAp > 0) {
380 monster.chestAp -= playerFinalDamage;
382 roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
384 if(monster.chestAp < 0) {
385 roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
386 roundData.roundDetails.push(`You dealt ${monster.chestAp * 1} damage to their HP`);
387 monster.hp += monster.chestAp;
392 roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
393 monster.hp -= playerFinalDamage;
397 if(monster.hp <= 0) {
398 roundData.monster.hp = 0;
399 roundData.winner = 'player';
401 roundData.rewards.exp = monster.exp;
402 roundData.rewards.gold = monster.gold;
404 player.gold += monster.gold;
405 player.exp += monster.exp;
407 if(player.exp >= expToLevel(player.level + 1)) {
408 player.exp -= expToLevel(player.level + 1)
410 roundData.rewards.levelIncrease = true;
411 let statPointsGained = 1;
413 if(player.profession !== 'Wanderer') {
414 statPointsGained = 2;
417 player.stat_points += statPointsGained;
419 roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
421 player.hp = maxHp(player.constitution, player.level);
423 // get the monster location!
424 const rawMonster = await loadMonster(monster.ref_id);
425 const monsterList = await getMonsterList(rawMonster.location_id);
426 potentialMonsters = monsterList.map(monster => {
430 level: monster.level,
436 await clearFight(player.id);
437 await updatePlayer(player);
438 socket.emit('fight-over', {roundData, monsters: potentialMonsters});
442 roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
443 if(equipment.has(target)) {
444 const item = equipment.get(target);
446 const mitigationPercentage = item.boosts.damage_mitigation || 0;
447 const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
449 item.currentAp -= damageAfterMitigation;
451 if(item.currentAp < 0) {
452 roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
453 roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
454 player.hp += item.currentAp;
456 await deleteInventoryItem(player.id, item.item_id);
459 roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
460 await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
465 roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
466 player.hp -= monster.strength;
469 if(playerFinalHeal > 0) {
470 player.hp += playerFinalHeal;
471 if(player.hp > maxHp(player.constitution, player.level)) {
472 player.hp = maxHp(player.constitution, player.level);
474 roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
477 // update the players inventory for this item!
481 roundData.winner = 'monster';
483 roundData.roundDetails.push(`You were killed by the ${monster.name}`);
485 await clearFight(player.id);
486 await updatePlayer(player);
488 socket.emit('fight-over', {roundData, monsters: []});
492 await updatePlayer(player);
493 await saveFightState(player.id, monster);
495 calcAp(equippedItems, socket);
496 socket.emit('fight-round', roundData);
499 // this is a special event to let the client know it can start
501 socket.emit('ready');
504 function authEndpoint(req: Request, res: Response, next: any) {
505 const authToken = req.headers['x-authtoken'];
507 logger.log(`Invalid auth token ${authToken}`);
515 app.get('/city/:id', async (req: Request, res: Response) => {
516 const id = parseInt(req.params.id);
517 if(!id || isNaN(id)) {
518 return res.sendStatus(400);
520 const [city, locations, paths] = await Promise.all([
526 res.json({city, locations, paths});
529 app.get('/fight', authEndpoint, async (req: Request, res: Response) => {
530 const authToken = req.headers['x-authtoken'].toString();
531 const player: Player = await loadPlayer(authToken)
534 logger.log(`Couldnt find player with id ${authToken}`);
535 return res.sendStatus(400);
538 const fight = await loadMonsterFromFight(player.id);
540 res.json(fight || null);
544 app.post('/fight', authEndpoint, async (req: Request, res: Response) => {
545 const authToken = req.headers['x-authtoken'].toString();
546 const player: Player = await loadPlayer(authToken)
549 logger.log(`Couldnt find player with id ${authToken}`);
550 return res.sendStatus(400);
554 logger.log(`Player didn\'t have enough hp`);
555 return res.sendStatus(400);
558 const monsterId: number = req.body.monsterId;
561 logger.log(`Missing monster Id ${monsterId}`);
562 return res.sendStatus(400);
565 const monster = await loadMonster(monsterId);
568 logger.log(`Couldnt find monster for ${monsterId}`);
569 return res.sendStatus(400);
572 const fight = await createFight(player.id, monster);
574 const data: MonsterForFight = {
584 app.post('/signup', async (req: Request, res: Response) => {
585 const {username, password} = req.body;
586 const authToken = req.headers['x-authtoken'];
588 if(!username || !password || !authToken) {
595 const player = await loadPlayer(authToken.toString());
596 console.log(`Attempted claim for ${player.username}`);
598 await signup(authToken.toString(), username, password);
600 await db('players').where({id: player.id}).update({
601 account_type: 'auth',
605 console.log(`Player claimed ${player.username} => ${username}`);
607 io.emit('chat', broadcastMessage('server', `${player.username} is now ${username}`));
609 player.username = username;
610 player.account_type = 'auth';
618 if(e?.constraint === 'players_username_unique') {
620 error: 'That username is already taken.'
624 res.send({error: 'Please try again'}).status(500);
629 app.post('/login', async (req: Request, res: Response) => {
630 const {username, password} = req.body;
632 const player = await login(username, password);
637 res.json({error: 'That user doesnt exist'}).status(500);
641 server.listen(process.env.API_PORT, () => {
642 logger.log(`Listening on port ${process.env.API_PORT}`);