feat: remove stamina as a game mechanic
authorxangelo <me@xangelo.ca>
Thu, 16 Jan 2025 04:41:33 +0000 (23:41 -0500)
committerxangelo <me@xangelo.ca>
Thu, 16 Jan 2025 04:41:33 +0000 (23:41 -0500)
after play-testing, its come up that the stamina mechanic is just confusing players.

it is called "vigor", which most games don't use, It regens too quicklt to actually
slow user actions down. The whole game is an active pbbg, so restricting actions
that the player has to manually do is just not fun.

migrations/20250115194051_remove_stamina.ts [new file with mode: 0644]
src/server/fight.ts
src/server/player.ts
src/server/routes/locations/dungeon.ts
src/server/routes/locations/healer.ts
src/server/routes/profile.ts
src/server/views/player-bar.ts
src/server/views/profile.ts
src/shared/player.ts

diff --git a/migrations/20250115194051_remove_stamina.ts b/migrations/20250115194051_remove_stamina.ts
new file mode 100644 (file)
index 0000000..6cf68d0
--- /dev/null
@@ -0,0 +1,13 @@
+import { Knex } from "knex";
+
+export async function up(knex: Knex): Promise<void> {
+    await knex.schema.alterTable('players', table => {
+        table.dropColumn('stamina');
+    });
+}
+
+export async function down(knex: Knex): Promise<void> {
+    await knex.schema.alterTable('players', table => {
+        table.integer('stamina').defaultTo(0);
+    });
+}
index dda4a7aa54a911496dfa4ae895fbeb441fb3e173..8c1ecdbbec69ed7eae9713dde16e59c74967ecb5 100644 (file)
@@ -12,7 +12,6 @@ import {
   expToLevel,
   maxHp,
   totalDefence,
-  maxVigor,
   baseDamage,
 } from "../shared/player";
 import { clearTravelPlan } from "./map";
@@ -184,7 +183,7 @@ export async function fightRound(
     }
 }
 
-  // @TODO implement flee based on dex + vigor
+  // @TODO implement flee based on dex
   if (data.action === "flee") {
     roundData.roundDetails.push(
       `You managed to escape from the ${monster.name}!`
@@ -318,7 +317,6 @@ export async function fightRound(
       );
 
       player.hp = maxHp(player.constitution, player.level);
-      player.vigor = maxVigor(player.constitution, player.level);
     }
     // get the monster location if it was an EXPLORED fight
     if (roundData.fightTrigger === "explore") {
@@ -337,11 +335,6 @@ export async function fightRound(
       });
     }
 
