import { config as dotenv } from 'dotenv';
import { join } from 'path';
import express, {Request, Response} from 'express';
+import bodyParser from 'body-parser';
import http from 'http';
import { Server, Socket } from 'socket.io';
import {FightRound} from '../shared/fight';
import {addInventoryItem, deleteInventoryItem, getEquippedItems, getInventory, getInventoryItem, updateAp} from './inventory';
import { getItemFromPlayer, getItemFromShop, getPlayersItems, getShopItems, givePlayerItem } from './items';
-import {FightTrigger, MonsterForFight} from '../shared/monsters';
+import {FightTrigger, Monster, MonsterForFight, MonsterWithFaction} from '../shared/monsters';
import {getShopEquipment, getShopItem, listShopItems } from './shopEquipment';
import {EquippedItemDetails} from '../shared/equipped';
import {ArmourEquipmentSlot, EquipmentSlot} from '../shared/inventory';
import { router as healerRouter } from './locations/healer';
+import * as Alert from './views/alert';
import { renderPlayerBar } from './views/player-bar'
import { renderEquipmentDetails, renderStore } from './views/stores';
import { renderMap } from './views/map';
import { renderProfilePage } from './views/profile';
import { renderSkills } from './views/skills';
import { renderInventoryPage } from './views/inventory';
-import * as Alert from './views/alert';
+import { renderMonsterSelector } from './views/monster-selector';
+import { renderFight, renderRoundDetails } from './views/fight';
// TEMP!
import { createMonsters } from '../../seeds/monsters';
const server = http.createServer(app);
app.use(express.static(join(__dirname, '..', '..', 'public')));
+app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.json());
const io = new Server(server);
}
});
- socket.on('fight', async (data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) => {
- const authToken = `token:${socket.handshake.headers['x-authtoken']}`;
- const player = cache.get(authToken);
- if(!player) {
- logger.log(`Invalid token for fight`)
- return;
- }
- const monster = await loadMonsterWithFaction(player.id);
- const playerSkills = await getPlayerSkillsAsObject(player.id);
- const roundData: FightRound = {
- monster,
- player,
- winner: 'in-progress',
- fightTrigger: monster.fight_trigger,
- roundDetails: [],
- rewards: {
- exp: 0,
- gold: 0,
- levelIncrease: false
- }
- };
- const equippedItems = await getEquippedItems(player.id);
-
- // we only use this if the player successfully defeated the monster
- // they were fighting, then we load the other monsters in this area
- // so they can "fight again"
- let potentialMonsters: MonsterForFight[] = [];
-
- /*
- * cumulative chance of head/arms/body/legs
- * 0 -> 0.2 = head
- * 0.21 -> 0.4 = arms
- *
- * we use the factor to decide how many decimal places
- * we care about
- */
- const factor = 100;
- const monsterTarget = [0.2, 0.4, 0.9, 1];
- const targets: ArmourEquipmentSlot[] = ['HEAD', 'CHEST', 'ARMS', 'LEGS'];
- // calc weighted
- const rand = Math.ceil(Math.random() * factor);
- let target: ArmourEquipmentSlot = 'CHEST';
- monsterTarget.forEach((i, idx) => {
- if (rand > (i * factor)) {
- target = targets[idx] as ArmourEquipmentSlot;
- }
- });
+ // this is a special event to let the client know it can start
+ // requesting data
+ socket.emit('ready');
+});
- const boost = {
- strength: 0,
- constitution: 0,
- dexterity: 0,
- intelligence: 0,
- damage: 0,
- hp: 0,
- };
+async function fightRound(player: Player, monster: MonsterWithFaction, data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) {
+ const playerSkills = await getPlayerSkillsAsObject(player.id);
+ const roundData: FightRound = {
+ monster,
+ player,
+ winner: 'in-progress',
+ fightTrigger: monster.fight_trigger,
+ roundDetails: [],
+ rewards: {
+ exp: 0,
+ gold: 0,
+ levelIncrease: false
+ }
+ };
+ const equippedItems = await getEquippedItems(player.id);
- const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
- const weapons: EquippedItemDetails[] = [];
- let anyDamageSpells: boolean = false;
- equippedItems.forEach(item => {
- if(item.type === 'ARMOUR') {
- equipment.set(item.equipment_slot, item);
- }
- else if(item.type === 'WEAPON') {
- weapons.push(item);
- }
- else if(item.type === 'SPELL') {
- if(item.affectedSkills.includes('destruction_magic')) {
- anyDamageSpells = true;
- }
- weapons.push(item);
- }
+ // we only use this if the player successfully defeated the monster
+ // they were fighting, then we load the other monsters in this area
+ // so they can "fight again"
+ let potentialMonsters: MonsterForFight[] = [];
+
+ /*
+ * cumulative chance of head/arms/body/legs
+ * 0 -> 0.2 = head
+ * 0.21 -> 0.4 = arms
+ *
+ * we use the factor to decide how many decimal places
+ * we care about
+ */
+ const factor = 100;
+ const monsterTarget = [0.2, 0.4, 0.9, 1];
+ const targets: ArmourEquipmentSlot[] = ['HEAD', 'CHEST', 'ARMS', 'LEGS'];
+ // calc weighted
+ const rand = Math.ceil(Math.random() * factor);
+ let target: ArmourEquipmentSlot = 'CHEST';
+ monsterTarget.forEach((i, idx) => {
+ if (rand > (i * factor)) {
+ target = targets[idx] as ArmourEquipmentSlot;
+ }
+ });
- boost.strength += item.boosts.strength;
- boost.constitution += item.boosts.constitution;
- boost.dexterity += item.boosts.dexterity;
- boost.intelligence += item.boosts.intelligence;
+ const boost = {
+ strength: 0,
+ constitution: 0,
+ dexterity: 0,
+ intelligence: 0,
+ damage: 0,
+ hp: 0,
+ };
- if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
- boost.hp += item.boosts.damage;
- }
- else {
- boost.damage += item.boosts.damage;
+ const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
+ const weapons: EquippedItemDetails[] = [];
+ let anyDamageSpells: boolean = false;
+ equippedItems.forEach(item => {
+ if(item.type === 'ARMOUR') {
+ equipment.set(item.equipment_slot, item);
+ }
+ else if(item.type === 'WEAPON') {
+ weapons.push(item);
+ }
+ else if(item.type === 'SPELL') {
+ if(item.affectedSkills.includes('destruction_magic')) {
+ anyDamageSpells = true;
}
- });
+ weapons.push(item);
+ }
- // if you flee'd, then we want to check your dex vs. the monsters
- // but we want to give you the item/weapon boosts you need
- // if not then you're going to get hit.
- if(data.action === 'flee') {
- roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
- roundData.winner = 'monster';
- await clearFight(player.id);
+ boost.strength += item.boosts.strength;
+ boost.constitution += item.boosts.constitution;
+ boost.dexterity += item.boosts.dexterity;
+ boost.intelligence += item.boosts.intelligence;
- socket.emit('fight-over', {roundData, monsters: []});
- return;
+ if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
+ boost.hp += item.boosts.damage;
+ }
+ else {
+ boost.damage += item.boosts.damage;
}
+ });
- const attackType = data.action === 'attack' ? 'physical' : 'magical';
- const primaryStat = data.action === 'attack' ? player.strength : player.intelligence;
- const boostStat = data.action === 'attack' ? boost.strength : boost.intelligence;
-
- const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
- const skillsUsed: Record<SkillID | any, number> = {};
- let hpHealAfterMasteries: number = -1;
- let playerDamageAfterMasteries: number = 0;
- // apply masteries!
- weapons.forEach(item => {
- item.affectedSkills.forEach(id => {
- if(id === 'restoration_magic') {
- if(hpHealAfterMasteries < 0) {
- hpHealAfterMasteries = 0;
- }
- hpHealAfterMasteries += Skills.get(id).effect(playerSkills.get(id));
- }
- else {
- playerDamageAfterMasteries += playerDamage * Skills.get(id).effect(playerSkills.get(id));
+ // if you flee'd, then we want to check your dex vs. the monsters
+ // but we want to give you the item/weapon boosts you need
+ // if not then you're going to get hit.
+ if(data.action === 'flee') {
+ roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
+ roundData.winner = 'monster';
+ await clearFight(player.id);
+
+ return { roundData, monsters: [] };
+ }
+
+ const attackType = data.action === 'attack' ? 'physical' : 'magical';
+ const primaryStat = data.action === 'attack' ? player.strength : player.intelligence;
+ const boostStat = data.action === 'attack' ? boost.strength : boost.intelligence;
+
+ const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
+ const skillsUsed: Record<SkillID | any, number> = {};
+ let hpHealAfterMasteries: number = -1;
+ let playerDamageAfterMasteries: number = 0;
+ // apply masteries!
+ weapons.forEach(item => {
+ item.affectedSkills.forEach(id => {
+ if(id === 'restoration_magic') {
+ if(hpHealAfterMasteries < 0) {
+ hpHealAfterMasteries = 0;
}
+ hpHealAfterMasteries += Skills.get(id).effect(playerSkills.get(id));
+ }
+ else {
+ playerDamageAfterMasteries += playerDamage * Skills.get(id).effect(playerSkills.get(id));
+ }
- if(!skillsUsed[id]) {
- skillsUsed[id] = 0;
- }
- skillsUsed[id]++;
- });
+ if(!skillsUsed[id]) {
+ skillsUsed[id] = 0;
+ }
+ skillsUsed[id]++;
});
+ });
- await updatePlayerSkills(player.id, skillsUsed);
-
- const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
- const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
-
- roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
- let armourKey: string;
- switch(data.target) {
- case 'arms':
- armourKey = 'armsAp';
- break;
- case 'head':
- armourKey = 'helmAp';
- break;
- case 'legs':
- armourKey = 'legsAp';
- break;
- case 'body':
- armourKey = 'chestAp';
- break;
+ await updatePlayerSkills(player.id, skillsUsed);
+
+ const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
+ const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
+
+ roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
+ let armourKey: string;
+ switch(data.target) {
+ case 'arms':
+ armourKey = 'armsAp';
+ break;
+ case 'head':
+ armourKey = 'helmAp';
+ break;
+ case 'legs':
+ armourKey = 'legsAp';
+ break;
+ case 'body':
+ armourKey = 'chestAp';
+ break;
+ }
+
+ if(monster[armourKey] && monster[armourKey] > 0) {
+ monster[armourKey] -= playerFinalDamage;
+
+ roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
+ if(monster[armourKey] < 0) {
+ roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
+ roundData.roundDetails.push(`You dealt ${monster[armourKey] * -1} damage to their HP`);
+ monster.hp += monster[armourKey];
+ monster[armourKey] = 0;
}
+ }
+ else {
+ roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
+ monster.hp -= playerFinalDamage;
+ }
- if(monster[armourKey] && monster[armourKey] > 0) {
- monster[armourKey] -= playerFinalDamage;
+ if(monster.hp <= 0) {
+ roundData.monster.hp = 0;
+ roundData.winner = 'player';
- roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
- if(monster[armourKey] < 0) {
- roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
- roundData.roundDetails.push(`You dealt ${monster[armourKey] * -1} damage to their HP`);
- monster.hp += monster[armourKey];
- monster[armourKey] = 0;
- }
- }
- else {
- roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
- monster.hp -= playerFinalDamage;
- }
+ roundData.rewards.exp = monster.exp;
+ roundData.rewards.gold = monster.gold;
+
+ player.gold += monster.gold;
+ player.exp += monster.exp;
- if(monster.hp <= 0) {
- roundData.monster.hp = 0;
- roundData.winner = 'player';
+ if(player.exp >= expToLevel(player.level + 1)) {
+ player.exp -= expToLevel(player.level + 1)
+ player.level++;
+ roundData.rewards.levelIncrease = true;
+ let statPointsGained = 1;
- roundData.rewards.exp = monster.exp;
- roundData.rewards.gold = monster.gold;
+ if(player.profession !== 'Wanderer') {
+ statPointsGained = 2;
+ }
- player.gold += monster.gold;
- player.exp += monster.exp;
+ player.stat_points += statPointsGained;
- if(player.exp >= expToLevel(player.level + 1)) {
- player.exp -= expToLevel(player.level + 1)
- player.level++;
- roundData.rewards.levelIncrease = true;
- let statPointsGained = 1;
+ roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
- if(player.profession !== 'Wanderer') {
- statPointsGained = 2;
+ player.hp = maxHp(player.constitution, player.level);
+ }
+ // get the monster location if it was an EXPLORED fight
+ if(roundData.fightTrigger === 'explore') {
+ const rawMonster = await loadMonster(monster.ref_id);
+ const monsterList = await getMonsterList(rawMonster.location_id);
+ potentialMonsters = monsterList.map(monster => {
+ return {
+ id: monster.id,
+ name: monster.name,
+ level: monster.level,
+ hp: monster.hp,
+ maxHp: monster.maxHp,
+ fight_trigger: 'explore'
}
+ });
+ }
- player.stat_points += statPointsGained;
+ await clearFight(player.id);
+ await updatePlayer(player);
+ return { roundData, monsters: potentialMonsters };
+ }
- roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
+ roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
+ if(equipment.has(target)) {
+ const item = equipment.get(target);
+ // apply mitigation!
+ const mitigationPercentage = item.boosts.damage_mitigation || 0;
+ const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
- player.hp = maxHp(player.constitution, player.level);
- }
- // get the monster location if it was an EXPLORED fight
- if(roundData.fightTrigger === 'explore') {
- const rawMonster = await loadMonster(monster.ref_id);
- const monsterList = await getMonsterList(rawMonster.location_id);
- potentialMonsters = monsterList.map(monster => {
- return {
- id: monster.id,
- name: monster.name,
- level: monster.level,
- hp: monster.hp,
- maxHp: monster.maxHp,
- fight_trigger: 'explore'
- }
- });
- }
-
- await clearFight(player.id);
- await updatePlayer(player);
- cache.set(authToken, player);
- socket.emit('fight-over', {roundData, monsters: potentialMonsters});
- return;
- }
-
- roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
- if(equipment.has(target)) {
- const item = equipment.get(target);
- // apply mitigation!
- const mitigationPercentage = item.boosts.damage_mitigation || 0;
- const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
-
- item.currentAp -= damageAfterMitigation;
-
- if(item.currentAp < 0) {
- roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
- roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
- player.hp += item.currentAp;
- item.currentAp = 0;
- await deleteInventoryItem(player.id, item.item_id);
- }
- else {
- roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
- await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
- }
+ item.currentAp -= damageAfterMitigation;
+ if(item.currentAp < 0) {
+ roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
+ roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
+ player.hp += item.currentAp;
+ item.currentAp = 0;
+ await deleteInventoryItem(player.id, item.item_id);
}
else {
- roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
- player.hp -= monster.strength;
+ roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
+ await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
}
- if(playerFinalHeal > 0) {
- player.hp += playerFinalHeal;
- if(player.hp > maxHp(player.constitution, player.level)) {
- player.hp = maxHp(player.constitution, player.level);
- }
- roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
- }
-
- // update the players inventory for this item!
-
- if(player.hp <= 0) {
- player.hp = 0;
- roundData.winner = 'monster';
+ }
+ else {
+ roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
+ player.hp -= monster.strength;
+ }
- roundData.roundDetails.push(`You were killed by the ${monster.name}`);
+ if(playerFinalHeal > 0) {
+ player.hp += playerFinalHeal;
+ if(player.hp > maxHp(player.constitution, player.level)) {
+ player.hp = maxHp(player.constitution, player.level);
+ }
+ roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
+ }
- await clearFight(player.id);
- await updatePlayer(player);
- await clearTravelPlan(player.id);
+ // update the players inventory for this item!
- cache.set(authToken, player);
+ if(player.hp <= 0) {
+ player.hp = 0;
+ roundData.winner = 'monster';
- socket.emit('fight-over', {roundData, monsters: []});
- return;
- }
+ roundData.roundDetails.push(`You were killed by the ${monster.name}`);
+ await clearFight(player.id);
await updatePlayer(player);
- await saveFightState(player.id, monster);
- cache.set(authToken, player);
+ await clearTravelPlan(player.id);
- calcAp(equippedItems, socket);
- socket.emit('fight-round', roundData);
- });
+ return { roundData, monsters: []};
+ }
- // this is a special event to let the client know it can start
- // requesting data
- socket.emit('ready');
-});
+ await updatePlayer(player);
+ await saveFightState(player.id, monster);
+
+ return { roundData, monsters: []};
+};
app.use(healerRouter);
if(fight) {
// ok lets display the fight screen!
- console.log('in a fight!');
+ const data: MonsterForFight = {
+ id: fight.id,
+ hp: fight.hp,
+ maxHp: fight.maxHp,
+ name: fight.name,
+ level: fight.level,
+ fight_trigger: fight.fight_trigger
+ };
+
+ res.send(renderFight(data));
}
else {
const travelPlan = await getTravelPlan(player.id);
res.send(html);
});
+app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: Request, res: Response) => {
+ const authToken = req.headers['x-authtoken'].toString();
+ const player: Player = await loadPlayer(authToken)
+ if(!player) {
+ logger.log(`Couldnt find player with id ${authToken}`);
+ return res.sendStatus(400);
+ }
+
+ const location = await getService(parseInt(req.params.location_id));
+ if(!location || location.city_id !== player.city_id) {
+
+ logger.log(`Invalid location: [${req.params.location_id}]`);
+ res.sendStatus(400);
+ }
+
+ const monsters: Monster[] = await getMonsterList(location.id);
+ res.send(renderMonsterSelector(monsters));
+});
+
app.post('/travel', authEndpoint, async (req: Request, res: Response) => {
const authToken = req.headers['x-authtoken'].toString();
const player: Player = await loadPlayer(authToken)
res.json(travelPlan);
});
+app.post('/fight/turn', authEndpoint, async (req: Request, res: Response) => {
+ const authToken = req.headers['x-authtoken'].toString();
+ const player: Player = await loadPlayer(authToken)
+
+ if(!player) {
+ logger.log(`Couldnt find player with id ${authToken}`);
+ return res.sendStatus(400);
+ }
+
+ const monster = await loadMonsterWithFaction(player.id);
+
+ const fightData = await fightRound(player, monster, {
+ action: req.body.action,
+ target: req.body.fightTarget
+ });
+
+ let html = renderFight(
+ monster,
+ renderRoundDetails(fightData.roundData),
+ fightData.roundData.winner === 'in-progress'
+ );
+
+ if(fightData.monsters.length && monster.fight_trigger === 'explore') {
+ html += renderMonsterSelector(fightData.monsters);
+ }
+
+ res.send(html);
+});
app.post('/fight', authEndpoint, async (req: Request, res: Response) => {
const authToken = req.headers['x-authtoken'].toString();
const fight = await createFight(player.id, monster, fightTrigger);
+
const data: MonsterForFight = {
id: fight.id,
hp: fight.hp,
level: fight.level,
fight_trigger: fight.fight_trigger
};
- res.json(data);
+
+ res.send(renderFight(data));
});
app.post('/signup', async (req: Request, res: Response) => {