fix: migrate explore fight to htmx
authorxangelo <me@xangelo.ca>
Fri, 4 Aug 2023 17:58:50 +0000 (13:58 -0400)
committerxangelo <me@xangelo.ca>
Fri, 4 Aug 2023 17:58:50 +0000 (13:58 -0400)
src/server/api.ts
src/server/monster.ts
src/server/views/fight.ts [new file with mode: 0644]
src/server/views/map.ts
src/server/views/monster-selector.ts [new file with mode: 0644]

index 0a91abf03e4afce158815d8ad3fa60761b11a9ee..2bd10ed5037c23ad2220a6ab84c1462413d24f28 100644 (file)
@@ -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<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
-    const weapons: EquippedItemDetails[] = [];
-    let anyDamageSpells: boolean = false;
-    equippedItems.forEach(item => {
-      if(item.type === 'ARMOUR') {
-        equipment.set(item.equipment_slot, item);
-      }
-      else if(item.type === 'WEAPON') {
-        weapons.push(item);
-      }
-      else if(item.type === 'SPELL') {
-        if(item.affectedSkills.includes('destruction_magic')) {
-          anyDamageSpells = true;
-        }
-        weapons.push(item);
-      }
+  // we only use this if the player successfully defeated the monster 
+  // they were fighting, then we load the other monsters in this area 
+  // so they can "fight again"
+  let potentialMonsters: MonsterForFight[] = [];
+
+  /*
+   * cumulative chance of head/arms/body/legs
+   * 0 -> 0.2 = head
+   * 0.21 -> 0.4 = arms
+   *
+   * we use the factor to decide how many decimal places 
+   * we care about
+   */
+  const factor = 100;
+  const monsterTarget = [0.2, 0.4, 0.9, 1];
+  const targets: ArmourEquipmentSlot[] = ['HEAD', 'CHEST', 'ARMS', 'LEGS'];
+  // calc weighted
+  const rand = Math.ceil(Math.random() * factor);
+  let target: ArmourEquipmentSlot = 'CHEST';
+  monsterTarget.forEach((i, idx) => {
+    if (rand > (i * factor)) {
+      target = targets[idx] as ArmourEquipmentSlot;
+    }
+  });
 
-      boost.strength += item.boosts.strength;
-      boost.constitution += item.boosts.constitution;
-      boost.dexterity += item.boosts.dexterity;
-      boost.intelligence += item.boosts.intelligence;
+  const boost = {
+    strength: 0,
+    constitution: 0,
+    dexterity: 0,
+    intelligence: 0,
+    damage: 0,
+    hp: 0,
+  };
 
-      if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
-        boost.hp += item.boosts.damage;
-      }
-      else {
-        boost.damage += item.boosts.damage;
+  const equipment: Map<EquipmentSlot, EquippedItemDetails> = new Map<EquipmentSlot, EquippedItemDetails>();
+  const weapons: EquippedItemDetails[] = [];
+  let anyDamageSpells: boolean = false;
+  equippedItems.forEach(item => {
+    if(item.type === 'ARMOUR') {
+      equipment.set(item.equipment_slot, item);
+    }
+    else if(item.type === 'WEAPON') {
+      weapons.push(item);
+    }
+    else if(item.type === 'SPELL') {
+      if(item.affectedSkills.includes('destruction_magic')) {
+        anyDamageSpells = true;
       }
-    });
+      weapons.push(item);
+    }
 
-    // if you flee'd, then we want to check your dex vs. the monsters
-    // but we want to give you the item/weapon boosts you need
-    // if not then you're going to get hit.
-    if(data.action === 'flee') {
-      roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
-      roundData.winner = 'monster';
-      await clearFight(player.id);
+    boost.strength += item.boosts.strength;
+    boost.constitution += item.boosts.constitution;
+    boost.dexterity += item.boosts.dexterity;
+    boost.intelligence += item.boosts.intelligence;
 
-      socket.emit('fight-over', {roundData, monsters: []});
-      return;
+    if(item.type === 'SPELL' && item.affectedSkills.includes('restoration_magic')) {
+      boost.hp += item.boosts.damage;
+    }
+    else {
+      boost.damage += item.boosts.damage;
     }
+  });
 
-    const attackType = data.action === 'attack' ? 'physical' : 'magical';
-    const primaryStat = data.action === 'attack' ? player.strength : player.intelligence;
-    const boostStat = data.action === 'attack' ? boost.strength : boost.intelligence;
-
-    const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
-    const skillsUsed: Record<SkillID | any, number> = {};
-    let hpHealAfterMasteries: number = -1;
-    let playerDamageAfterMasteries: number = 0;
-    // apply masteries!
-    weapons.forEach(item => {
-      item.affectedSkills.forEach(id => {
-        if(id === 'restoration_magic') {
-          if(hpHealAfterMasteries < 0) {
-            hpHealAfterMasteries = 0;
-          }
-          hpHealAfterMasteries += Skills.get(id).effect(playerSkills.get(id));
-        }
-        else {
-          playerDamageAfterMasteries += playerDamage * Skills.get(id).effect(playerSkills.get(id));
+  // if you flee'd, then we want to check your dex vs. the monsters
+  // but we want to give you the item/weapon boosts you need
+  // if not then you're going to get hit.
+  if(data.action === 'flee') {
+    roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
+    roundData.winner = 'monster';
+    await clearFight(player.id);
+
+    return { roundData, monsters: [] };
+  }
+
+  const attackType = data.action === 'attack' ? 'physical' : 'magical';
+  const primaryStat = data.action === 'attack' ? player.strength : player.intelligence;
+  const boostStat = data.action === 'attack' ? boost.strength : boost.intelligence;
+
+  const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
+  const skillsUsed: Record<SkillID | any, number> = {};
+  let hpHealAfterMasteries: number = -1;
+  let playerDamageAfterMasteries: number = 0;
+  // apply masteries!
+  weapons.forEach(item => {
+    item.affectedSkills.forEach(id => {
+      if(id === 'restoration_magic') {
+        if(hpHealAfterMasteries < 0) {
+          hpHealAfterMasteries = 0;
         }
+        hpHealAfterMasteries += Skills.get(id).effect(playerSkills.get(id));
+      }
+      else {
+        playerDamageAfterMasteries += playerDamage * Skills.get(id).effect(playerSkills.get(id));
+      }
 
-        if(!skillsUsed[id]) {
-          skillsUsed[id] = 0;
-        }
-        skillsUsed[id]++;
-      });
+      if(!skillsUsed[id]) {
+        skillsUsed[id] = 0;
+      }
+      skillsUsed[id]++;
     });
+  });
 
-    await updatePlayerSkills(player.id, skillsUsed);
-
-    const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
-    const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
-
-    roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
-    let armourKey: string;
-    switch(data.target) {
-      case 'arms':
-        armourKey = 'armsAp';
-        break;
-      case 'head':
-        armourKey = 'helmAp';
-        break;
-      case 'legs':
-        armourKey = 'legsAp';
-        break;
-      case 'body':
-        armourKey = 'chestAp';
-        break;
+  await updatePlayerSkills(player.id, skillsUsed);
+
+  const playerFinalDamage = (data.action === 'cast' && !anyDamageSpells) ? 0 : Math.floor(playerDamage + playerDamageAfterMasteries);
+  const playerFinalHeal = Math.floor(boost.hp + hpHealAfterMasteries);
+
+  roundData.roundDetails.push(`You targeted the monsters ${data.target.toUpperCase()} with ${attackType} damage!`);
+  let armourKey: string;
+  switch(data.target) {
+    case 'arms':
+      armourKey = 'armsAp';
+      break;
+    case 'head':
+      armourKey = 'helmAp';
+      break;
+    case 'legs':
+      armourKey = 'legsAp';
+      break;
+    case 'body':
+      armourKey = 'chestAp';
+      break;
+  }
+
+  if(monster[armourKey] && monster[armourKey] > 0) {
+    monster[armourKey] -= playerFinalDamage;
+
+    roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
+    if(monster[armourKey] < 0) {
+      roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
+      roundData.roundDetails.push(`You dealt ${monster[armourKey] * -1} damage to their HP`);
+      monster.hp += monster[armourKey];
+      monster[armourKey] = 0;
     }
+  }
+  else {
+    roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
+    monster.hp -= playerFinalDamage;
+  }
 
-    if(monster[armourKey] && monster[armourKey] > 0) {
-      monster[armourKey] -= playerFinalDamage;
+  if(monster.hp <= 0) {
+    roundData.monster.hp = 0;
+    roundData.winner = 'player';
 
-      roundData.roundDetails.push(`You dealt ${playerFinalDamage} damage to their armour`);
-      if(monster[armourKey] < 0) {
-        roundData.roundDetails.push(`You destroyed the ${monster.name}'s armour!'`);
-        roundData.roundDetails.push(`You dealt ${monster[armourKey] * -1} damage to their HP`);
-        monster.hp += monster[armourKey];
-        monster[armourKey] = 0;
-      }
-    }
-    else {
-      roundData.roundDetails.push(`You hit the ${monster.name} for ${playerFinalDamage} damage.`);
-      monster.hp -= playerFinalDamage;
-    }
+    roundData.rewards.exp = monster.exp;
+    roundData.rewards.gold = monster.gold;
+
+    player.gold += monster.gold;
+    player.exp += monster.exp;
 
-    if(monster.hp <= 0) {
-      roundData.monster.hp = 0;
-      roundData.winner = 'player';
+    if(player.exp >= expToLevel(player.level + 1)) {
+      player.exp -= expToLevel(player.level + 1)
+      player.level++;
+      roundData.rewards.levelIncrease = true;
+      let statPointsGained = 1;
 
-      roundData.rewards.exp = monster.exp;
-      roundData.rewards.gold = monster.gold;
+      if(player.profession !== 'Wanderer') {
+        statPointsGained = 2;
+      }
 
-      player.gold += monster.gold;
-      player.exp += monster.exp;
+      player.stat_points += statPointsGained;
 
-      if(player.exp >= expToLevel(player.level + 1)) {
-        player.exp -= expToLevel(player.level + 1)
-        player.level++;
-        roundData.rewards.levelIncrease = true;
-        let statPointsGained = 1;
+      roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
 
-        if(player.profession !== 'Wanderer') {
-          statPointsGained = 2;
+      player.hp = maxHp(player.constitution, player.level);
+    }
+    // get the monster location if it was an EXPLORED fight
+    if(roundData.fightTrigger === 'explore') {
+      const rawMonster = await loadMonster(monster.ref_id);
+      const monsterList  = await getMonsterList(rawMonster.location_id);
+      potentialMonsters = monsterList.map(monster => {
+        return {
+          id: monster.id,
+          name: monster.name,
+          level: monster.level,
+          hp: monster.hp,
+          maxHp: monster.maxHp,
+          fight_trigger: 'explore'
         }
+      });
+    }
 
-        player.stat_points += statPointsGained;
+    await clearFight(player.id);
+    await updatePlayer(player);
+    return { roundData, monsters: potentialMonsters };
+  }
 
-        roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
+  roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
+  if(equipment.has(target)) {
+    const item = equipment.get(target);
+    // apply mitigation!
+    const mitigationPercentage = item.boosts.damage_mitigation || 0;
+    const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
 
-        player.hp = maxHp(player.constitution, player.level);
-      }
-      // get the monster location if it was an EXPLORED fight
-      if(roundData.fightTrigger === 'explore') {
-        const rawMonster = await loadMonster(monster.ref_id);
-        const monsterList  = await getMonsterList(rawMonster.location_id);
-        potentialMonsters = monsterList.map(monster => {
-          return {
-            id: monster.id,
-            name: monster.name,
-            level: monster.level,
-            hp: monster.hp,
-            maxHp: monster.maxHp,
-            fight_trigger: 'explore'
-          }
-        });
-      }
-
-      await clearFight(player.id);
-      await updatePlayer(player);
-      cache.set(authToken, player);
-      socket.emit('fight-over', {roundData, monsters: potentialMonsters});
-      return;
-    }
-
-    roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
-    if(equipment.has(target)) {
-      const item = equipment.get(target);
-      // apply mitigation!
-      const mitigationPercentage = item.boosts.damage_mitigation || 0;
-      const damageAfterMitigation = Math.floor(monster.strength * ((100-mitigationPercentage)/100));
-
-      item.currentAp -= damageAfterMitigation;
-
-      if(item.currentAp < 0) {
-        roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
-        roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
-        player.hp += item.currentAp;
-        item.currentAp = 0;
-        await deleteInventoryItem(player.id, item.item_id);
-      }
-      else {
-        roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
-        await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
-      }
+    item.currentAp -= damageAfterMitigation;
 
+    if(item.currentAp < 0) {
+      roundData.roundDetails.push(`Your ${item.name} amour was destroyed`);
+      roundData.roundDetails.push(`The ${monster.name} hit your HP for ${item.currentAp * -1} damage!`);
+      player.hp += item.currentAp;
+      item.currentAp = 0;
+      await deleteInventoryItem(player.id, item.item_id);
     }
     else {
-      roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
-      player.hp -= monster.strength;
+      roundData.roundDetails.push(`Your ${target} took ${damageAfterMitigation} damage!`);
+      await updateAp(player.id, item.item_id, item.currentAp, item.maxAp);
     }
 
-    if(playerFinalHeal > 0) {
-      player.hp += playerFinalHeal;
-      if(player.hp > maxHp(player.constitution, player.level)) {
-        player.hp = maxHp(player.constitution, player.level);
-      }
-      roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
-    }
-
-    // update the players inventory for this item!
-
-    if(player.hp <= 0) {
-      player.hp = 0;
-      roundData.winner = 'monster';
+  }
+  else {
+    roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
+    player.hp -= monster.strength;
+  }
 
-      roundData.roundDetails.push(`You were killed by the ${monster.name}`);
+  if(playerFinalHeal > 0) {
+    player.hp += playerFinalHeal;
+    if(player.hp > maxHp(player.constitution, player.level)) {
+      player.hp = maxHp(player.constitution, player.level);
+    }
+    roundData.roundDetails.push(`You healed for ${playerFinalHeal} HP`);
+  }
 
-      await clearFight(player.id);
-      await updatePlayer(player);
-      await clearTravelPlan(player.id);
+  // update the players inventory for this item!
 
-      cache.set(authToken, player);
+  if(player.hp <= 0) {
+    player.hp = 0;
+    roundData.winner = 'monster';
 
-      socket.emit('fight-over', {roundData, monsters: []});
-      return;
-    }
+    roundData.roundDetails.push(`You were killed by the ${monster.name}`);
 
+    await clearFight(player.id);
     await updatePlayer(player);
-    await saveFightState(player.id, monster);
-    cache.set(authToken, player);
+    await clearTravelPlan(player.id);
 
-    calcAp(equippedItems, socket);
-    socket.emit('fight-round', roundData);
-  });
+    return { roundData, monsters: []};
+  }
 
-  // this is a special event to let the client know it can start 
-  // requesting data
-  socket.emit('ready');
-});
+  await updatePlayer(player);
+  await saveFightState(player.id, monster);
+
+  return { roundData, monsters: []};
+};
 
 app.use(healerRouter);
 
@@ -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) => {
index 1d5f0fd44608e0d49b23e1761eea528c31a260f9..7565146436f687578c619fdae62d0498bf99b9fa 100644 (file)
@@ -31,13 +31,13 @@ export async function loadMonster(id: number): Promise<Monster> {
   }).first();
 }
 
