feat: variable level monsters
authorxangelo <me@xangelo.ca>
Fri, 1 Sep 2023 18:47:24 +0000 (14:47 -0400)
committerxangelo <me@xangelo.ca>
Fri, 1 Sep 2023 19:27:19 +0000 (15:27 -0400)
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 [new file with mode: 0644]
seeds/monsters.ts
src/server/fight.ts
src/server/monster.ts
src/server/views/fight.ts
src/server/views/monster-selector.ts
src/shared/monsters.ts

diff --git a/migrations/20230901182406_monster-variance.ts b/migrations/20230901182406_monster-variance.ts
new file mode 100644 (file)
index 0000000..35e6db0
--- /dev/null
@@ -0,0 +1,20 @@
+import { Knex } from "knex";
+
+
+export async function up(knex: Knex): Promise<void> {
+  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<void> {
+  return knex.schema.alterTable('monsters', function(table) {
+    table.dropColumn('minLevel');
+    table.dropColumn('maxLevel');
+    table.integer('level').notNullable().defaultTo(1);
+  });
+}
+
index 44afa4f723d8542914e86542187e6b9b015d1fa1..02eecb821d279c342517f93b7f9ac9e63a20c5db 100644 (file)
@@ -50,12 +50,13 @@ export async function createMonsters(): Promise<void> {
         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,
index 2c9383d19b361f22d2c36fbd40420f151c64a85c..6e262dfe1c2ea34329de281d90543686715dc7f4 100644 (file)
@@ -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'
index 943a868205bedd89cee65471efd21111ab7c0435..18c12435660fdc389054fd922b1992ff1552d34b 100644 (file)
@@ -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<Monster>('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<Fight> {
+  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<Fight[]>('*');
index 06ebcdb170d8971288de6ae765fd9fbba57e8812..f09ab41ce244efc199813cadeac077f8ddac3c93 100644 (file)
@@ -79,7 +79,7 @@ export function renderFight(monster: MonsterForFight, results: string = '', disp
         <img id="avatar" src="https://via.placeholder.com/64x64">
       </div>
       <div id="defender-stat-bars">
-        <div id="defender-name">${monster.name}</div>
+        <div id="defender-name">${monster.name}, level ${monster.level}</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>
@@ -122,7 +122,7 @@ export function renderFightPreRound(monster: MonsterForFight,  displayFightActio
         <img id="avatar" src="https://via.placeholder.com/64x64">
       </div>
       <div id="defender-stat-bars">
-        <div id="defender-name">${monster.name}</div>
+        <div id="defender-name">${monster.name}, level ${monster.level}</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>
index e357835be3082955452dfd74686657b29a54e35a..03d61979d1eef92ac1437b4829f2d41dff90cac0 100644 (file)
@@ -22,9 +22,9 @@ export function renderMonsterSelector(monsters: Monster[] | MonsterForFight[], a
 <div class="service-in-town"><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: (Monster | MonsterForFight)) => {
-      const range = [monster.level, monster.level + 3];
-      return `<option value="${monster.id}" ${monster.id === activeMonsterId ? 'selected': ''}>${monster.name} (${range[0]} - ${range[1]})</option>`;
+  ${monsters.map((monster: (any)) => {
+      const range = [monster?.minLevel ?? monster.level, monster?.maxLevel ?? monster.level + 3];
+      return `<option value="${monster.id}" ${monster.id === activeMonsterId ? 'selected': ''}>${monster.name} (LVL ${range[0]} - ${range[1]})</option>`;
   }).join("\n")}
   </select> <button type="submit" class="red">Fight</button></form>
 <br><br>
index c29a4777800047d5e4cc3acfb408940352410aa3..91c0dfade191e42c3736471bce41b9a9ca1d5c40 100644 (file)
@@ -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<Monster, 'id' | 'faction_id' | 'location_id'> & { 
-  id: string,
-  player_id: string,
-  ref_id: number
-  fight_trigger: FightTrigger
+export type Fight = Omit<Monster, 'id' | 'faction_id' | 'location_id' | 'minLevel' | 'maxLevel'> & { 
+  id: string;
+  player_id: string;
+  level: number;
+  ref_id: number;
+  fight_trigger: FightTrigger;
 };
 
 export type MonsterWithFaction = Fight & {