-    player.vigor -= 1;
-    if (player.vigor < 0) {
-      player.vigor = 0;
-    }
-
     const unequippedItems = await updateAp(
       player.id,
       1,
@@ -381,7 +374,6 @@ export async function fightRound(
 
   if (player.hp <= 0) {
     player.hp = 0;
-    player.vigor = 0;
     roundData.winner = "monster";
 
     roundData.roundDetails.push(`You were killed by the ${monster.name}`);
index 36910ac514b22bf98be2c6aecc6f4fe670210c20..6c94fca33e31cf89bc058370ef31100fc98b137f 100644 (file)
@@ -1,5 +1,5 @@
 import { db } from './lib/db';
-import {Player, maxHp, maxVigor} from "@shared/player";
+import {Player, maxHp} from "@shared/player";
 import { random } from 'lodash';
 import { Skills } from '@shared/skills';
 import {Profession} from '@shared/profession';
@@ -68,7 +68,6 @@ export async function createPlayer(): Promise<Player> {
   };
 
   raw.hp = maxHp(raw.constitution, 1);
-  raw.vigor = maxVigor(raw.constitution, 1);
 
   const res  = await db('players').insert(raw).returning<Player[]>('*');
   const player = res.pop();
@@ -103,7 +102,6 @@ export async function updatePlayer(player: Player) {
       id: player.id
     }).update({
       hp: player.hp,
-      vigor: player.vigor,
       strength: player.strength,
       constitution: player.constitution,
       dexterity: player.dexterity,
index 6ed09b39e6f3f9c6f7bf1fdc1f7137df6edb7520..ef025e2f3855d36fec8bd9757f40aaeae5cee725 100644 (file)
@@ -8,7 +8,7 @@ import * as Alert from '../../views/alert';
 import { createFight, loadMonster } from "../../monster";
 import { renderFightPreRoundDungeon } from "../../views/fight";
 import { has, max, each } from 'lodash';
-import { expToLevel, maxHp, maxVigor } from "../../../shared/player";
+import { expToLevel, maxHp} from "../../../shared/player";
 import { updatePlayer } from "../../player";
 import { getEventHistoryToday } from "../../events";
 
@@ -175,7 +175,6 @@ dungeonRouter.post('/city/dungeon/:dungeon_id/complete', authEndpoint, async (re
     req.player.level++;
     req.player.stat_points += req.player.profession === 'Wanderer' ? 1 : 2;
     req.player.hp = maxHp(req.player.constitution, req.player.level);
-    req.player.vigor = maxVigor(req.player.constitution, req.player.level);
   }
 
   // delete the tracking for this dungeon-run
index 6edc0b579ee6b079a0c9581c4ce87239cbdd3bdf..a82ccbc22b3ab772095ffbdc1f27c16945a5c921 100644 (file)
@@ -1,5 +1,5 @@
 import { Request, Response, Router } from "express";
-import { maxHp, maxVigor } from "../../../shared/player";
+import { maxHp} from "../../../shared/player";
 import { authEndpoint } from '../../auth';
 import { updatePlayer } from "../../player";
 import { getCityDetails, getService } from '../../map';
@@ -103,7 +103,7 @@ healerRouter.get('/city/services/healer/:location_id', authEndpoint, async (req:
 
   text.push(`<p>"${getText('intro', service, city)}"</p>`);
 
-  if(req.player.hp === maxHp(req.player.constitution, req.player.level) && req.player.vigor === maxVigor(req.player.constitution, req.player.level)) {
+  if(req.player.hp === maxHp(req.player.constitution, req.player.level)) {
     text.push(`<p>You're already in peak condition!</p>`);
   }
   else {
@@ -150,7 +150,6 @@ healerRouter.post('/city/services/healer/heal/:location_id', authEndpoint, async
   }
   else {
     req.player.hp = maxHp(req.player.constitution, req.player.level);
-    req.player.vigor = maxVigor(req.player.constitution, req.player.level);
     req.player.gold -= cost;
 
     await updatePlayer(req.player);
index 79b0bd50cad62a4735be9ed139865dc8c7e36925..4d862dffa9a56379913f681f716659f2649b5641 100644 (file)
@@ -2,7 +2,7 @@ import { Request, Response, Router } from 'express';
 import { authEndpoint } from '../auth';
 import { getEquippedItems } from '../inventory';
 import { updatePlayer } from '../player';
-import {maxHp, maxVigor } from '../../shared/player';
+import {maxHp} from '../../shared/player';
 import * as Alert from '../views/alert';
 import { renderPlayerBar } from '../views/player-bar'
 import { renderProfilePage } from '../views/profile';
@@ -41,7 +41,6 @@ profileRouter.post('/player/stat/:stat', authEndpoint, async (req: Request, res:
   req.player[stat]++;
 
   req.player.hp = maxHp(req.player.constitution, req.player.level);
-  req.player.vigor = maxVigor(req.player.constitution, req.player.level);
   updatePlayer(req.player);
 
   res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment));
index aa5679907ccab2792cdf4ec0dce98faec4011a97..50dc9066db6685349792203940af39e789628b82 100644 (file)
@@ -1,4 +1,4 @@
-import { expToLevel, maxHp, maxVigor, Player } from "@shared/player";
+import { expToLevel, maxHp, Player } from "@shared/player";
 import { ProgressBar } from "./components/progress-bar";
 
 function displayLoginSignupForm(): string {
@@ -32,7 +32,6 @@ export function renderPlayerBar(player: Player): string {
         <div class="gold">${player.gold.toLocaleString()}</div>
       </div>
       ${ProgressBar(player.hp, maxHp(player.constitution, player.level), 'hp-bar', { endingColor: '#ff7070', startingColor: '#d62f2f', title: 'HP', displayPercent: true })}
-      ${ProgressBar(player.vigor, maxVigor(player.constitution, player.level), 'vigor-bar', { endingColor: '#5ebb5e', startingColor: '#7be67b', title: 'Vigor', displayPercent: true})}
       ${ProgressBar(player.exp, expToLevel(player.level + 1), 'exp-bar', { endingColor: '#5997f9', startingColor: '#1d64d4', title: 'EXP', displayPercent: true})}
     </div>
     ${player.account_type === 'session' ? displayLoginSignupForm() : ''}
index 27e2b13bafcda702c273065edee6e3f5644fb2bc..100007a820e2782caa438a129de7166a9584f9ce 100644 (file)
@@ -1,5 +1,5 @@
 import { EquippedItemDetails } from "@shared/equipped";
-import { expToLevel, maxHp, maxVigor, Player, StatDef, StatDisplay, totalDefence } from "@shared/player";
+import { expToLevel, maxHp, Player, StatDef, StatDisplay, totalDefence } from "@shared/player";
 
 function statPointIncreaser(stat: StatDisplay) {
   return `<button class="increase-stat" hx-post="/player/stat/${stat.id}" hx-target="#profile">+</button>`;
@@ -25,17 +25,13 @@ export function renderProfilePage(player: Player, equipment: EquippedItemDetails
       <th title="The total amount of damage you can take before you pass out" tabindex="0" class="tooltip">HP</th>
       <td>${player.hp.toLocaleString()}/${maxHp(player.constitution, player.level).toLocaleString()}</td>
     </tr>
-    <tr>
-      <th title="Your energy level. Low vigor will cause your overall defence and damage to drop." tabindex="0" class="tooltip">Vigor</th>
-      <td>${player.vigor.toLocaleString()}/${maxVigor(player.constitution, player.level).toLocaleString()}</td>
-    </tr>
     <tr>
       <th title="How many experience points you need to get to your next level" tabindex="0" class="tooltip">EXP</th>
       <td>${player.exp.toLocaleString()}/${expToLevel(player.level + 1).toLocaleString()}</td>
     </tr>
     <tr>
-      <th title="The max defence you can have (and your true defence affected by your vigor)" tabindex="0" class="tooltip">Defence</th>
-      <td>${totalDefence(equipment, player, false).toLocaleString()} (${totalDefence(equipment, player).toLocaleString()})</td>
+      <th title="The max defence you can have" tabindex="0" class="tooltip">Defence</th>
+      <td>${totalDefence(equipment, player).toLocaleString()} (${totalDefence(equipment, player).toLocaleString()})</td>
     </tr>
     <tr>
       <th title="You can use these to increase the base stats below" tabindex="0" class="tooltip">Stat Points</th>
index d9aac734ee109881e458aaf6cb76b7669a3b0900..e8dc02773ca58824993d358ecd800983a415c668 100644 (file)
@@ -21,14 +21,12 @@ export type Player = {
   constitution: number;
   dexterity: number;
   intelligence: number;
-  stamina: number;
   exp: number;
   level: number;
   gold: number;
   hp: number;
   city_id: number;
   stat_points: number;
-  vigor: number;
   permissions: Permission[]
 }
 
@@ -40,10 +38,6 @@ export function maxHp(constitution: number, playerLevel: number): number {
   return Math.ceil((constitution * 1.7) + (playerLevel * 1.3));
 }
 
-export function maxVigor(constitution: number, playerLevel: number): number {
-  return Math.ceil((constitution * 3.8) + (playerLevel * 1.5));
-}
-
 export function expToLevel(level: number): number {
   if(level < 10) {
     return level * 10 - 10;
@@ -53,20 +47,13 @@ export function expToLevel(level: number): number {
   }
 }
 
-export function totalDefence(equippedItems: EquippedItemDetails[], player: Player, accountForVigor: boolean = true): number {
-  const vigorPercent = player.vigor / maxVigor(player.constitution, player.level);
-
+export function totalDefence(equippedItems: EquippedItemDetails[], player: Player): number {
   const totalDefence = equippedItems.reduce((acc, curr) => {
     let defence = curr.boosts.defence ?? 0;
     return acc += defence;
   }, 0);
 
-  if(accountForVigor) {
-    return Math.ceil(totalDefence * vigorPercent);
-  }
-  else {
-    return totalDefence;
-  }
+  return totalDefence;
 }
 
 export function baseDamage(statValue: number, boostStat: number, damage: number): number {
@@ -93,7 +80,7 @@ StatDef.set(Stat.constitution, {
   id: Stat.constitution,
   display: 'Constitution',
   abbrv: 'CON',
-  description: 'Affects your max HP and Vigor'
+  description: 'Affects your max HP'
 });
 
 StatDef.set(Stat.dexterity, {