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 let armourKey: string;
325 switch(data.target) {
327 armourKey = 'armsAp';
330 armourKey = 'helmAp';
333 armourKey = 'legsAp';
336 armourKey = 'chestAp';
340 if(monster[armourKey] && monster[armourKey] > 0) {
341 monster[armourKey] -= playerFinalDamage;
343 roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
344 if(monster[armourKey] < 0) {
345 roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
346 roundData.roundDetails.push(`You dealt ${monster[armourKey] * -1} damage to their HP`);
347 monster.hp += monster[armourKey];
348 monster[armourKey] = 0;
352 roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
353 monster.hp -= playerFinalDamage;
356 if(monster.hp <= 0) {
357 roundData.monster.hp = 0;
358 roundData.winner = 'player';
360 roundData.rewards.exp = monster.exp;
361 roundData.rewards.gold = monster.gold;
363 player.gold += monster.gold;
364 player.exp += monster.exp;
366 if(player.exp >= expToLevel(player.level + 1)) {
367 player.exp -= expToLevel(player.level + 1)
369 roundData.rewards.levelIncrease = true;
370 let statPointsGained = 1;
372 if(player.profession !== 'Wanderer') {
373 statPointsGained = 2;
376 player.stat_points += statPointsGained;
378 roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
380 player.hp = maxHp(player.constitution, player.level);
382 // get the monster location!
383 const rawMonster = await loadMonster(monster.ref_id);
384 const monsterList = await getMonsterList(rawMonster.location_id);
385 potentialMonsters = monsterList.map(monster => {
389 level: monster.level,
395 await clearFight(player.id);
396 await updatePlayer(player);
397 socket.emit('fight-over', {roundData, monsters: potentialMonsters});
401 roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
402 if(equipment.has(target)) {
403 const item = equipment.get(target);
405 const mitigationPercentage = item.boosts.damage_mitigation || 0;
406 const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
408 item.currentAp -= damageAfterMitigation;
410 if(item.currentAp < 0) {
411 roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
412 roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
413 player.hp += item.currentAp;
415 await deleteInventoryItem(player.id, item.item_id);
418 roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
419 await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
424 roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
425 player.hp -= monster.strength;
428 if(playerFinalHeal > 0) {
429 player.hp += playerFinalHeal;
430 if(player.hp > maxHp(player.constitution, player.level)) {
431 player.hp = maxHp(player.constitution, player.level);
433 roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
436 // update the players inventory for this item!
440 roundData.winner = 'monster';
442 roundData.roundDetails.push(`You were killed by the ${monster.name}`);
444 await clearFight(player.id);
445 await updatePlayer(player);
447 socket.emit('fight-over', {roundData, monsters: []});
451 await updatePlayer(player);
452 await saveFightState(player.id, monster);
454 calcAp(equippedItems, socket);
455 socket.emit('fight-round', roundData);
458 // this is a special event to let the client know it can start
460 socket.emit('ready');
463 function authEndpoint(req: Request, res: Response, next: any) {
464 const authToken = req.headers['x-authtoken'];
466 logger.log(`Invalid auth token ${authToken}`);
474 app.get('/city/:id', async (req: Request, res: Response) => {
475 const id = parseInt(req.params.id);
476 if(!id || isNaN(id)) {
477 return res.sendStatus(400);
479 const [city, locations, paths] = await Promise.all([
485 res.json({city, locations, paths});
488 app.get('/fight', authEndpoint, async (req: Request, res: Response) => {
489 const authToken = req.headers['x-authtoken'].toString();
490 const player: Player = await loadPlayer(authToken)
493 logger.log(`Couldnt find player with id ${authToken}`);
494 return res.sendStatus(400);
497 const fight = await loadMonsterFromFight(player.id);
499 res.json(fight || null);
503 app.post('/fight', authEndpoint, async (req: Request, res: Response) => {
504 const authToken = req.headers['x-authtoken'].toString();
505 const player: Player = await loadPlayer(authToken)
508 logger.log(`Couldnt find player with id ${authToken}`);
509 return res.sendStatus(400);
513 logger.log(`Player didn\'t have enough hp`);
514 return res.sendStatus(400);
517 const monsterId: number = req.body.monsterId;
520 logger.log(`Missing monster Id ${monsterId}`);
521 return res.sendStatus(400);
524 const monster = await loadMonster(monsterId);
527 logger.log(`Couldnt find monster for ${monsterId}`);
528 return res.sendStatus(400);
531 const fight = await createFight(player.id, monster);
533 const data: MonsterForFight = {
543 app.post('/signup', async (req: Request, res: Response) => {
544 const {username, password} = req.body;
545 const authToken = req.headers['x-authtoken'];
547 if(!username || !password || !authToken) {
554 const player = await loadPlayer(authToken.toString());
555 console.log(`Attempted claim for ${player.username}`);
557 await signup(authToken.toString(), username, password);
559 await db('players').where({id: player.id}).update({
560 account_type: 'auth',
564 console.log(`Player claimed ${player.username} => ${username}`);
566 io.emit('chat', broadcastMessage('server', `${player.username} is now ${username}`));
568 player.username = username;
569 player.account_type = 'auth';
577 if(e?.constraint === 'players_username_unique') {
579 error: 'That username is already taken.'
583 res.send({error: 'Please try again'}).status(500);
588 app.post('/login', async (req: Request, res: Response) => {
589 const {username, password} = req.body;
591 const player = await login(username, password);
596 res.json({error: 'That user doesnt exist'}).status(500);
600 server.listen(process.env.API_PORT, () => {
601 logger.log(`Listening on port ${process.env.API_PORT}`);