From d955aa8811dca26f5e29fd0e7c17ba628fec381d Mon Sep 17 00:00:00 2001 From: xangelo Date: Fri, 4 Aug 2023 13:58:50 -0400 Subject: [PATCH] fix: migrate explore fight to htmx --- src/server/api.ts | 537 +++++++++++++++------------ src/server/monster.ts | 8 +- src/server/views/fight.ts | 68 ++++ src/server/views/map.ts | 2 +- src/server/views/monster-selector.ts | 13 + 5 files changed, 378 insertions(+), 250 deletions(-) create mode 100644 src/server/views/fight.ts create mode 100644 src/server/views/monster-selector.ts diff --git a/src/server/api.ts b/src/server/api.ts index 0a91abf..2bd10ed 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -3,6 +3,7 @@ import { version } from "../../package.json"; 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'; @@ -15,7 +16,7 @@ import {clearFight, createFight, getMonsterList, loadMonster, loadMonsterFromFig 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'; @@ -27,13 +28,15 @@ import {SkillID, Skills} from '../shared/skills'; 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'; @@ -50,6 +53,7 @@ const app = express(); 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); @@ -182,285 +186,270 @@ io.on('connection', async socket => { } }); - 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 = new Map(); - 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 = new Map(); + 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 = {}; - 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 = {}; + 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); @@ -606,7 +595,16 @@ app.get('/player/explore', authEndpoint, async (req: Request, res: Response) => 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); @@ -825,6 +823,25 @@ app.get('/city/stores/city:stores/:location_id', authEndpoint, async (req: Reque 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) @@ -845,6 +862,34 @@ app.post('/travel', authEndpoint, async (req: Request, res: Response) => { 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(); @@ -883,6 +928,7 @@ app.post('/fight', authEndpoint, async (req: Request, res: Response) => { const fight = await createFight(player.id, monster, fightTrigger); + const data: MonsterForFight = { id: fight.id, hp: fight.hp, @@ -891,7 +937,8 @@ app.post('/fight', authEndpoint, async (req: Request, res: Response) => { level: fight.level, fight_trigger: fight.fight_trigger }; - res.json(data); + + res.send(renderFight(data)); }); app.post('/signup', async (req: Request, res: Response) => { diff --git a/src/server/monster.ts b/src/server/monster.ts index 1d5f0fd..7565146 100644 --- a/src/server/monster.ts +++ b/src/server/monster.ts @@ -31,13 +31,13 @@ export async function loadMonster(id: number): Promise { }).first(); } -export async function loadMonsterFromFight(authToken: string): Promise { +export async function loadMonsterFromFight(player_id: string): Promise { return await db.first().select('*').from('fight').where({ - player_id: authToken, + player_id, }); } -export async function loadMonsterWithFaction(authToken: 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 @@ -46,7 +46,7 @@ export async function loadMonsterWithFaction(authToken: string): Promise `
${d}
`); + + switch(roundData.winner) { + case 'player': + html.push(`
You defeated the ${roundData.monster.name}!
`); + if(roundData.rewards.gold) { + html.push(`
You gained ${roundData.rewards.gold} gold`); + } + if(roundData.rewards.exp) { + html.push(`
You gained ${roundData.rewards.exp} exp`); + } + if(roundData.rewards.levelIncrease) { + html.push(`
You gained a level! ${roundData.player.level}`); + } + break; + case 'monster': + // prompt to return to town and don't let them do anything + html.push(`

You were killed...

`); + html.push('

'); + break; + case 'in-progress': + // still in progress? + console.log('in progress still'); + break; + } + + + return html.join("\n"); +} + +export function renderFight(monster: MonsterForFight, results: string = '', displayFightActions: boolean = true) { + const hpPercent = Math.floor((monster.hp / monster.maxHp) * 100); + + let html = `
+
+
+ +
+
+
${monster.name}
+
${hpPercent}% - ${monster.hp} / ${monster.maxHp}
+
+
+
+ ${displayFightActions ? ` +
+ + + + +
+ `: ''} +
+ +
${results}
+
`; + + return html; +} diff --git a/src/server/views/map.ts b/src/server/views/map.ts index dc1eedc..7626ab1 100644 --- a/src/server/views/map.ts +++ b/src/server/views/map.ts @@ -13,7 +13,7 @@ export async function renderMap(data: { city: City, locations: Location[], paths }; data.locations.forEach(l => { - servicesParsed[l.type].push(`${l.name}`); + servicesParsed[l.type].push(`${l.name}`); }); let html = ` diff --git a/src/server/views/monster-selector.ts b/src/server/views/monster-selector.ts new file mode 100644 index 0000000..370de3b --- /dev/null +++ b/src/server/views/monster-selector.ts @@ -0,0 +1,13 @@ +import { Monster, MonsterForFight } from "../../shared/monsters"; + +export function renderMonsterSelector(monsters: Monster[] | MonsterForFight[]): string { + let html = `
+ +
`; + + return html; +} -- 2.25.1