From 161b5bff66119d174887ea17024bc9c3600e7336 Mon Sep 17 00:00:00 2001 From: xangelo Date: Tue, 29 Aug 2023 15:34:47 -0400 Subject: [PATCH] feat: repairing damaged equipment If your equipment is damaged in battle, you can visit the Iron Smith to repair it for a fraction of what you would pay to buy new equipment! --- src/server/api.ts | 4 +- src/server/inventory.ts | 9 +++ src/server/locations/repair.ts | 70 +++++++++++++++++ src/server/views/components/city.ts | 12 +++ src/server/views/inventory.ts | 2 +- src/server/views/repair.ts | 115 ++++++++++++++++++++++++++++ src/server/views/stores.ts | 26 ++++++- src/shared/inventory.ts | 8 ++ 8 files changed, 240 insertions(+), 6 deletions(-) create mode 100644 src/server/locations/repair.ts create mode 100644 src/server/views/components/city.ts create mode 100644 src/server/views/repair.ts diff --git a/src/server/api.ts b/src/server/api.ts index 61e7f5c..77d54b2 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -28,8 +28,9 @@ import { getPlayerSkills} from './skills'; import { fightRound, blockPlayerInFight } from './fight'; -import { router as healerRouter } from './locations/healer'; +import { router as healerRouter } from './locations/healer'; import { router as professionRouter } from './locations/recruiter'; +import { router as repairRouter } from './locations/repair'; import * as Alert from './views/alert'; import { renderPlayerBar } from './views/player-bar' @@ -130,6 +131,7 @@ io.on('connection', async socket => { app.use(healerRouter); app.use(professionRouter); +app.use(repairRouter); app.get('/chat/history', authEndpoint, async (req: AuthRequest, res: Response) => { diff --git a/src/server/inventory.ts b/src/server/inventory.ts index 71e5da0..3074644 100644 --- a/src/server/inventory.ts +++ b/src/server/inventory.ts @@ -94,6 +94,15 @@ export async function updateAp(player_id: string, apDamage: number, itemIds: str return itemsToUnequip; } +export async function repair(player_id: string, item_id: string) { + return db('inventory').where({ + player_id, + item_id + }).update({ + 'currentAp': db.raw('"maxAp"') + }); +} + export async function deleteInventoryItem(player_id: string, item_id: string) { await db('equipped').where({ player_id, diff --git a/src/server/locations/repair.ts b/src/server/locations/repair.ts new file mode 100644 index 0000000..6bb7423 --- /dev/null +++ b/src/server/locations/repair.ts @@ -0,0 +1,70 @@ +import { Response, Router } from "express"; +import { authEndpoint, AuthRequest } from '../auth'; +import { logger } from "../lib/logger"; +import { getService } from "../map"; +import { getInventory, getInventoryItem, repair } from '../inventory'; +import { renderRepairService } from '../views/repair'; +import { repairCost } from "../../shared/inventory"; +import * as Alert from "../views/alert"; +import { updatePlayer } from "../player"; +import { renderPlayerBar } from "../views/player-bar"; + +export const router = Router(); + +router.get('/city/services/repair/:location_id', authEndpoint, async(req: AuthRequest, res: Response) => { + const service = await getService(parseInt(req.params.location_id)); + + if(!service || service.city_id !== req.player.city_id) { + logger.log(`Invalid location: [${req.params.location_id}]`); + res.sendStatus(400); + } + + const equippedItems = await getInventory(req.player.id); + + const damaged = equippedItems.filter(i => { + return i.currentAp < i.maxAp + }); + + res.send(renderRepairService(damaged, req.player, service)); +}); + +router.post('/city/services/:location_id/repair/:item_id', authEndpoint, async (req: AuthRequest, res: Response) => { + const service = await getService(parseInt(req.params.location_id)); + + if(!service || service.city_id !== req.player.city_id) { + logger.log(`Invalid location: [${req.params.location_id}]`); + res.sendStatus(400); + } + + const item = await getInventoryItem(req.player.id, req.params.item_id); + + if(!item) { + logger.log(`Invalid item [${req.params.item_id}]`); + res.sendStatus(400); + } + + const cost = repairCost(item); + + if(req.player.gold < cost) { + res.status(400).send(Alert.ErrorAlert(`You need at least ${cost}G to repair your ${item.name}`)); + return; + } + + req.player.gold -= cost; + item.currentAp = item.maxAp; + + await updatePlayer(req.player); + await repair(req.player.id, item.item_id); + + const equippedItems = await getInventory(req.player.id); + + const damaged = equippedItems.filter(i => { + return i.currentAp < i.maxAp + }); + + res.send( + renderRepairService(damaged, req.player, service) + + renderPlayerBar(req.player) + + Alert.SuccessAlert(`You repaired your ${item.name} for ${cost}G`) + ); +}); diff --git a/src/server/views/components/city.ts b/src/server/views/components/city.ts new file mode 100644 index 0000000..18eb3e2 --- /dev/null +++ b/src/server/views/components/city.ts @@ -0,0 +1,12 @@ +export function Title(str: string): string { + return `
${str}
`; +} + +export function Details(name: string, content: string): string { + return ` +
+

${name}

+ ${content} +
+ `; +} diff --git a/src/server/views/inventory.ts b/src/server/views/inventory.ts index bef40c3..e21d1fd 100644 --- a/src/server/views/inventory.ts +++ b/src/server/views/inventory.ts @@ -113,7 +113,7 @@ function renderInventoryItem(item: EquippedItemDetails , action: (item: Equipped ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''} ${item.boosts.damage ? renderStatBoost('DMG', item.boosts.damage) : ''} ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''} - ${['WEAPON','SPELL'].includes(item.type) ? '': generateProgressBar(item.currentAp, item.maxAp, '#7be67b')} + ${['SPELL'].includes(item.type) ? '': generateProgressBar(item.currentAp, item.maxAp, '#7be67b')} ${item.hasOwnProperty('id') ? `
${item.cost.toLocaleString()}G
` : ''} diff --git a/src/server/views/repair.ts b/src/server/views/repair.ts new file mode 100644 index 0000000..447dc84 --- /dev/null +++ b/src/server/views/repair.ts @@ -0,0 +1,115 @@ +import { capitalize } from "lodash"; +import { Player } from "../../shared/player"; +import { repairCost } from "../../shared/inventory"; +import { LocationWithCity } from "../../shared/map"; +import { EquippedItemDetails } from "../../shared/equipped"; +import { ProgressBar } from "./components/progress-bar"; +import * as City from './components/city'; + +function renderStatBoost(name: string, val: number | string): string { + let valSign: string = ''; + if(typeof val === 'number') { + valSign = val > 0 ? '+' : '-'; + } + return `${name}: ${valSign}${val}`; +} + +function renderRequirement(name: string, val: number | string, currentVal?: number): string { + let colorIndicator = ''; + if(currentVal) { + colorIndicator = currentVal >= val ? 'success' : 'error'; + } + return `${name}: ${val.toLocaleString()}`; +} + +export function renderEquipmentDetails(item: EquippedItemDetails, player: Player): string { + return ` +
+
${item.name}${item.equipment_slot === 'TWO_HANDED' ? ' (2H)': ''}
+
+ ${item.requirements.level ? renderRequirement('LVL', item.requirements.level, player.level): ''} + ${item.requirements.strength ? renderRequirement('STR', item.requirements.strength, player.strength): ''} + ${item.requirements.constitution ? renderRequirement('CON', item.requirements.constitution, player.constitution): ''} + ${item.requirements.dexterity ? renderRequirement('DEX', item.requirements.dexterity, player.dexterity): ''} + ${item.requirements.intelligence ? renderRequirement('INT', item.requirements.intelligence, player.intelligence): ''} + ${renderRequirement('PRF', item.profession)} +
+
+ ${item.boosts.defence ? renderStatBoost('DEF', item.boosts.defence) : ''} + ${item.boosts.strength ? renderStatBoost('STR', item.boosts.strength) : ''} + ${item.boosts.constitution ? renderStatBoost('CON', item.boosts.constitution) : ''} + ${item.boosts.dexterity ? renderStatBoost('DEX', item.boosts.dexterity) : ''} + ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''} + ${item.boosts.damage ? renderStatBoost(item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item.boosts.damage) : ''} + ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''} + ${['WEAPON','SPELL'].includes(item.type) ? '' : ProgressBar(item.currentAp, item.maxAp, `${item.item_id}-ap`, { +displayPercent: false, +title: 'Durability', +startingColor: '#7be67b', +endingColor: '#7be67b' +})} +
+
${repairCost(item).toLocaleString()}G to Repair
+
+` + +} + +function renderEquipmentToRepair(item: EquippedItemDetails, action: (item: EquippedItemDetails) => string, player: Player): string { + return `
+
+
${action(item)}
+
+ ${renderEquipmentDetails(item, player)} +
`; +} + + + +export function renderRepairService(equipment: EquippedItemDetails[], player: Player, location: LocationWithCity): string { + const listing: Record = {}; + const listingTypes = new Set(); + + if(equipment.length === 0) { + return ` + ${City.Title(location.city_name)} + ${City.Details(location.name, `You don't have any equipment that needs repairing.`)} + `; + } + + equipment.forEach(item => { + const filter = item.type === 'ARMOUR' ? item.equipment_slot : item.type; + + listingTypes.add(filter); + if(!listing[filter]) { + listing[filter] = ''; + } + + listing[filter] += renderEquipmentToRepair(item, i => { + return `` + }, player); + + }); + + let activeTab: string = listingTypes.keys().next().value; + + const nav: string[] = []; + const finalListing: string[] = []; + + listingTypes.forEach(type => { + nav.push(`${capitalize(type)}`); + finalListing.push(`
${listing[type]}
`); + }); + + let html = ` + ${City.Title(location.city_name)} + ${City.Details(location.name, ` +
+
+ ${finalListing.join("\n")} +
+
+`)}`; + + return html; +} diff --git a/src/server/views/stores.ts b/src/server/views/stores.ts index 75e1a30..be20cf0 100644 --- a/src/server/views/stores.ts +++ b/src/server/views/stores.ts @@ -1,15 +1,33 @@ import { ShopEquipment } from "../../shared/inventory"; import { ShopItem, Item } from "../../shared/items"; -import { capitalize } from "lodash"; +import { capitalize, merge } from "lodash"; import { Player } from "../../shared/player"; import { LocationWithCity } from "shared/map"; +import { ProgressBar } from "./components/progress-bar"; -function renderStatBoost(name: string, val: number | string): string { +type RenderStatOptions = { + unsigned: boolean +} + +function renderStat(title: string, display: string, val: any, options?: RenderStatOptions): string { + const opts = merge({ + unsigned: false + } as RenderStatOptions, options); + + let valSign: string = ''; + if(typeof val === 'number') { + valSign = val > 0 ? '+' : '-'; + } + + return `${display}: ${opts.unsigned ? val : `${valSign}${val}`}`; +} + +function renderStatBoost(name: string, val: number | string, title?: string): string { let valSign: string = ''; if(typeof val === 'number') { valSign = val > 0 ? '+' : '-'; } - return `${name}: ${valSign}${val}`; + return `${name}: ${valSign}${val}`; } function renderRequirement(name: string, val: number | string, currentVal?: number): string { @@ -55,7 +73,7 @@ export function renderEquipmentDetails(item: ShopEquipment, player: Player): str ${item.boosts.intelligence ? renderStatBoost('INT', item.boosts.intelligence) : ''} ${item.boosts.damage ? renderStatBoost(item.affectedSkills.includes('restoration_magic') ? 'HP' : 'DMG', item.boosts.damage) : ''} ${item.boosts.damage_mitigation ? renderStatBoost('MIT', item.boosts.damage_mitigation.toString())+'%' : ''} - ${['WEAPON','SPELL'].includes(item.type) ? '' : renderStatBoost('AP', item.maxAp.toString())} + ${['SPELL'].includes(item.type) ? '' : renderStat('Durability', 'DUR', item.maxAp, { unsigned: true })} ${item.hasOwnProperty('id') ? `
${item.cost.toLocaleString()}G
` : ''} diff --git a/src/shared/inventory.ts b/src/shared/inventory.ts index c0b0859..0ed26dd 100644 --- a/src/shared/inventory.ts +++ b/src/shared/inventory.ts @@ -46,3 +46,11 @@ export type ShopEquipment = Omit & { id: number; location_id: number; }; + +export function repairCost(item: InventoryItem): number { + const totalCost = item.cost * 0.7; + + const damageRatio = 1 - (item.currentAp / item.maxAp); + + return Math.floor(totalCost * damageRatio); +} -- 2.25.1