From e09d7f104f523455a2dc057e5e5d0117cc72d558 Mon Sep 17 00:00:00 2001 From: xangelo Date: Fri, 1 Sep 2023 14:46:37 -0400 Subject: [PATCH 01/11] chore: ignore migrations during live reload --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2dc99db..72c58f7 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ "src/client/**", "**/client.ts", "public/**", - "**.test.ts" + "**.test.ts", + "migrations/**" ] }, "volta": { -- 2.25.1 From b5577b4abc9f6f3aabee6bdac9256faf268399c4 Mon Sep 17 00:00:00 2001 From: xangelo Date: Fri, 1 Sep 2023 14:47:24 -0400 Subject: [PATCH 02/11] feat: variable level monsters Monsters now define a min/max level. When you start a fight a monster is generated within that level range. We use that to define a modifier for all stats on the monster and adjust accordingly. The stats defined on the monster sheet in airtable reference the min-stats on the monster. --- migrations/20230901182406_monster-variance.ts | 20 ++++++++++++++ seeds/monsters.ts | 11 ++++---- src/server/fight.ts | 2 +- src/server/monster.ts | 27 +++++++++++-------- src/server/views/fight.ts | 4 +-- src/server/views/monster-selector.ts | 6 ++--- src/shared/monsters.ts | 14 +++++----- 7 files changed, 56 insertions(+), 28 deletions(-) create mode 100644 migrations/20230901182406_monster-variance.ts diff --git a/migrations/20230901182406_monster-variance.ts b/migrations/20230901182406_monster-variance.ts new file mode 100644 index 0000000..35e6db0 --- /dev/null +++ b/migrations/20230901182406_monster-variance.ts @@ -0,0 +1,20 @@ +import { Knex } from "knex"; + + +export async function up(knex: Knex): Promise { + return knex.schema.alterTable('monsters', function(table) { + table.dropColumn('level'); + table.integer('minLevel').notNullable().defaultTo(1); + table.integer('maxLevel').notNullable().defaultTo(1); + }); +} + + +export async function down(knex: Knex): Promise { + return knex.schema.alterTable('monsters', function(table) { + table.dropColumn('minLevel'); + table.dropColumn('maxLevel'); + table.integer('level').notNullable().defaultTo(1); + }); +} + diff --git a/seeds/monsters.ts b/seeds/monsters.ts index 44afa4f..02eecb8 100644 --- a/seeds/monsters.ts +++ b/seeds/monsters.ts @@ -50,12 +50,13 @@ export async function createMonsters(): Promise { return { id: r.fields.id, name: r.fields.Name, - strength: r.fields['STR_override'] || r.fields.STR, - constitution: r.fields['CON_override'] || r.fields.CON, - dexterity: r.fields['DEX_override'] || r.fields.DEX, - intelligence: r.fields['INT_override'] || r.fields.INT, + strength: r.fields.STR, + constitution: r.fields.CON, + dexterity: r.fields.DEX, + intelligence: r.fields.INT, exp: r.fields.EXP, - level: r.fields.Level, + minLevel: r.fields.minLevel, + maxLevel: r.fields.maxLevel ?? r.fields.minLevel, gold: r.fields.GOLD, hp: r.fields.HP, maxHp: r.fields.HP, diff --git a/src/server/fight.ts b/src/server/fight.ts index 2c9383d..6e262df 100644 --- a/src/server/fight.ts +++ b/src/server/fight.ts @@ -196,7 +196,7 @@ export async function fightRound(player: Player, monster: MonsterWithFaction, d return { id: monster.id, name: monster.name, - level: monster.level, + level: monster.minLevel, hp: monster.hp, maxHp: monster.maxHp, fight_trigger: 'explore' diff --git a/src/server/monster.ts b/src/server/monster.ts index 943a868..18c1243 100644 --- a/src/server/monster.ts +++ b/src/server/monster.ts @@ -2,6 +2,7 @@ import { db } from './lib/db'; import { Fight, Monster, MonsterWithFaction, MonsterForList, FightTrigger } from '../shared/monsters'; import { TimePeriod, TimeManager } from '../shared/time'; import { LocationWithCity } from 'shared/map'; +import { max, random } from 'lodash'; const time = new TimeManager(); @@ -21,7 +22,7 @@ export async function getMonsterList(location_id: number, timePeriod: TimePeriod .where({ location_id }) .whereIn('time_period', timePeriod) .from('monsters') - .orderBy('level'); + .orderBy('minLevel'); return res; } @@ -62,19 +63,23 @@ export async function saveFightState(authToken: string, monster: Fight) { } export async function createFight(playerId: string, monster: Monster, fightTrigger: FightTrigger): Promise { + const chosenLevel = random(monster.minLevel, monster.maxLevel); + // 30% boost per level difference + const modifier = Math.pow(Math.E, (chosenLevel - monster.minLevel)/monster.maxLevel); + const res = await db('fight').insert({ player_id: playerId, name: monster.name, - strength: monster.strength, - constitution: monster.constitution, - dexterity: monster.dexterity, - intelligence: monster.intelligence, - exp: monster.exp, - level: monster.level, - gold: monster.gold, - hp: monster.hp, - defence: monster.defence, - maxHp: monster.maxHp, + strength: Math.floor(monster.strength * modifier), + constitution: Math.floor(monster.constitution * modifier), + dexterity: Math.floor(monster.dexterity * modifier), + intelligence: Math.floor(monster.intelligence * modifier), + exp: Math.floor(monster.exp * modifier), + level: chosenLevel, + gold: Math.floor(monster.gold * modifier), + hp: Math.floor(monster.hp * modifier), + defence: Math.floor(monster.defence * modifier), + maxHp: Math.floor(monster.maxHp * modifier), ref_id: monster.id, fight_trigger: fightTrigger }).returning('*'); diff --git a/src/server/views/fight.ts b/src/server/views/fight.ts index 06ebcdb..f09ab41 100644 --- a/src/server/views/fight.ts +++ b/src/server/views/fight.ts @@ -79,7 +79,7 @@ export function renderFight(monster: MonsterForFight, results: string = '', disp
-
${monster.name}
+
${monster.name}, level ${monster.level}
${hpPercent}% - ${monster.hp} / ${monster.maxHp}
@@ -122,7 +122,7 @@ export function renderFightPreRound(monster: MonsterForFight, displayFightActio
-
${monster.name}
+
${monster.name}, level ${monster.level}
${hpPercent}% - ${monster.hp} / ${monster.maxHp}
diff --git a/src/server/views/monster-selector.ts b/src/server/views/monster-selector.ts index e357835..03d6197 100644 --- a/src/server/views/monster-selector.ts +++ b/src/server/views/monster-selector.ts @@ -22,9 +22,9 @@ export function renderMonsterSelector(monsters: Monster[] | MonsterForFight[], a


diff --git a/src/shared/monsters.ts b/src/shared/monsters.ts index c29a477..91c0dfa 100644 --- a/src/shared/monsters.ts +++ b/src/shared/monsters.ts @@ -7,7 +7,8 @@ export type Monster = { dexterity: number; intelligence: number; constitution: number; - level: number; + minLevel: number; + maxLevel: number; gold: number; exp: number; hp: number; @@ -26,11 +27,12 @@ export type MonsterForList = { export type FightTrigger = 'explore' | 'travel'; -export type Fight = Omit & { - id: string, - player_id: string, - ref_id: number - fight_trigger: FightTrigger +export type Fight = Omit & { + id: string; + player_id: string; + level: number; + ref_id: number; + fight_trigger: FightTrigger; }; export type MonsterWithFaction = Fight & { -- 2.25.1 From 4bf369dfb7f5f3c0886870a0a34415db2fa3bf70 Mon Sep 17 00:00:00 2001 From: xangelo Date: Tue, 5 Sep 2023 10:55:30 -0400 Subject: [PATCH 03/11] chore: remove unused websocket handlers --- src/events/client.ts | 2 - src/events/equipping-items/server.ts | 108 -------------------- src/events/explore/server.ts | 26 ----- src/events/items/server.ts | 54 ---------- src/events/profession-changing/client.ts | 24 ----- src/events/profession-changing/server.ts | 107 -------------------- src/events/profession-changing/shared.ts | 1 - src/events/server.ts | 7 -- src/events/stat-points/server.ts | 36 ------- src/events/stores/server.ts | 30 ------ src/events/travel/client.ts | 18 ---- src/events/travel/server.ts | 123 ----------------------- src/events/travel/shared.ts | 16 --- 13 files changed, 552 deletions(-) delete mode 100644 src/events/client.ts delete mode 100644 src/events/equipping-items/server.ts delete mode 100644 src/events/explore/server.ts delete mode 100644 src/events/items/server.ts delete mode 100644 src/events/profession-changing/client.ts delete mode 100644 src/events/profession-changing/server.ts delete mode 100644 src/events/profession-changing/shared.ts delete mode 100644 src/events/server.ts delete mode 100644 src/events/stat-points/server.ts delete mode 100644 src/events/stores/server.ts delete mode 100644 src/events/travel/client.ts delete mode 100644 src/events/travel/server.ts delete mode 100644 src/events/travel/shared.ts diff --git a/src/events/client.ts b/src/events/client.ts deleted file mode 100644 index 4d0754f..0000000 --- a/src/events/client.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './profession-changing/client'; -export * from './travel/client'; diff --git a/src/events/equipping-items/server.ts b/src/events/equipping-items/server.ts deleted file mode 100644 index 7b00316..0000000 --- a/src/events/equipping-items/server.ts +++ /dev/null @@ -1,108 +0,0 @@ -import {SocketEvent} from "../../server/socket-event.server"; -import { Socket } from 'socket.io'; -import {EquipmentSlot} from '../../shared/inventory'; -import {getEquippedItems, getInventory, getInventoryItem} from '../../server/inventory'; -import { equip, unequip } from '../../server/equipment'; -import { logger } from '../../server/lib/logger'; -import {EquippedItemDetails} from '../../shared/equipped'; -import { getPlayersItems } from "../../server/items"; - -class EquipmentInSlotError extends Error { - code: number; - constructor() { - super('You already have something in that slot'); - this.code = 23505; - } -} - - -function calcAp(inventoryItem: EquippedItemDetails[], socket: Socket) { - const ap: Record = {}; - inventoryItem.forEach(item => { - if(item.is_equipped && item.type === 'ARMOUR') { - ap[item.equipment_slot] = { - currentAp: item.currentAp, - maxAp: item.maxAp - }; - } - }); - - socket.emit('calc:ap', {ap}); -} - -export const equipItem: SocketEvent = { - eventName: 'equip', - handler: async (api, data: {id: string, slot: EquipmentSlot}) => { - const inventoryItem = await getInventoryItem(api.player.id, data.id); - const equippedItems = await getEquippedItems(api.player.id); - let desiredSlot: EquipmentSlot = inventoryItem.equipment_slot; - - try { - // handes the situation where you're trying to equip an item - // that can be equipped to any hand - if(inventoryItem.equipment_slot === 'ANY_HAND') { - if(data.slot === 'LEFT_HAND' || data.slot === 'RIGHT_HAND') { - // get the players current equipment in that slot! - if(equippedItems.some(v => { - return v.equipment_slot === data.slot || v.equipment_slot === 'TWO_HANDED'; - })) { - throw new EquipmentInSlotError(); - } - else { - desiredSlot = data.slot; - } - } - } - - if(data.slot === 'TWO_HANDED') { - if(equippedItems.some(v => { - return v.equipment_slot === 'LEFT_HAND' || v.equipment_slot === 'RIGHT_HAND'; - })) { - throw new EquipmentInSlotError(); - } - } - - - await equip(api.player.id, inventoryItem, desiredSlot); - api.socket.emit('alert', { - type: 'success', - text: `You equipped your ${inventoryItem.name}` - }); - } - catch(e) { - if(e.code.toString() === '23505') { - api.socket.emit('alert', { - type: 'error', - text: 'You already have an item equipped in that slot' - }); - } - else { - logger.log(e); - } - } - - const inventory = await getInventory(api.player.id); - const items = await getPlayersItems(api.player.id); - api.socket.emit('inventory', { - inventory, - items - }); - calcAp(inventory, api.socket); - - } -} - -export const unequipItem: SocketEvent = { - eventName: 'unequip', - handler: async (api, data: {id: string}) => { - await unequip(api.player.id, data.id); - - const inventory = await getInventory(api.player.id); - calcAp(inventory, api.socket); - const items = await getPlayersItems(api.player.id); - api.socket.emit('inventory', { - inventory, - items - }); - } -} diff --git a/src/events/explore/server.ts b/src/events/explore/server.ts deleted file mode 100644 index ed12d9d..0000000 --- a/src/events/explore/server.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {Monster, MonsterForList} from "../../shared/monsters"; -import {SocketEvent} from "../../server/socket-event.server"; -import {getMonsterList} from "../../server/monster"; - -export const exploreInCity: SocketEvent = { - eventName: 'city:explore', - handler: async (api, data: { args: string }) => { - // @TODO add check to make sure player is in this town and can actually explore this area - const locationId = parseInt(data.args); - - if(!locationId || isNaN(locationId)) { - return; - } - - const monsters: Monster[] = await getMonsterList(locationId); - let monsterList: MonsterForList[] = monsters.map(monster => { - return { - id: monster.id, - name: monster.name, - level: monster.level - } - }); - - api.socket.emit('explore:fights', monsterList); - } -} diff --git a/src/events/items/server.ts b/src/events/items/server.ts deleted file mode 100644 index 0c9023d..0000000 --- a/src/events/items/server.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { getItemFromPlayer, getPlayersItems, updateItemCount } from "../../server/items"; -import {API, SocketEvent} from "../../server/socket-event.server"; -import { maxHp } from "../../shared/player"; -import { updatePlayer } from "../../server/player"; -import { HealthPotionSmall } from "../../shared/items/health_potion"; -import { getInventory } from "../../server/inventory"; - -export const smallHeal: SocketEvent = { - eventName: 'item:use:heal_small', - handler: async (api: API, data: {args: string}): Promise => { - const itemId = parseInt(data.args); - const item = await getItemFromPlayer(api.player.id, itemId); - - if(!item) { - console.log(`Can't find item [${data.args}]`); - return; - } - - if(item.amount < 1) { - api.socket.emit('alert', { - type: 'error', - text: `You don't have enough ${item.name}` - }); - return; - } - - item.amount -= 1; - - const hpGain = HealthPotionSmall.effect(api.player); - - api.player.hp += hpGain; - - if(api.player.hp > maxHp(api.player.constitution, api.player.level)) { - api.player.hp = maxHp(api.player.constitution, api.player.level); - } - - await updateItemCount(api.player.id, item.item_id, -1); - await updatePlayer(api.player); - const inventory = await getInventory(api.player.id); - const items = await getPlayersItems(api.player.id); - - api.socket.emit('updatePlayer', api.player); - api.socket.emit('inventory', { - inventory, - items - }); - api.socket.emit('alert', { - type: 'success', - text: `You used the Small Health Potion` - }); - - return; - } -} diff --git a/src/events/profession-changing/client.ts b/src/events/profession-changing/client.ts deleted file mode 100644 index 5a045c6..0000000 --- a/src/events/profession-changing/client.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {SocketEvent} from "../../client/socket-event.client"; -import $ from 'jquery'; -import { EVENT_NAME } from "./shared"; - -function generalTextReplacementHandler(api, data: { text: string }) { - $('#map').html(data.text); -} - -export const professionRecruiter: SocketEvent = new SocketEvent(EVENT_NAME, generalTextReplacementHandler); - -export const professionChangeWarrior: SocketEvent = new SocketEvent( - `${EVENT_NAME}:warrior`, - generalTextReplacementHandler -); - -export const professionChangeMage: SocketEvent = new SocketEvent( - `${EVENT_NAME}:mage`, - generalTextReplacementHandler -); - -export const professionChangeRogue: SocketEvent = new SocketEvent( - `${EVENT_NAME}:rogue`, - generalTextReplacementHandler -); diff --git a/src/events/profession-changing/server.ts b/src/events/profession-changing/server.ts deleted file mode 100644 index 60882e8..0000000 --- a/src/events/profession-changing/server.ts +++ /dev/null @@ -1,107 +0,0 @@ -import {API, SocketEvent} from "../../server/socket-event.server"; -import { EVENT_NAME } from './shared'; -import { changeProfession } from "../../server/player"; -import {Profession} from "shared/profession"; -import {broadcastMessage} from "../../shared/message"; - -const MIN_LEVEL = 25; - -export const professionRecruiter: SocketEvent = { - eventName: EVENT_NAME, - handler: (api, data): Promise => { - - let text: string = ''; - - if(api.player.profession === 'Wanderer') { - text = ` - Welcome to the Professional Services at Windcross! -

Our duty is to help Wanderers such as yourself become more than they are. By helping you achieve new levels in service of the King, we can ensure that the Kingdom of Khatis continues to grow!

-

You have 3 choices laid before you.

-

You could become a great and mighty Warrior! Wielding powerful swords and maces.

-

You could become a powerful Mage! Casting spells to rain fire upon our enemies.

-

You could become a lithe Rogue! Attacking our enemies swiftly when they least expect!

- `; - - if(api.player.level < MIN_LEVEL) { - text += `

Unfortunately.. you have to be at least level ${MIN_LEVEL} to take part in our training...

`; - } - else { - text += ` -

Be Careful! Once you change your profession, you will never again be a Wanderer

-
- - - -
- ` - } - } - else { - - let town = 'UNSET'; - let place = 'UNSETPLACE'; - switch(api.player.profession) { - case 'Warrior': - town = 'Stether'; - place = 'Highbreaker Inn' - break; - case 'Mage': - town = 'Davelfell'; - place = 'Mages Tower'; - break; - case 'Rogue': - town = 'Ferbalt Gap'; - place = 'Keepers Tavern'; - break; - } - - text = `

Welcome ${api.player.profession}!`; - text += `

Unfortunately I won't be of much help to you now that you are no longer a wanderer...

`; - text += `

However, you should visit the ${place} in ${town} that can probably provide some guidance!

`; - } - - - api.socket.emit('city:services:profession_recruitor_windcross', { - text - }); - - return; - } -}; - -async function generalFirstLevelProfessionChange(api: API, data: unknown, profession: Profession) { - console.log(`${api.player.username} is becoming a ${profession}!`); - const update = await changeProfession(api.player.id, profession); - - api.player.level = update.level; - api.player.exp = update.exp; - api.player.profession = profession; - - api.socket.emit('updatePlayer', api.player); - api.socket.emit(`${EVENT_NAME}:${profession.toLowerCase()}`, { - text: `Congratulations! You are now a ${profession}!` - }); - - api.io.emit('chat', broadcastMessage('server', `${api.player.username} is now a ${profession}`)); -} - -export const professionChangeWarrior: SocketEvent = { - eventName: `${EVENT_NAME}:warrior`, - handler: async (api, data) => { - generalFirstLevelProfessionChange(api, data, 'Warrior'); - } -} - -export const professionChangeMage: SocketEvent = { - eventName: `${EVENT_NAME}:mage`, - handler: async (api, data) => { - generalFirstLevelProfessionChange(api, data, 'Mage'); - } -} - -export const professionChangeRogue: SocketEvent = { - eventName: `${EVENT_NAME}:rogue`, - handler: async (api, data) => { - generalFirstLevelProfessionChange(api, data, 'Rogue'); - } -} diff --git a/src/events/profession-changing/shared.ts b/src/events/profession-changing/shared.ts deleted file mode 100644 index bbd9879..0000000 --- a/src/events/profession-changing/shared.ts +++ /dev/null @@ -1 +0,0 @@ -export const EVENT_NAME: string = 'city:services:profession_recruitor_windcross'; diff --git a/src/events/server.ts b/src/events/server.ts deleted file mode 100644 index 6f9811a..0000000 --- a/src/events/server.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './profession-changing/server'; -export * from './equipping-items/server'; -export * from './travel/server'; -export * from './explore/server'; -export * from './stores/server'; -export * from './stat-points/server'; -export * from './items/server'; diff --git a/src/events/stat-points/server.ts b/src/events/stat-points/server.ts deleted file mode 100644 index 697959a..0000000 --- a/src/events/stat-points/server.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {SocketEvent} from "../../server/socket-event.server"; -import { professionList } from "../../shared/profession"; -import { updatePlayer } from "../../server/player"; -import { Stat } from "../../shared/stats"; - -export const spendStatPoint: SocketEvent = { - eventName: 'spend-stat-point', - handler: async (api, data: { args: any }): Promise => { - - if(!Stat[data.args]) { - api.socket.emit('alert', { - type: 'error', - text: `Invalid stat type [${data.args}]` - }); - return; - } - - const statToIncrease: Stat = data.args as Stat; - const costToIncrease: number = professionList[api.player.profession].classStats.includes(statToIncrease) ? 1 : 2; - - if(api.player.stat_points < costToIncrease) { - api.socket.emit('alert', { - type: 'error', - text: 'You don\'t have enough stat points' - }); - } - else { - api.player[statToIncrease] += costToIncrease; - api.player.stat_points -= costToIncrease; - - await updatePlayer(api.player); - api.socket.emit('updatePlayer', api.player); - } - - } -} diff --git a/src/events/stores/server.ts b/src/events/stores/server.ts deleted file mode 100644 index 1acbd8c..0000000 --- a/src/events/stores/server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {SocketEvent} from "../../server/socket-event.server"; -import {listShopItems} from '../../server/shopEquipment'; -import { getShopItems } from "../../server/items"; -import { logger } from '../../server/lib/logger'; - -export const stores: SocketEvent = { - eventName: 'city:stores', - handler: async (api, data: {args: string}) => { - const storeId = parseInt(data.args); - - if(!storeId || isNaN(storeId)) { - logger.log(`Invalid store id: ${storeId}`); - } - - const [shopEquipemnt, shopItems] = await Promise.all([ - listShopItems({location_id: storeId}), - getShopItems(storeId) - ]); - - if(shopEquipemnt && shopEquipemnt.length) { - api.socket.emit('city:stores', { - equipment: shopEquipemnt, - items: shopItems - }); - } - else { - logger.log(`Insufficient shop items: ${shopEquipemnt.length}`); - } - } -} diff --git a/src/events/travel/client.ts b/src/events/travel/client.ts deleted file mode 100644 index df11e92..0000000 --- a/src/events/travel/client.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {SocketEvent} from "../../client/socket-event.client"; -import $ from 'jquery'; -import { TravelDTO } from './shared'; - -export const travel: SocketEvent = new SocketEvent( - 'city:travel', - (api, data) => { - api.events.emit('renderTravel',[data as TravelDTO]); - } -); - -export const renderCity: SocketEvent = new SocketEvent( - 'city:display', - async (api, data) => { - api.cache.delete('currentMapHTML'); - api.events.emit('renderMap', [data]); - } -) diff --git a/src/events/travel/server.ts b/src/events/travel/server.ts deleted file mode 100644 index 26b81bd..0000000 --- a/src/events/travel/server.ts +++ /dev/null @@ -1,123 +0,0 @@ -import {movePlayer} from "../../server/player"; -import {getCityDetails, getAllServices, getAllPaths, travel, getTravelPlan, stepForward, completeTravel} from "../../server/map"; -import {SocketEvent} from "../../server/socket-event.server"; -import { sample, random } from 'lodash'; -import { getRandomMonster } from "../../server/monster"; -import { STEP_DELAY } from "./shared"; - -const MONSTER_ENCOUNTER_CHANCE = 30; - -export const explore: SocketEvent = { - eventName: 'city:travel', - handler: async (api, data: { args: string }) => { - if(api.player.hp <= 0) { - api.socket.emit('alert', { - type: 'error', - text: 'Sorry, you need some HP to start your travel' - }); - return; - } - - const destinationCity = parseInt(data.args); - - - if(!destinationCity || isNaN(destinationCity)) { - console.log(`Invalid destination city [${destinationCity}]`); - return; - } - - try { - const city = await getCityDetails(destinationCity); - - if(!city) { - console.log(`Invalid destination city [${destinationCity}]`); - return; - } - - console.log(`${api.player.username} attempting travel to ${city.name}`); - const travelPlan = await travel(api.player, city.id); - - api.socket.emit('city:travel', { - things: [], - nextAction: 0, - closestTown: api.player.city_id, - walkingText: '' - }); - } - catch(e) { - console.log(e); - } - } -} - -const walkingText: string[] = [ - 'You take a step forward', - 'You keep moving' -]; - -export const nextStep: SocketEvent = { - eventName: 'travel:step', - handler: async (api, data: { args: string }) => { - const stepTimerKey = `step:${api.player.id}`; - const travelPlan = await getTravelPlan(api.player.id); - - if(api.cache[stepTimerKey]) { - if(api.cache[stepTimerKey] > Date.now()) { - // clicked too fast - return; - } - } - - if(!travelPlan) { - return; - } - - travelPlan.current_position++; - if(travelPlan.current_position >= travelPlan.total_distance) { - const travel = await completeTravel(api.player.id); - - api.player.city_id = travel.destination_id; - await movePlayer(travel.destination_id, api.player.id); - - const [city, locations, paths] = await Promise.all([ - getCityDetails(travel.destination_id), - getAllServices(travel.destination_id), - getAllPaths(travel.destination_id) - ]); - - api.socket.emit('city:display', { - city, - locations, - paths - }); - - delete api.cache[stepTimerKey]; - } - else { - // update existing plan.. - // decide if they will run into anything - const travelPlan = await stepForward(api.player.id); - - const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id; - - const chanceToSeeMonster = random(0, 100); - const things: any[] = []; - if(chanceToSeeMonster < MONSTER_ENCOUNTER_CHANCE) { - const monster = await getRandomMonster([closest]); - things.push(monster); - } - - const nextAction = Date.now() + STEP_DELAY; - - api.cache[stepTimerKey] = nextAction; - - api.socket.emit('city:travel', { - things, - nextAction, - closestTown: closest, - walkingText: sample(walkingText) - }); - - } - } -}; diff --git a/src/events/travel/shared.ts b/src/events/travel/shared.ts deleted file mode 100644 index d6f2f10..0000000 --- a/src/events/travel/shared.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {City, Location, Path} from "../../shared/map" - -export type DTO = { - city: City, - locations: Location[], - paths: Path[] -} - -export type TravelDTO = { - things: any[], - nextAction: number; - walkingText: string, - closestTown: number; -} - -export const STEP_DELAY = 3000; -- 2.25.1 From e6eeb759bd23814a529fbcd98c7b6ad7b21fb91f Mon Sep 17 00:00:00 2001 From: xangelo Date: Tue, 5 Sep 2023 10:58:59 -0400 Subject: [PATCH 04/11] chore: move healer handler --- .../locations/{healer/index.ts => healer.ts} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename src/server/locations/{healer/index.ts => healer.ts} (92%) diff --git a/src/server/locations/healer/index.ts b/src/server/locations/healer.ts similarity index 92% rename from src/server/locations/healer/index.ts rename to src/server/locations/healer.ts index 83551b2..2d12db0 100644 --- a/src/server/locations/healer/index.ts +++ b/src/server/locations/healer.ts @@ -1,13 +1,13 @@ import { Response, Router } from "express"; -import { maxHp, maxVigor } from "../../../shared/player"; -import { authEndpoint, AuthRequest } from '../../auth'; -import { logger } from "../../lib/logger"; -import { updatePlayer } from "../../player"; -import { getCityDetails, getService } from '../../map'; +import { maxHp, maxVigor } from "../../shared/player"; +import { authEndpoint, AuthRequest } from '../auth'; +import { logger } from "../lib/logger"; +import { updatePlayer } from "../player"; +import { getCityDetails, getService } from '../map'; import { sample } from 'lodash'; -import { City, Location } from "../../../shared/map"; -import { renderPlayerBar } from "../../views/player-bar"; -import { BackToTown } from "../../views/components/button"; +import { City, Location } from "../../shared/map"; +import { renderPlayerBar } from "../views/player-bar"; +import { BackToTown } from "../views/components/button"; export const router = Router(); -- 2.25.1 From 32389654d7acc630d524a157881069712e1822b5 Mon Sep 17 00:00:00 2001 From: xangelo Date: Tue, 5 Sep 2023 11:21:00 -0400 Subject: [PATCH 05/11] feat: add debug/error methods to logger --- src/server/lib/logger.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/server/lib/logger.ts b/src/server/lib/logger.ts index c49e6bf..1d0759a 100644 --- a/src/server/lib/logger.ts +++ b/src/server/lib/logger.ts @@ -1,5 +1,11 @@ export const logger = { + debug: (...args: any | any[]) => { + console.debug(...args); + }, log: (...args: any | any[]) => { console.log(...args); - } + }, + error: (...args: any | any[]) => { + console.error(...args); + }, } -- 2.25.1 From d820e11cc3b87c45a6bafad91714db44b9107279 Mon Sep 17 00:00:00 2001 From: xangelo Date: Tue, 5 Sep 2023 11:39:00 -0400 Subject: [PATCH 06/11] feat: migrate to augmenting express.Request interface We were originally extending the express.Request interface inline in `src/server/auth.ts`, We are now just augmenting the actual express.Request interface via typescript definition. --- src/server/api.ts | 52 +++++++++++++++---------------- src/server/auth.ts | 6 +--- src/server/fight.ts | 5 ++- src/server/locations/healer.ts | 8 ++--- src/server/locations/recruiter.ts | 8 ++--- src/server/locations/repair.ts | 8 ++--- src/types/express/index.d.ts | 7 +++++ tsconfig.json | 31 +++++++++--------- 8 files changed, 64 insertions(+), 61 deletions(-) create mode 100644 src/types/express/index.d.ts diff --git a/src/server/api.ts b/src/server/api.ts index f167ff6..355bd04 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -22,7 +22,7 @@ import {FightTrigger, Monster, MonsterForFight} from '../shared/monsters'; import {getShopEquipment, listShopItems } from './shopEquipment'; import {EquipmentSlot} from '../shared/inventory'; import { clearTravelPlan, completeTravel, getAllPaths, getAllServices, getCityDetails, getService, getTravelPlan, stepForward, travel } from './map'; -import { signup, login, authEndpoint, AuthRequest } from './auth'; +import { signup, login, authEndpoint } from './auth'; import {db} from './lib/db'; import { getPlayerSkills} from './skills'; @@ -150,13 +150,13 @@ app.use(professionRouter); app.use(repairRouter); -app.get('/chat/history', authEndpoint, async (req: AuthRequest, res: Response) => { +app.get('/chat/history', authEndpoint, async (req: Request, res: Response) => { let html = chatHistory.map(renderChatMessage); res.send(html.join("\n")); }); -app.post('/chat', authEndpoint, async (req: AuthRequest, res: Response) => { +app.post('/chat', authEndpoint, async (req: Request, res: Response) => { const msg = req.body.message.trim(); if(!msg || !msg.length) { @@ -207,12 +207,12 @@ app.post('/chat', authEndpoint, async (req: AuthRequest, res: Response) => { } }); -app.get('/player', authEndpoint, async (req: AuthRequest, res: Response) => { +app.get('/player', authEndpoint, async (req: Request, res: Response) => { const equipment = await getEquippedItems(req.player.id); res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment)); }); -app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Response) => { +app.post('/player/stat/:stat', authEndpoint, async (req: Request, res: Response) => { const equipment = await getEquippedItems(req.player.id); const stat = req.params.stat; if(!['strength', 'constitution', 'dexterity', 'intelligence'].includes(stat)) { @@ -235,13 +235,13 @@ app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Respo res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment)); }); -app.get('/player/skills', authEndpoint, async (req: AuthRequest, res: Response) => { +app.get('/player/skills', authEndpoint, async (req: Request, res: Response) => { const skills = await getPlayerSkills(req.player.id); res.send(renderSkills(skills)); }); -app.get('/player/inventory', authEndpoint, async (req: AuthRequest, res: Response) => { +app.get('/player/inventory', authEndpoint, async (req: Request, res: Response) => { const [inventory, items] = await Promise.all([ getInventory(req.player.id), getPlayersItems(req.player.id) @@ -250,7 +250,7 @@ app.get('/player/inventory', authEndpoint, async (req: AuthRequest, res: Respons res.send(renderInventoryPage(inventory, items)); }); -app.post('/player/equip/:item_id/:slot', authEndpoint, blockPlayerInFight, async (req: AuthRequest, res: Response) => { +app.post('/player/equip/:item_id/:slot', authEndpoint, blockPlayerInFight, async (req: Request, res: Response) => { const inventoryItem = await getInventoryItem(req.player.id, req.params.item_id); const equippedItems = await getEquippedItems(req.player.id); const requestedSlot = req.params.slot; @@ -302,7 +302,7 @@ app.post('/player/equip/:item_id/:slot', authEndpoint, blockPlayerInFight, async res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(req.player)); }); -app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (req: AuthRequest, res: Response) => { +app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (req: Request, res: Response) => { const [item, ] = await Promise.all([ getInventoryItem(req.player.id, req.params.item_id), unequip(req.player.id, req.params.item_id) @@ -316,7 +316,7 @@ app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (re res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player)); }); -app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) => { +app.get('/player/explore', authEndpoint, async (req: Request, res: Response) => { const fight = await loadMonsterFromFight(req.player.id); const travelPlan = await getTravelPlan(req.player.id); let closestTown = req.player.city_id; @@ -375,7 +375,7 @@ app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) }); // used to purchase equipment from a particular shop -app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => { +app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: Request, res: Response) => { const item = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id)); if(!item) { @@ -397,7 +397,7 @@ app.put('/location/:location_id/equipment/:item_id', authEndpoint, async (req: A }); // used to purchase items from a particular shop -app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => { +app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: Request, res: Response) => { const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id)); if(!item) { @@ -421,7 +421,7 @@ app.put('/location/:location_id/items/:item_id', authEndpoint, async (req: AuthR // used to display equipment modals in a store, validates that // the equipment is actually in this store before displaying // the modal -app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => { +app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, async (req: Request, res: Response) => { const equipment = await getShopEquipment(parseInt(req.params.item_id), parseInt(req.params.location_id)); if(!equipment) { @@ -452,7 +452,7 @@ app.get('/location/:location_id/equipment/:item_id/overview', authEndpoint, asyn // used to display item modals in a store, validates that // the item is actually in this store before displaying // the modal -app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (req: AuthRequest, res: Response) => { +app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (req: Request, res: Response) => { const item: (ShopItem & Item) = await getItemFromShop(parseInt(req.params.item_id), parseInt(req.params.location_id)); if(!item) { @@ -481,7 +481,7 @@ app.get('/location/:location_id/items/:item_id/overview', authEndpoint, async (r res.send(html); }); -app.put('/item/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => { +app.put('/item/:item_id', authEndpoint, async (req: Request, res: Response) => { const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id)); if(!item) { @@ -524,7 +524,7 @@ app.put('/item/:item_id', authEndpoint, async (req: AuthRequest, res: Response) }); -app.get('/modal/items/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => { +app.get('/modal/items/:item_id', authEndpoint, async (req: Request, res: Response) => { const item: PlayerItem = await getItemFromPlayer(req.player.id, parseInt(req.params.item_id)); if(!item) { @@ -553,7 +553,7 @@ app.get('/modal/items/:item_id', authEndpoint, async (req: AuthRequest, res: Res res.send(html); }); -app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => { +app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: Request, res: Response) => { const location = await getService(parseInt(req.params.location_id)); if(!location || location.city_id !== req.player.city_id) { @@ -570,7 +570,7 @@ app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: AuthR res.send(html); }); -app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => { +app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: Request, res: Response) => { const location = await getService(parseInt(req.params.location_id)); if(!location || location.city_id !== req.player.city_id) { @@ -582,7 +582,7 @@ app.get('/city/explore/city:explore/:location_id', authEndpoint, async (req: Aut res.send(renderOnlyMonsterSelector(monsters, 0, location)); }); -app.post('/travel', authEndpoint, async (req: AuthRequest, res: Response) => { +app.post('/travel', authEndpoint, async (req: Request, res: Response) => { const destination_id = parseInt(req.body.destination_id); if(!destination_id || isNaN(destination_id)) { @@ -595,7 +595,7 @@ app.post('/travel', authEndpoint, async (req: AuthRequest, res: Response) => { res.json(travelPlan); }); -app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) => { +app.post('/fight/turn', authEndpoint, async (req: Request, res: Response) => { const fightBlockKey = `fightturn:${req.player.id}`; if(cache[fightBlockKey] && cache[fightBlockKey] > Date.now()) { @@ -646,7 +646,7 @@ app.post('/fight/turn', authEndpoint, async (req: AuthRequest, res: Response) => res.send(html + travelSection + playerBar); }); -app.post('/fight', fightRateLimiter, authEndpoint, async (req: AuthRequest, res: Response) => { +app.post('/fight', fightRateLimiter, authEndpoint, async (req: Request, res: Response) => { if(req.player.hp <= 0) { logger.log(`Player didn\'t have enough hp`); return res.sendStatus(400); @@ -688,7 +688,7 @@ app.post('/fight', fightRateLimiter, authEndpoint, async (req: AuthRequest, res: res.send(renderFightPreRound(data, true, location, location.city_id)); }); -app.post('/travel/step', authEndpoint, async (req: AuthRequest, res: Response) => { +app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => { const stepTimerKey = `step:${req.player.id}`; const travelPlan = await getTravelPlan(req.player.id); @@ -755,7 +755,7 @@ app.post('/travel/step', authEndpoint, async (req: AuthRequest, res: Response) = } }); -app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res: Response) => { +app.post('/travel/return-to-source', authEndpoint, async (req: Request, res: Response) => { // puts the player back in their starting town // doesn't matter if they don't have one // redirect them! @@ -789,7 +789,7 @@ app.post('/travel/return-to-source', authEndpoint, async (req: AuthRequest, res: }); -app.post('/travel/:destination_id', authEndpoint, async (req: AuthRequest, res: Response) => { +app.post('/travel/:destination_id', authEndpoint, async (req: Request, res: Response) => { if(req.player.hp <= 0) { logger.log(`Player didn\'t have enough hp`); res.send(Alert.ErrorAlert('Sorry, you need some HP to start travelling.')); @@ -814,7 +814,7 @@ if(req.player.hp <= 0) { })); }); -app.get('/settings', authEndpoint, async (req: AuthRequest, res: Response) => { +app.get('/settings', authEndpoint, async (req: Request, res: Response) => { let warning = ''; let html = ''; if(req.player.account_type === 'session') { @@ -825,7 +825,7 @@ app.get('/settings', authEndpoint, async (req: AuthRequest, res: Response) => { res.send(warning + html); }); -app.post('/logout', authEndpoint, async (req: AuthRequest, res: Response) => { +app.post('/logout', authEndpoint, async (req: Request, res: Response) => { // ref to get the socket id for a particular player cache.delete(`socket:${req.player.id}`); // ref to get the player object diff --git a/src/server/auth.ts b/src/server/auth.ts index 456d293..9b4a3a4 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -6,10 +6,6 @@ import { Auth } from '../shared/auth'; import { db } from './lib/db'; import { Request, Response } from 'express'; -export interface AuthRequest extends Request { - player: Player -} - export async function signup(playerId: string, username: string, password: string): Promise { const salt = await bcrypt.genSalt(10); const hash = await bcrypt.hash(password, salt); @@ -63,7 +59,7 @@ export async function login(username: string, password: string): Promise } -export async function authEndpoint(req: AuthRequest, res: Response, next: any) { +export async function authEndpoint(req: Request, res: Response, next: any) { const authToken = req.headers['x-authtoken']; if(!authToken) { console.log(`Invalid auth token ${authToken}`); diff --git a/src/server/fight.ts b/src/server/fight.ts index 6e262df..c1bdc45 100644 --- a/src/server/fight.ts +++ b/src/server/fight.ts @@ -9,11 +9,10 @@ import { EquipmentSlot } from '../shared/inventory'; import { MonsterWithFaction, MonsterForFight } from '../shared/monsters'; import { getPlayerSkillsAsObject, updatePlayerSkills } from './skills'; import { SkillID, Skills } from '../shared/skills'; -import { AuthRequest } from './auth'; -import { Response } from 'express'; +import { Request, Response } from 'express'; import * as Alert from './views/alert'; -export async function blockPlayerInFight(req: AuthRequest, res: Response, next: any) { +export async function blockPlayerInFight(req: Request, res: Response, next: any) { const fight = await loadMonsterFromFight(req.player.id); if(!fight) { next(); diff --git a/src/server/locations/healer.ts b/src/server/locations/healer.ts index 2d12db0..3d7374b 100644 --- a/src/server/locations/healer.ts +++ b/src/server/locations/healer.ts @@ -1,6 +1,6 @@ -import { Response, Router } from "express"; +import { Request, Response, Router } from "express"; import { maxHp, maxVigor } from "../../shared/player"; -import { authEndpoint, AuthRequest } from '../auth'; +import { authEndpoint } from '../auth'; import { logger } from "../lib/logger"; import { updatePlayer } from "../player"; import { getCityDetails, getService } from '../map'; @@ -91,7 +91,7 @@ function getText(type: TextSegment, location: Location, city: City): string { } -router.get('/city/services/healer/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => { +router.get('/city/services/healer/:location_id', authEndpoint, async (req: Request, res: Response) => { const service = await getService(parseInt(req.params.location_id)); const city = await getCityDetails(service.city_id); @@ -134,7 +134,7 @@ ${BackToTown()} -router.post('/city/services/healer/heal/:location_id', authEndpoint, async (req: AuthRequest, res: Response) => { +router.post('/city/services/healer/heal/:location_id', authEndpoint, async (req: Request, res: Response) => { const service = await getService(parseInt(req.params.location_id)); const city = await getCityDetails(service.city_id); diff --git a/src/server/locations/recruiter.ts b/src/server/locations/recruiter.ts index 96f720d..fa46860 100644 --- a/src/server/locations/recruiter.ts +++ b/src/server/locations/recruiter.ts @@ -1,6 +1,6 @@ -import { Response, Router } from "express"; +import { Request, Response, Router } from "express"; import { getService } from "../map"; -import { authEndpoint, AuthRequest } from '../auth'; +import { authEndpoint } from '../auth'; import { logger } from "../lib/logger"; import * as Alert from "../views/alert"; import { changeProfession } from "../player"; @@ -15,7 +15,7 @@ export const router = Router(); const MIN_LEVEL = 25; -router.get('/city/services/profession_recruitor/:location_id', authEndpoint, async(req: AuthRequest, res: Response) => { +router.get('/city/services/profession_recruitor/:location_id', authEndpoint, async(req: Request, res: Response) => { const service = await getService(parseInt(req.params.location_id)); if(!service || service.city_id !== req.player.city_id) { @@ -80,7 +80,7 @@ router.get('/city/services/profession_recruitor/:location_id', authEndpoint, asy `); }); -router.post('/city/services/profession_change/:location_id', authEndpoint, async(req: AuthRequest, res: Response) => { +router.post('/city/services/profession_change/:location_id', authEndpoint, async(req: Request, res: Response) => { const service = await getService(parseInt(req.params.location_id)); if(!service || service.city_id !== req.player.city_id) { diff --git a/src/server/locations/repair.ts b/src/server/locations/repair.ts index 6bb7423..19d382c 100644 --- a/src/server/locations/repair.ts +++ b/src/server/locations/repair.ts @@ -1,5 +1,5 @@ -import { Response, Router } from "express"; -import { authEndpoint, AuthRequest } from '../auth'; +import { Request, Response, Router } from "express"; +import { authEndpoint } from '../auth'; import { logger } from "../lib/logger"; import { getService } from "../map"; import { getInventory, getInventoryItem, repair } from '../inventory'; @@ -11,7 +11,7 @@ import { renderPlayerBar } from "../views/player-bar"; export const router = Router(); -router.get('/city/services/repair/:location_id', authEndpoint, async(req: AuthRequest, res: Response) => { +router.get('/city/services/repair/:location_id', authEndpoint, async(req: Request, res: Response) => { const service = await getService(parseInt(req.params.location_id)); if(!service || service.city_id !== req.player.city_id) { @@ -28,7 +28,7 @@ router.get('/city/services/repair/:location_id', authEndpoint, async(req: AuthRe res.send(renderRepairService(damaged, req.player, service)); }); -router.post('/city/services/:location_id/repair/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => { +router.post('/city/services/:location_id/repair/:item_id', authEndpoint, async (req: Request, res: Response) => { const service = await getService(parseInt(req.params.location_id)); if(!service || service.city_id !== req.player.city_id) { diff --git a/src/types/express/index.d.ts b/src/types/express/index.d.ts new file mode 100644 index 0000000..9f12cde --- /dev/null +++ b/src/types/express/index.d.ts @@ -0,0 +1,7 @@ +import { Player } from '../../shared/player'; + +declare module 'express-serve-static-core' { + interface Request { + player: Player + } +} diff --git a/tsconfig.json b/tsconfig.json index 47b69bd..fa4cf87 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,18 @@ { - "compilerOptions": { - "module": "commonjs", - "esModuleInterop": true, - "target": "es6", - "moduleResolution": "node", - "removeComments": true, - "preserveConstEnums": true, - "sourceMap": true, - "baseUrl": "src", - "outDir": "dist", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "resolveJsonModule": true - }, - "include": ["src/server/api.ts"] + "compilerOptions": { + "module": "commonjs", + "esModuleInterop": true, + "target": "es6", + "moduleResolution": "node", + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": true, + "baseUrl": "src", + "outDir": "dist", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "typeRoots": ["./src/types"], + }, + "include": ["src/server/api.ts"] } -- 2.25.1 From bedec85a722b974beedd92ff86f07494048b11a5 Mon Sep 17 00:00:00 2001 From: xangelo Date: Tue, 5 Sep 2023 14:12:51 -0400 Subject: [PATCH 07/11] fix: min/max level definitions for monsters There was some confusion over types and whether or not we needed to included faction data with monsters (We don't). This should sort out that mess and give us a clearer definition for monsters. --- src/server/api.ts | 74 ++++++++++------------------ src/server/fight.ts | 7 +-- src/server/monster.ts | 3 +- src/server/views/fight.ts | 6 +-- src/server/views/monster-selector.ts | 2 +- src/shared/fight.ts | 4 +- src/shared/monsters.ts | 3 +- 7 files changed, 40 insertions(+), 59 deletions(-) diff --git a/src/server/api.ts b/src/server/api.ts index 355bd04..7428e79 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -166,24 +166,29 @@ app.post('/chat', authEndpoint, async (req: Request, res: Response) => { let message: Message; if(msg.startsWith('/server lmnop')) { - if(msg === '/server lmnop refresh-monsters') { - await createMonsters(); - message = broadcastMessage('server', 'Monster refresh!'); - } - else if(msg === '/server lmnop refresh-cities') { - await createAllCitiesAndLocations(); - message = broadcastMessage('server', 'Cities, Locations, and Paths refreshed!'); - } - else if(msg === '/server lmnop refresh-shops') { - await createShopItems(); - await createShopEquipment(); - message = broadcastMessage('server', 'Refresh shop items'); - } - else { - const str = msg.split('/server lmnop ')[1]; - if(str) { - message = broadcastMessage('server', str); + try { + if(msg === '/server lmnop refresh-monsters') { + await createMonsters(); + message = broadcastMessage('server', 'Monster refresh!'); + } + else if(msg === '/server lmnop refresh-cities') { + await createAllCitiesAndLocations(); + message = broadcastMessage('server', 'Cities, Locations, and Paths refreshed!'); + } + else if(msg === '/server lmnop refresh-shops') { + await createShopItems(); + await createShopEquipment(); + message = broadcastMessage('server', 'Refresh shop items'); } + else { + const str = msg.split('/server lmnop ')[1]; + if(str) { + message = broadcastMessage('server', str); + } + } + } + catch(e) { + io.to(cache.get(`socket:${req.player.id}`)).emit('chat', renderChatMessage(broadcastMessage('server', e.message))); } } else if(msg === '/online') { @@ -326,18 +331,9 @@ app.get('/player/explore', authEndpoint, async (req: Request, res: Response) => } if(fight) { - const data: MonsterForFight = { - id: fight.id, - hp: fight.hp, - maxHp: fight.maxHp, - name: fight.name, - level: fight.level, - fight_trigger: fight.fight_trigger - }; const location = await getMonsterLocation(fight.ref_id); - - res.send(renderPlayerBar(req.player) + renderFightPreRound(data, true, location, closestTown)); + res.send(renderPlayerBar(req.player) + renderFightPreRound(fight, true, location, closestTown)); } else { if(travelPlan) { @@ -605,7 +601,7 @@ app.post('/fight/turn', authEndpoint, async (req: Request, res: Response) => { } cache[fightBlockKey] = Date.now() + CONSTANT.FIGHT_ATTACK_DELAY; - const monster = await loadMonsterWithFaction(req.player.id); + const monster = await loadMonsterFromFight(req.player.id); if(!monster) { res.send(Alert.ErrorAlert('Not in a fight')); @@ -675,17 +671,7 @@ app.post('/fight', fightRateLimiter, authEndpoint, async (req: Request, res: Res const fight = await createFight(req.player.id, monster, fightTrigger); const location = await getService(monster.location_id); - - 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(renderFightPreRound(data, true, location, location.city_id)); + res.send(renderFightPreRound(fight, true, location, location.city_id)); }); app.post('/travel/step', authEndpoint, async (req: Request, res: Response) => { @@ -764,17 +750,9 @@ app.post('/travel/return-to-source', authEndpoint, async (req: Request, res: Res const fight = await loadMonsterFromFight(req.player.id); if(fight) { // go to the fight screen - const data: MonsterForFight = { - id: fight.id, - hp: fight.hp, - maxHp: fight.maxHp, - name: fight.name, - level: fight.level, - fight_trigger: fight.fight_trigger - }; const location = await getMonsterLocation(fight.ref_id); - res.send(renderPlayerBar(req.player) + renderFightPreRound(data, true, location, req.player.city_id)); + res.send(renderPlayerBar(req.player) + renderFightPreRound(fight, true, location, req.player.city_id)); } else { const [city, locations, paths] = await Promise.all([ diff --git a/src/server/fight.ts b/src/server/fight.ts index c1bdc45..ab7b2c6 100644 --- a/src/server/fight.ts +++ b/src/server/fight.ts @@ -6,7 +6,7 @@ import { updatePlayer } from './player'; import { getEquippedItems, updateAp } from './inventory'; import { EquippedItemDetails } from '../shared/equipped'; import { EquipmentSlot } from '../shared/inventory'; -import { MonsterWithFaction, MonsterForFight } from '../shared/monsters'; +import { MonsterWithFaction, MonsterForFight, Fight } from '../shared/monsters'; import { getPlayerSkillsAsObject, updatePlayerSkills } from './skills'; import { SkillID, Skills } from '../shared/skills'; import { Request, Response } from 'express'; @@ -35,7 +35,7 @@ function exponentialExp(exp: number, monsterLevel: number, playerLevel: number): return Math.floor(finalExp); } -export async function fightRound(player: Player, monster: MonsterWithFaction, data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) { +export async function fightRound(player: Player, monster: Fight, data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) { const playerSkills = await getPlayerSkillsAsObject(player.id); const roundData: FightRound = { monster, @@ -195,7 +195,8 @@ export async function fightRound(player: Player, monster: MonsterWithFaction, d return { id: monster.id, name: monster.name, - level: monster.minLevel, + minLevel: monster.minLevel, + maxLevel: monster.maxLevel, hp: monster.hp, maxHp: monster.maxHp, fight_trigger: 'explore' diff --git a/src/server/monster.ts b/src/server/monster.ts index 18c1243..54a49db 100644 --- a/src/server/monster.ts +++ b/src/server/monster.ts @@ -42,7 +42,8 @@ export async function loadMonsterFromFight(player_id: string): Promise { export async function loadMonsterWithFaction(player_id: string): Promise { const res = await db.raw(` select - f.*, fa.id as faction_id, fa.name as faction_name + f.*, fa.id as faction_id, fa.name as faction_name, + m.minLevel, m.maxLevel from fight f join monsters m on f.ref_id = m.id left outer join factions fa on m.faction_id = fa.id diff --git a/src/server/views/fight.ts b/src/server/views/fight.ts index f09ab41..b3ba986 100644 --- a/src/server/views/fight.ts +++ b/src/server/views/fight.ts @@ -1,6 +1,6 @@ import { FightRound } from "shared/fight"; import { LocationWithCity } from "shared/map"; -import { MonsterForFight } from "../../shared/monsters"; +import { Fight, MonsterForFight } from "../../shared/monsters"; import { Button, ButtonWithBlock } from "./components/button"; export function renderRoundDetails(roundData: FightRound): string { @@ -69,7 +69,7 @@ function AttackButton(blockTime?: number): string { } } -export function renderFight(monster: MonsterForFight, results: string = '', displayFightActions: boolean = true, blockTime: number = 0) { +export function renderFight(monster: Fight, results: string = '', displayFightActions: boolean = true, blockTime: number = 0) { const hpPercent = Math.floor((monster.hp / monster.maxHp) * 100); let html = ` @@ -105,7 +105,7 @@ export function renderFight(monster: MonsterForFight, results: string = '', disp return html; } -export function renderFightPreRound(monster: MonsterForFight, displayFightActions: boolean = true, location: LocationWithCity, closestTown: number) { +export function renderFightPreRound(monster: Fight, displayFightActions: boolean = true, location: LocationWithCity, closestTown: number) { const hpPercent = Math.floor((monster.hp / monster.maxHp) * 100); let html = ` diff --git a/src/server/views/monster-selector.ts b/src/server/views/monster-selector.ts index 03d6197..0fc5e5b 100644 --- a/src/server/views/monster-selector.ts +++ b/src/server/views/monster-selector.ts @@ -23,7 +23,7 @@ export function renderMonsterSelector(monsters: Monster[] | MonsterForFight[], a diff --git a/src/shared/fight.ts b/src/shared/fight.ts index d0f7b97..22f0ed9 100644 --- a/src/shared/fight.ts +++ b/src/shared/fight.ts @@ -1,4 +1,4 @@ -import {FightTrigger, MonsterWithFaction} from "./monsters" +import {Fight, FightTrigger, MonsterWithFaction} from "./monsters" import {Player} from "./player" export type FightReward = { @@ -8,7 +8,7 @@ export type FightReward = { } export type FightRound = { - monster: MonsterWithFaction, + monster: Fight, player: Player, fightTrigger: FightTrigger, winner: 'player' | 'monster' | 'in-progress', diff --git a/src/shared/monsters.ts b/src/shared/monsters.ts index 91c0dfa..f5147ff 100644 --- a/src/shared/monsters.ts +++ b/src/shared/monsters.ts @@ -45,6 +45,7 @@ export type MonsterForFight = { hp: number; maxHp: number; name: string; - level: number; + minLevel: number; + maxLevel: number; fight_trigger: FightTrigger; } -- 2.25.1 From c6a5f62efdb29dfbe89e3d2b6a8cea27e0f35e15 Mon Sep 17 00:00:00 2001 From: xangelo Date: Tue, 5 Sep 2023 15:37:32 -0400 Subject: [PATCH 08/11] feat: remove body-part targeting The body part targeting stopped working once we moved over to the new vigor system ,but the UI was left in just in case. --- src/server/api.ts | 3 +-- src/server/fight.ts | 2 +- src/server/views/fight.ts | 36 +----------------------------------- 3 files changed, 3 insertions(+), 38 deletions(-) diff --git a/src/server/api.ts b/src/server/api.ts index 7428e79..80aa82a 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -609,8 +609,7 @@ app.post('/fight/turn', authEndpoint, async (req: Request, res: Response) => { } const fightData = await fightRound(req.player, monster, { - action: req.body.action, - target: req.body.fightTarget + action: req.body.action }); diff --git a/src/server/fight.ts b/src/server/fight.ts index ab7b2c6..011cc5c 100644 --- a/src/server/fight.ts +++ b/src/server/fight.ts @@ -35,7 +35,7 @@ function exponentialExp(exp: number, monsterLevel: number, playerLevel: number): return Math.floor(finalExp); } -export async function fightRound(player: Player, monster: Fight, data: {action: 'attack' | 'cast' | 'flee', target: 'head' | 'body' | 'arms' | 'legs'}) { +export async function fightRound(player: Player, monster: Fight, data: {action: 'attack' | 'cast' | 'flee'}) { const playerSkills = await getPlayerSkillsAsObject(player.id); const roundData: FightRound = { monster, diff --git a/src/server/views/fight.ts b/src/server/views/fight.ts index b3ba986..b89f32e 100644 --- a/src/server/views/fight.ts +++ b/src/server/views/fight.ts @@ -86,12 +86,6 @@ export function renderFight(monster: Fight, results: string = '', displayFightAc
${displayFightActions ? `
- ${AttackButton(blockTime)} ${CastButton(blockTime)} @@ -106,8 +100,6 @@ export function renderFight(monster: Fight, results: string = '', displayFightAc } export function renderFightPreRound(monster: Fight, displayFightActions: boolean = true, location: LocationWithCity, closestTown: number) { - const hpPercent = Math.floor((monster.hp / monster.maxHp) * 100); - let html = `
@@ -116,34 +108,8 @@ export function renderFightPreRound(monster: Fight, displayFightActions: boolea

${location.name}

-
-
-
- -
-
-
${monster.name}, level ${monster.level}
-
${hpPercent}% - ${monster.hp} / ${monster.maxHp}
-
-
-
- ${displayFightActions ? ` - - - ${AttackButton()} - ${CastButton()} - - - `: ''} -
-
+ ${renderFight(monster, '', displayFightActions)}
-
`; -- 2.25.1 From b8bed2377f894d1c424e809fe0a1894c38851c78 Mon Sep 17 00:00:00 2001 From: xangelo Date: Wed, 6 Sep 2023 13:16:29 -0400 Subject: [PATCH 09/11] feat: monster variants Introduced Elder, Skittish, and Brute monster variants. These adjust the stats/rewards of the monsters further. --- public/assets/css/game.css | 14 +++++++++- src/server/monster.ts | 51 ++++++++++++++++++++++++----------- src/server/views/fight.ts | 2 +- src/shared/constants.ts | 2 ++ src/shared/monsters.ts | 55 +++++++++++++++++++++++++++++++++++++- 5 files changed, 106 insertions(+), 18 deletions(-) diff --git a/public/assets/css/game.css b/public/assets/css/game.css index da277db..898b78a 100644 --- a/public/assets/css/game.css +++ b/public/assets/css/game.css @@ -441,9 +441,21 @@ nav.filter-result.active { width: 70%; margin: 0 auto 1rem; } -#defender-name { +.monster-identifier { text-align: left; } +#defender-name { + font-weight: bold; +} +.Elder #defender-name { + color: #2b2b2b; +} +.Skittish #defender-name { + color: #8700ff; +} +.Brute #defender-name { + color: #a91313; +} #fight-results { margin-top: 1rem; } diff --git a/src/server/monster.ts b/src/server/monster.ts index 54a49db..a0b7aac 100644 --- a/src/server/monster.ts +++ b/src/server/monster.ts @@ -1,8 +1,9 @@ import { db } from './lib/db'; -import { Fight, Monster, MonsterWithFaction, MonsterForList, FightTrigger } from '../shared/monsters'; +import { Fight, Monster, MonsterWithFaction, MonsterForList, FightTrigger, MonsterVariant, MonsterVariants } from '../shared/monsters'; import { TimePeriod, TimeManager } from '../shared/time'; -import { LocationWithCity } from 'shared/map'; -import { max, random } from 'lodash'; +import { LocationWithCity } from '../shared/map'; +import { random, sample } from 'lodash'; +import { CHANCE_TO_FIGHT_SPECIAL } from '../shared/constants'; const time = new TimeManager(); @@ -67,23 +68,43 @@ export async function createFight(playerId: string, monster: Monster, fightTrigg const chosenLevel = random(monster.minLevel, monster.maxLevel); // 30% boost per level difference const modifier = Math.pow(Math.E, (chosenLevel - monster.minLevel)/monster.maxLevel); + let variant: MonsterVariant = { + name: '', + display: '{{name}}', + strength: 1, + constitution: 1, + dexterity: 1, + intelligence: 1, + exp: 1, + gold: 1, + maxHp: 1, + defence: 1 + }; + + if(monster.maxLevel >= 5 && random(0,100) <= CHANCE_TO_FIGHT_SPECIAL) { + variant = sample(MonsterVariants); + } - const res = await db('fight').insert({ + const monsterData: Omit = { player_id: playerId, - name: monster.name, - strength: Math.floor(monster.strength * modifier), - constitution: Math.floor(monster.constitution * modifier), - dexterity: Math.floor(monster.dexterity * modifier), - intelligence: Math.floor(monster.intelligence * modifier), - exp: Math.floor(monster.exp * modifier), + variant: variant.name, + name: variant.display.replace("{{name}}", monster.name), + strength: Math.floor(monster.strength * modifier * variant.strength), + constitution: Math.floor(monster.constitution * modifier * variant.constitution), + dexterity: Math.floor(monster.dexterity * modifier * variant.dexterity), + intelligence: Math.floor(monster.intelligence * modifier * variant.intelligence), + exp: Math.floor(monster.exp * modifier * variant.exp), level: chosenLevel, - gold: Math.floor(monster.gold * modifier), - hp: Math.floor(monster.hp * modifier), - defence: Math.floor(monster.defence * modifier), - maxHp: Math.floor(monster.maxHp * modifier), + gold: Math.floor(monster.gold * modifier * variant.exp), + hp: Math.floor(monster.hp * modifier * variant.maxHp), + defence: Math.floor(monster.defence * modifier * variant.defence), + maxHp: Math.floor(monster.maxHp * modifier * variant.maxHp), ref_id: monster.id, fight_trigger: fightTrigger - }).returning('*'); + } + + const res = await db('fight').insert(monsterData) + .returning('*'); return res.pop(); } diff --git a/src/server/views/fight.ts b/src/server/views/fight.ts index b89f32e..c66cdbd 100644 --- a/src/server/views/fight.ts +++ b/src/server/views/fight.ts @@ -79,7 +79,7 @@ export function renderFight(monster: Fight, results: string = '', displayFightAc
-
${monster.name}, level ${monster.level}
+
${monster.name}, level ${monster.level}
${hpPercent}% - ${monster.hp} / ${monster.maxHp}
diff --git a/src/shared/constants.ts b/src/shared/constants.ts index fae88bf..c95c80d 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,3 +1,5 @@ export const FIGHT_ATTACK_DELAY = 1500; export const STEP_DELAY = 2000; export const ALERT_DISPLAY_LENGTH = 3000; +// this is displayed as a percentage out of 100 +export const CHANCE_TO_FIGHT_SPECIAL = 100; diff --git a/src/shared/monsters.ts b/src/shared/monsters.ts index f5147ff..c7f8700 100644 --- a/src/shared/monsters.ts +++ b/src/shared/monsters.ts @@ -27,9 +27,10 @@ export type MonsterForList = { export type FightTrigger = 'explore' | 'travel'; -export type Fight = Omit & { +export type Fight = Omit & { id: string; player_id: string; + variant: string; level: number; ref_id: number; fight_trigger: FightTrigger; @@ -49,3 +50,55 @@ export type MonsterForFight = { maxLevel: number; fight_trigger: FightTrigger; } + +export type MonsterVariant = { + name: string; + display: string; + strength: number; + constitution: number; + dexterity: number; + intelligence: number; + exp: number; + gold: number; + maxHp: number; + defence: number; +}; + +export const MonsterVariants: MonsterVariant[] = [ + { + name: 'Brute', + display: '{{name}} Brute', + strength: 1, + constitution: 1, + dexterity: 0.6, + intelligence: 0.2, + exp: 4, + gold: 3, + maxHp: 2, + defence: 3 + }, + { + name: 'Elder', + display: 'Elder {{name}}', + strength: 0.8, + constitution: 1.2, + dexterity: 0.6, + intelligence: 1.6, + exp: 2, + gold: 1, + maxHp: 1, + defence: 1 + }, + { + name: 'Skittish', + display: 'Skittish {{name}}', + strength: 0.8, + constitution: 1.2, + dexterity: 0.6, + intelligence: 1.6, + exp: 1, + gold: 1.2, + maxHp: 1, + defence: 0.8 + } +]; -- 2.25.1 From d7569d31080cb8d55c5e57bae9a16f6a676c12ff Mon Sep 17 00:00:00 2001 From: xangelo Date: Wed, 6 Sep 2023 13:25:27 -0400 Subject: [PATCH 10/11] feat: display equippable hand option only When you are equipping items it will only show you the equip button for the hand that you have available --- src/server/views/inventory.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/server/views/inventory.ts b/src/server/views/inventory.ts index 6026775..88e3c8c 100644 --- a/src/server/views/inventory.ts +++ b/src/server/views/inventory.ts @@ -121,21 +121,30 @@ function renderInventoryItem(item: EquippedItemDetails , action: (item: Equipped } function renderInventorySection(inventory: EquippedItemDetails[]): string { + const used_slots = inventory.filter(i => i.is_equipped).map(i => i.equipment_slot); + return inventory.map(item => { return renderInventoryItem(item, item => { if(item.is_equipped) { return ``; } else { - if(item.equipment_slot === 'ANY_HAND') { - return ` -`; - } - else if(item.equipment_slot === 'LEFT_HAND') { - return ``; - } - else if(item.equipment_slot === 'RIGHT_HAND') { - return ``; + if(['ANY_HAND', 'LEFT_HAND', 'RIGHT_HAND'].includes(item.equipment_slot)) { + const str: string[] = [ + ``, + `` + ]; + + if(used_slots.includes('LEFT_HAND') && !used_slots.includes('RIGHT_HAND')) { + return str[1]; + } + else if(used_slots.includes('RIGHT_HAND') && !used_slots.includes('LEFT_HAND')) { + return str[0]; + } + else if(used_slots.includes('LEFT_HAND') && used_slots.includes('RIGHT_HAND')) { + return ""; + } + return str.join(""); } else { return ``; -- 2.25.1 From 5480e913e63fad43f61f06e3f50deda7cd6785d5 Mon Sep 17 00:00:00 2001 From: xangelo Date: Wed, 6 Sep 2023 13:28:29 -0400 Subject: [PATCH 11/11] chore(release): 0.3.3 --- CHANGELOG.md | 17 +++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d59abd..947be5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.3.3](https://git.xangelo.ca/?p=risinglegends.git;a=commitdiff;h=v0.3.3;hp=v0.3.2;ds=sidebyside) (2023-09-06) + + +### Features + +* add debug/error methods to logger 3238965 +* display equippable hand option only d7569d3 +* migrate to augmenting express.Request interface d820e11 +* monster variants b8bed23 +* remove body-part targeting c6a5f62 +* variable level monsters b5577b4 + + +### Bug Fixes + +* min/max level definitions for monsters bedec85 + ### [0.3.2](https://git.xangelo.ca/?p=risinglegends.git;a=commitdiff;h=v0.3.2;hp=v0.3.1;ds=sidebyside) (2023-09-01) diff --git a/package-lock.json b/package-lock.json index 4d86644..bf0c893 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rising-legends", - "version": "0.3.2", + "version": "0.3.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "rising-legends", - "version": "0.3.2", + "version": "0.3.3", "dependencies": { "@honeycombio/opentelemetry-node": "^0.4.0", "@opentelemetry/auto-instrumentations-node": "^0.37.0", diff --git a/package.json b/package.json index 72c58f7..fe8ae12 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "rising-legends", "private": true, - "version": "0.3.2", + "version": "0.3.3", "scripts": { "up": "npx prisma migrate dev --name \"init\"", "start": "pm2 start dist/server/api.js", -- 2.25.1