-export async function loadMonsterFromFight(authToken: string): Promise<Fight> {
+export async function loadMonsterFromFight(player_id: string): Promise<Fight> {
   return await db.first().select('*').from<Fight>('fight').where({
-    player_id: authToken,
+    player_id,
   });
 }
 
-export async function loadMonsterWithFaction(authToken: string): Promise<MonsterWithFaction> {
+export async function loadMonsterWithFaction(player_id: string): Promise<MonsterWithFaction> {
   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<Monster
                       left outer join factions fa on m.faction_id = fa.id
                       where f.player_id = ?
                         limit 1
-                      `, [authToken]);
+                      `, [player_id]);
 
   return res.rows[0];
 }
diff --git a/src/server/views/fight.ts b/src/server/views/fight.ts
new file mode 100644 (file)
index 0000000..0000245
--- /dev/null
@@ -0,0 +1,68 @@
+import { FightRound } from "shared/fight";
+import { MonsterForFight } from "../../shared/monsters";
+
+export function renderRoundDetails(roundData: FightRound): string {
+  let html: string[] = roundData.roundDetails.map(d => `<div>${d}</div>`);
+
+  switch(roundData.winner) {
+    case 'player':
+      html.push(`<div>You defeated the ${roundData.monster.name}!</div>`);
+      if(roundData.rewards.gold) {
+        html.push(`<div>You gained ${roundData.rewards.gold} gold`);
+      }
+      if(roundData.rewards.exp) {
+        html.push(`<div>You gained ${roundData.rewards.exp} exp`);
+      }
+      if(roundData.rewards.levelIncrease) {
+        html.push(`<div>You gained a level! ${roundData.player.level}`);
+      }
+    break;
+    case 'monster':
+      // prompt to return to town and don't let them do anything
+      html.push(`<p>You were killed...</p>`);
+      html.push('<p><button class="emit-event-internal" data-event="tab:explore" data-args="">Back to Town</button></p>');
+    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 = `<div id="fight-container">
+    <div id="defender-info">
+      <div class="avatar-container">
+        <img id="avatar" src="https://via.placeholder.com/64x64">
+      </div>
+      <div id="defender-stat-bars">
+        <div id="defender-name">${monster.name}</div>
+        <div class="progress-bar" id="defender-hp-bar" style="background: linear-gradient(to right, red, red ${hpPercent}%, transparent ${hpPercent}%, transparent)" title="${hpPercent}% - ${monster.hp}/${monster.maxHp}">${hpPercent}% - ${monster.hp} / ${monster.maxHp}</div>
+      </div>
+    </div>
+    <div id="fight-actions">
+      ${displayFightActions ? `
+      <form hx-post="/fight/turn" hx-target="#fight-container">
+        <select id="fight-target" name="fightTarget">
+          <option value="head">Head</option>
+          <option value="body">Body</option>
+          <option value="arms">Arms</option>
+          <option value="legs">Legs</option>
+        </select>
+        <button type="submit" class="fight-action" name="action" value="attack">Attack</button>
+        <button type="submit" class="fight-action" name="action" value="cast">Cast</button>
+        <button type="submit" class="fight-action" name="action" value="flee">Flee</button>
+      </form>
+      `: ''}
+      </div>
+  </form>
+    <div id="fight-results">${results}</div>
+  </div>`;
+
+  return html;
+}
index dc1eedc7b86694b140935bbc4a89fe980bdef06b..7626ab1829e3ed37eeb04cee149f08906b63a706 100644 (file)
@@ -13,7 +13,7 @@ export async function renderMap(data: { city: City, locations: Location[], paths
   };
 
   data.locations.forEach(l => {
-    servicesParsed[l.type].push(`<a href="#" hx-get="/city/${l.type.toLowerCase()}/${l.event_name}/${l.id}" hx-trigger="click" id="location-${l.id}">${l.name}</a>`);
+    servicesParsed[l.type].push(`<a href="#" hx-get="/city/${l.type.toLowerCase()}/${l.event_name}/${l.id}" hx-target="#map">${l.name}</a>`);
   });
 
   let html = `
diff --git a/src/server/views/monster-selector.ts b/src/server/views/monster-selector.ts
new file mode 100644 (file)
index 0000000..370de3b
--- /dev/null
@@ -0,0 +1,13 @@
+import { Monster, MonsterForFight } from "../../shared/monsters";
+
+export function renderMonsterSelector(monsters: Monster[] | MonsterForFight[]): string {
+  let html = `<form id="fight-selector" hx-post="/fight" hx-target="#explore">
+  <input type="hidden" name="fightTrigger" value="explore">
+  <select id="monsterId" name="monsterId">
+  ${monsters.map((monster) => {
+    return `<option value="${monster.id}">${monster.name}</option>`;
+  }).join("\n")}
+  </select> <button type="submit">Fight</button></form>`;
+
+  return html;
+}