1 import * as otel from './tracing';
2 import { config as dotenv } from 'dotenv';
3 import { join } from 'path';
4 import express, {Request, Response} from 'express';
5 import http from 'http';
6 import { Server, Socket } from 'socket.io';
7 import { logger } from './lib/logger';
8 import { loadPlayer, createPlayer, updatePlayer } from './player';
9 import * as _ from 'lodash';
10 import {broadcastMessage, Message} from '../shared/message';
11 import {expToLevel, maxHp, Player} from '../shared/player';
12 import { professionList } from '../shared/profession';
13 import {clearFight, createFight, getMonsterList, loadMonster, loadMonsterFromFight, saveFightState} from './monster';
14 import {FightRound} from '../shared/fight';
15 import {addInventoryItem, deleteInventoryItem, getEquippedItems, getInventory, updateAp} from './inventory';
16 import {Monster, MonsterForFight, MonsterForList} from '../shared/monsters';
17 import {getShopItem } from './shopItem';
18 import { v4 as uuid } from 'uuid';
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';
35 const app = express();
36 const server = http.createServer(app);
38 app.use(express.static(join(__dirname, '..', '..', 'public')));
39 app.use(express.json());
41 const io = new Server(server);
43 const cache: Record<string, any> = {};
44 const chatHistory: Message[] = [];
46 function calcAp(inventoryItem: EquippedItemDetails[], socket: Socket) {
47 const ap: Record<any | EquipmentSlot, {currentAp: number, maxAp: number}> = {};
48 inventoryItem.forEach(item => {
49 if(item.is_equipped && item.type === 'ARMOUR') {
50 ap[item.equipment_slot] = {
51 currentAp: item.currentAp,
57 socket.emit('calc:ap', {ap});
60 function setServerStats() {
61 io.emit('server-stats', {
62 onlinePlayers: io.sockets.sockets.size
66 async function socketSendMonsterList(location_id: string, socket: Socket) {
67 const monsters: Monster[] = await getMonsterList(location_id);
68 let data: MonsterForList[] = monsters.map(monster => {
76 socket.emit('explore:fights', data);
79 setTimeout(setServerStats, 5000);
81 io.on('connection', async socket => {
82 logger.log(`socket ${socket.id} connected, authToken: ${socket.handshake.headers['x-authtoken']}`);
84 const authToken = socket.handshake.headers['x-authtoken'].toString() === 'null' ? null : socket.handshake.headers['x-authtoken'].toString();
88 logger.log(`Attempting to load player with id ${authToken}`);
89 player = await loadPlayer(authToken);
92 logger.log(`Creating player`);
93 player = await createPlayer();
96 cache[`token:${player.id}`] = socket.id;
98 logger.log(`Socket [${socket.id}] auth token: ${player.id}`);
100 socket.emit('authToken', player.id);
101 socket.emit('player', player);
103 const inventory = await getEquippedItems(player.id);
104 calcAp(inventory, socket);
108 io.emit('chathistory', chatHistory);
110 socket.emit('chat', broadcastMessage('server', `${player.username} just logged in`));
112 socket.on('chat', async (msg: string) => {
115 let message: Message;
116 if(msg.startsWith('/server lmnop')) {
117 if(msg === '/server lmnop refresh-monsters') {
118 await createMonsters();
119 message = broadcastMessage('server', 'Monster refresh!');
122 message = broadcastMessage('server', msg.split('/server lmnop ')[1]);
126 message = broadcastMessage(player.username, msg);
129 chatHistory.push(message);
130 chatHistory.slice(-10);
131 io.emit('chat', message);
135 socket.on('calc:ap', async () => {
136 const items = await getEquippedItems(player.id);
137 calcAp(items, socket);
140 socket.on('city:explore:alley_1', async () => {
141 socketSendMonsterList('alley_1', socket);
144 socket.on('city:explore:forest_1', async () => {
145 socketSendMonsterList('forest_1', socket);
148 socket.on('purchase', async (data) => {
149 const shopItem = await getShopItem(data.id);
152 shopItem.id = uuid();
153 if(player.gold < shopItem.cost) {
154 socket.emit('alert', {
156 text: `You dont have enough gold to buy the ${shopItem.name}`
161 player.gold -= shopItem.cost;
162 await updatePlayer(player);
163 await addInventoryItem(player.id, shopItem);
165 socket.emit('alert', {
167 text: `You bought the ${shopItem.name}`
170 socket.emit('updatePlayer', player);
175 _.each(EventList, event => {
176 logger.log(`Bound event listener: ${event.eventName}`);
177 socket.on(event.eventName, event.handler.bind(null, {
185 socket.on('skills', async () => {
186 const skills = await getPlayerSkills(player.id);
187 socket.emit('skills', {skills});
190 socket.on('inventory', async () => {
191 const inventory = await getInventory(player.id);
192 socket.emit('inventory', {
197 socket.on('fight', async (data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) => {
198 const monster = await loadMonsterFromFight(player.id);
199 const playerSkills = await getPlayerSkillsAsObject(player.id);
200 const roundData: FightRound = {
203 winner: 'in-progress',
211 const equippedItems = await getEquippedItems(player.id);
213 // we only use this if the player successfully defeated the monster
214 // they were fighting, then we load the other monsters in this area
215 // so they can "fight again"
216 let potentialMonsters: MonsterForFight[] = [];
219 * cumulative chance of head/arms/body/legs
223 * we use the factor to decide how many decimal places
227 const monsterTarget = [0.2, 0.4, 0.9, 1];
228 const targets: ArmourEquipmentSlot[] = ['HEAD', 'CHEST', 'ARMS', 'LEGS'];
230 const rand = Math.ceil(Math.random() * factor);
231 let target: ArmourEquipmentSlot = 'CHEST';
232 monsterTarget.forEach((i, idx) => {
233 if (rand > (i * factor)) {
234 target = targets[idx] as ArmourEquipmentSlot;
247 const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
248 const weapons: EquippedItemDetails[] = [];
249 let anyDamageSpells: boolean = false;
250 equippedItems.forEach(item => {
251 if(item.type === 'ARMOUR') {
252 equipment.set(item.equipment_slot, item);
254 else if(item.type === 'WEAPON') {
257 else if(item.type === 'SPELL') {
258 if(item.affectedSkills.includes('destruction_magic')) {
259 anyDamageSpells = true;
264 boost.strength += item.boosts.strength;
265 boost.constitution += item.boosts.constitution;
266 boost.dexterity += item.boosts.dexterity;
267 boost.intelligence += item.boosts.intelligence;
269 if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
270 boost.hp += item.boosts.damage;
273 boost.damage += item.boosts.damage;
277 // if you flee'd, then we want to check your dex vs. the monsters
278 // but we want to give you the item/weapon boosts you need
279 // if not then you're going to get hit.
280 if(data.action === 'flee') {
281 roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
282 roundData.winner = 'monster';
283 await clearFight(player.id);
285 socket.emit('fight-over', {roundData, monsters: []});
289 const primaryStat = data.action === 'attack' ? player.strength : player.constitution;
290 const boostStat = data.action === 'attack' ? boost.strength : boost.constitution;
291 const attackType = data.action === 'attack' ? 'physical' : 'magical';
293 const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
294 const skillsUsed: Record<SkillID | any, number> = {};
295 let hpHealAfterMasteries: number = -1;
296 let playerDamageAfterMasteries: number = 0;
298 weapons.forEach(item => {
299 item.affectedSkills.forEach(id => {
300 if(id === 'restoration_magic') {
301 if(hpHealAfterMasteries < 0) {
302 hpHealAfterMasteries = 0;
304 hpHealAfterMasteries += Skills.get(id).effect(playerSkills.get(id));
307 playerDamageAfterMasteries += playerDamage * Skills.get(id).effect(playerSkills.get(id));
310 if(!skillsUsed[id]) {
317 await updatePlayerSkills(player.id, skillsUsed);
319 const playerFinalDamage = (attackType === 'magical' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
320 const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
322 roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
323 if(data.target === 'arms') {
324 if(monster.armsAp > 0) {
325 monster.armsAp -= playerFinalDamage;
327 roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
328 if(monster.armsAp < 0) {
330 roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
331 roundData.roundDetails.push(`You dealt ${monster.armsAp * -1} damage to their HP`);
332 monster.hp += monster.armsAp;
337 roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
338 monster.hp -= playerFinalDamage;
341 else if (data.target === 'head') {
342 if(monster.helmAp > 0) {
343 monster.helmAp -= playerFinalDamage;
345 roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
346 if(monster.helmAp < 0) {
348 roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
349 roundData.roundDetails.push(`You dealt ${monster.armsAp * 1} damage to their HP`);
350 monster.hp += monster.helmAp;
355 roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
356 monster.hp -= playerFinalDamage;
359 else if(data.target === 'legs') {
360 if(monster.legsAp > 0) {
361 monster.legsAp -= playerFinalDamage;
363 roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
364 if(monster.legsAp < 0) {
366 roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
367 roundData.roundDetails.push(`You dealt ${monster.legsAp * 1} damage to their HP`);
368 monster.hp += monster.legsAp;
373 roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
374 monster.hp -= playerFinalDamage;
378 if(monster.chestAp > 0) {
379 monster.chestAp -= playerFinalDamage;
381 roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
383 if(monster.chestAp < 0) {
384 roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
385 roundData.roundDetails.push(`You dealt ${monster.chestAp * 1} damage to their HP`);
386 monster.hp += monster.chestAp;
391 roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
392 monster.hp -= playerFinalDamage;
396 if(monster.hp <= 0) {
397 roundData.monster.hp = 0;
398 roundData.winner = 'player';
400 roundData.rewards.exp = monster.exp;
401 roundData.rewards.gold = monster.gold;
403 player.gold += monster.gold;
404 player.exp += monster.exp;
406 if(player.exp >= expToLevel(player.level + 1)) {
407 player.exp -= expToLevel(player.level + 1)
409 roundData.rewards.levelIncrease = true;
411 _.each(professionList[player.profession].onLevelUpStatIncrease(player.level), (v, stat) => {
413 roundData.roundDetails.push(`You gained +${v} ${stat}`);
418 player.hp = maxHp(player.constitution, player.level);
420 // get the monster location!
421 const rawMonster = await loadMonster(monster.ref_id);
422 const monsterList = await getMonsterList(rawMonster.location_id);
423 potentialMonsters = monsterList.map(monster => {
427 level: monster.level,
433 await clearFight(player.id);
434 await updatePlayer(player);
435 socket.emit('fight-over', {roundData, monsters: potentialMonsters});
439 roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
440 if(equipment.has(target)) {
441 const item = equipment.get(target);
442 item.currentAp -= monster.strength;
443 if(item.currentAp < 0) {
444 roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
445 roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
446 player.hp += item.currentAp;
448 await deleteInventoryItem(player.id, item.item_id);
451 roundData.roundDetails.push(`Your ${target} took ${monster.strength} damage!`);
452 await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
457 roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
458 player.hp -= monster.strength;
461 if(playerFinalHeal > 0) {
462 player.hp += playerFinalHeal;
463 if(player.hp > maxHp(player.constitution, player.level)) {
464 player.hp = maxHp(player.constitution, player.level);
466 roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
469 // update the players inventory for this item!
473 roundData.winner = 'monster';
475 roundData.roundDetails.push(`You were killed by the ${monster.name}`);
477 await clearFight(player.id);
478 await updatePlayer(player);
480 socket.emit('fight-over', {roundData, monsters: []});
484 await updatePlayer(player);
485 await saveFightState(player.id, monster);
487 calcAp(equippedItems, socket);
488 socket.emit('fight-round', roundData);
491 // this is a special event to let the client know it can start
493 socket.emit('ready');
496 function authEndpoint(req: Request, res: Response, next: any) {
497 const authToken = req.headers['x-authtoken'];
499 logger.log(`Invalid auth token ${authToken}`);
507 app.get('/city/:id', async (req: Request, res: Response) => {
508 const [city, locations, paths] = await Promise.all([
509 getCityDetails(req.params.id),
510 getAllServices(req.params.id),
511 getAllPaths(req.params.id)
514 res.json({city, locations, paths});
517 app.get('/fight', authEndpoint, async (req: Request, res: Response) => {
518 const authToken = req.headers['x-authtoken'].toString();
519 const player: Player = await loadPlayer(authToken)
522 logger.log(`Couldnt find player with id ${authToken}`);
523 return res.sendStatus(400);
526 const fight = await loadMonsterFromFight(player.id);
528 res.json(fight || null);
532 app.post('/fight', authEndpoint, async (req: Request, res: Response) => {
533 const authToken = req.headers['x-authtoken'].toString();
534 const player: Player = await loadPlayer(authToken)
537 logger.log(`Couldnt find player with id ${authToken}`);
538 return res.sendStatus(400);
541 const monsterId: number = req.body.monsterId;
544 logger.log(`Missing monster Id ${monsterId}`);
545 return res.sendStatus(400);
548 const monster = await loadMonster(monsterId);
551 logger.log(`Couldnt find monster for ${monsterId}`);
552 return res.sendStatus(400);
555 const fight = await createFight(player.id, monster);
557 const data: MonsterForFight = {
567 app.post('/signup', async (req: Request, res: Response) => {
568 const {username, password} = req.body;
569 const authToken = req.headers['x-authtoken'];
571 if(!username || !password || !authToken) {
578 const player = await loadPlayer(authToken.toString());
579 console.log(`Attempted claim for ${player.username}`);
581 await signup(authToken.toString(), username, password);
583 await db('players').where({id: player.id}).update({
584 account_type: 'auth',
588 console.log(`Player claimed ${player.username} => ${username}`);
590 io.emit('chat', broadcastMessage('server', `${player.username} is now ${username}`));
592 player.username = username;
593 player.account_type = 'auth';
601 if(e?.constraint === 'players_username_unique') {
603 error: 'That username is already taken.'
607 res.send({error: 'Please try again'}).status(500);
612 app.post('/login', async (req: Request, res: Response) => {
613 const {username, password} = req.body;
615 const player = await login(username, password);
620 res.json({error: 'That user doesnt exist'}).status(500);
624 server.listen(process.env.API_PORT, () => {
625 logger.log(`Listening on port ${process.env.API_PORT}`);