--- /dev/null
+import { Knex } from "knex";
+
+
+const monsterColumns = ['helmAp', 'chestAp', 'armsAp', 'legsAp'];
+
+export async function up(knex: Knex): Promise<void> {
+ return knex.schema.alterTable('players', function(table) {
+ table.integer('vigor').defaultTo(0);
+ }).alterTable('monsters', function(table) {
+ monsterColumns.forEach(col => {
+ table.dropColumn(col);
+ });
+ table.integer('defence').notNullable().defaultTo(0);
+ }).alterTable('fight', function(table) {
+ monsterColumns.forEach(col => {
+ table.dropColumn(col);
+ });
+ table.integer('defence').notNullable().defaultTo(0);
+ });
+}
+
+
+export async function down(knex: Knex): Promise<void> {
+ return knex.schema.alterTable('players', function(table) {
+ table.dropColumn('vigor');
+ }).alterTable('monsters', function(table) {
+ monsterColumns.forEach(col => {
+ table.integer(col).defaultTo(0)
+ });
+ table.dropColumn('defence');
+ }).alterTable('fight', function(table) {
+ monsterColumns.forEach(col => {
+ table.integer(col).defaultTo(0)
+ });
+ table.dropColumn('defence');
+ });
+}
+
background-color: #fff;
margin-bottom: 1rem;
}
+#announcements {
+ margin: 1rem 0;
+}
#signup {
display: flex;
padding: 1rem;
}
-#stat-breakdown th {
+.stat-breakdown th {
font-weight: bold;
text-align: right;
background-color: #6d251c;
color: #fff;
background-image: url(data:image/png;base64,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);
}
-#stat-breakdown th, #stat-breakdown td {
+.stat-breakdown th, .stat-breakdown td {
padding: 0.5rem;
+ min-width: 100px;
+ line-height: 1rem;
+}
+.stat-breakdown tr:nth-child(even) {
+ background-color: #c7b7a1;
}
#explore {
#skill-list {
width: 100%;
}
-#skill-list tr:nth-child(even) {
- background-color: #eee;
-}
#skill-list .skill-level {
font-size: 2rem;
vertical-align: middle;
text-align: center;
border: solid 1px #000;
}
+#skill-list .skill-details table {
+ width: 100%;
+}
+#skill-list .skill-title {
+ text-align: left;
+ padding: 0.6rem 0.6rem 0 0.6rem;
+ line-height: 1.2rem;
+ font-weight: bold;
+}
#skill-list .skill-description {
- padding: 0 0.6rem;
+ padding: 0.6rem;
line-height: 1.2rem;
}
#skill-list .skill-exp {
- float: right;
+ text-align: right;
+ padding-right: 0.6rem;
+}
+#skill-list tr:nth-child(even) .skill-details {
+ background-color: #c7b7a1;
}
margin-top: 2rem;
text-align: center;
}
+
+/* tooltip styling */
+@media(pointer: coarse), (hover: none) {
+ [title] {
+ position: realtive;
+ display: flex;
+ justify-content: center;
+ }
+ [title]:focus::after {
+ content: attr(title);
+ background-color: #fff;
+ color: #222;
+ font-size: 14px;
+ padding: 8px 12px;
+ max-height: 100px;
+ height: fit-content;
+ width: fit-content;
+ position: absolute;
+ text-align: center;
+ left: 50%;
+ transform: translate(-100%, 0%) scale(1);
+ transform-origin: top;
+ display: block;
+ box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.05);
+ overflow: auto;
+ }
+}
</header>
<div id="signup-prompt" class="hidden"></div>
- <div id="announcements" class="hidden"></div>
<div id="alerts"></div>
<div id="modal-wrapper"></div>
gold: r.fields.GOLD,
hp: r.fields.HP,
maxHp: r.fields.HP,
- helmAp: r.fields.helmAp,
- chestAp: r.fields.chestAp,
- legsAp: r.fields.legsAp,
- armsAp: r.fields.armsAm,
+ defence: Math.floor(parseInt(r.fields.Defence.toString() || '0')),
location_id: r.fields.location_id[0],
faction_id: factionId,
time_period: r.fields.time_period ? r.fields.time_period : 'any'
dexterity: r.fields['Boost DEX'],
intelligence: r.fields['Boost INT'],
damage: r.fields['Boost DMG'],
- damage_mitigation: r.fields['Damage Mitigation']
+ damage_mitigation: r.fields['Damage Mitigation'],
+ defence: r.fields['Defence'],
},
currentAp: r.fields['Armour Points'],
maxAp: r.fields['Armour Points'],
import { loadPlayer, createPlayer, updatePlayer, movePlayer } from './player';
import { random, sample } from 'lodash';
import {broadcastMessage, Message} from '../shared/message';
-import {maxHp, Player} from '../shared/player';
+import {maxHp, maxVigor, Player} from '../shared/player';
import {createFight, getMonsterList, getMonsterLocation, getRandomMonster, loadMonster, loadMonsterFromFight, loadMonsterWithFaction} from './monster';
import {addInventoryItem, getEquippedItems, getInventory, getInventoryItem} from './inventory';
import { getItemFromPlayer, getItemFromShop, getPlayersItems, getShopItems, givePlayerItem, updateItemCount } from './items';
});
app.get('/player', authEndpoint, async (req: AuthRequest, res: Response) => {
- const inventory = await getEquippedItems(req.player.id);
-
- res.send(renderPlayerBar(req.player, inventory) + renderProfilePage(req.player));
+ const equipment = await getEquippedItems(req.player.id);
+ res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment));
});
app.post('/player/stat/:stat', authEndpoint, async (req: AuthRequest, res: Response) => {
+ const equipment = await getEquippedItems(req.player.id);
const stat = req.params.stat;
if(!['strength', 'constitution', 'dexterity', 'intelligence'].includes(stat)) {
res.send(Alert.ErrorAlert(`Sorry, that's not a valid stat to increase`));
req.player.stat_points -= 1;
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);
- const equippedItems = await getEquippedItems(req.player.id);
- res.send(renderPlayerBar(req.player, equippedItems) + renderProfilePage(req.player));
+ res.send(renderPlayerBar(req.player) + renderProfilePage(req.player, equipment));
});
app.get('/player/skills', authEndpoint, async (req: AuthRequest, res: Response) => {
getPlayersItems(req.player.id)
]);
- res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(req.player, inventory));
+ res.send(renderInventoryPage(inventory, items, inventoryItem.type) + renderPlayerBar(req.player));
});
app.post('/player/unequip/:item_id', authEndpoint, blockPlayerInFight, async (req: AuthRequest, res: Response) => {
getPlayersItems(req.player.id)
]);
- res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player, inventory));
+ res.send(renderInventoryPage(inventory, items, item.type) + renderPlayerBar(req.player));
});
app.get('/player/explore', authEndpoint, async (req: AuthRequest, res: Response) => {
closestTown = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id;
}
- const equippedItems = await getEquippedItems(req.player.id);
if(fight) {
const data: MonsterForFight = {
id: fight.id,
const location = await getMonsterLocation(fight.ref_id);
- res.send(renderPlayerBar(req.player, equippedItems) + renderFightPreRound(data, true, location, closestTown));
+ res.send(renderPlayerBar(req.player) + renderFightPreRound(data, true, location, closestTown));
}
else {
if(travelPlan) {
// STEP_DELAY
const nextAction = cache[`step:${req.player.id}`] || 0;
- res.send(renderPlayerBar(req.player, equippedItems) + renderTravel({
+ res.send(renderPlayerBar(req.player) + renderTravel({
things,
nextAction,
closestTown: closestTown,
getAllPaths(req.player.city_id)
]);
- res.send(renderPlayerBar(req.player, equippedItems) + await renderMap({city, locations, paths}, closestTown));
+ res.send(renderPlayerBar(req.player) + await renderMap({city, locations, paths}, closestTown));
}
}
await updatePlayer(req.player);
await addInventoryItem(req.player.id, item);
- const equippedItems = await getEquippedItems(req.player.id);
-
- res.send(renderPlayerBar(req.player, equippedItems) + Alert.SuccessAlert(`You purchased ${item.name}`));
+ res.send(renderPlayerBar(req.player) + Alert.SuccessAlert(`You purchased ${item.name}`));
});
// used to purchase items from a particular shop
await updatePlayer(req.player);
await givePlayerItem(req.player.id, item.id, 1);
- const equippedItems = await getEquippedItems(req.player.id);
-
- res.send(renderPlayerBar(req.player, equippedItems) + Alert.SuccessAlert(`You purchased a ${item.name}`));
+ res.send(renderPlayerBar(req.player) + Alert.SuccessAlert(`You purchased a ${item.name}`));
});
// used to display equipment modals in a store, validates that
await updatePlayer(req.player);
const inventory = await getInventory(req.player.id);
- const equippedItems = inventory.filter(i => i.is_equipped);
const items = await getPlayersItems(req.player.id);
res.send(
[
- renderPlayerBar(req.player, equippedItems),
+ renderPlayerBar(req.player),
renderInventoryPage(inventory, items, 'ITEMS'),
Alert.SuccessAlert(`You used the ${item.name}`)
].join("")
travelSection = travelButton(0);
}
- const equippedItems = await getEquippedItems(req.player.id);
- const playerBar = renderPlayerBar(fightData.player, equippedItems);
+ const playerBar = renderPlayerBar(fightData.player);
res.send(html + travelSection + playerBar);
});
// doesn't matter if they don't have one
// redirect them!
await clearTravelPlan(req.player.id);
- const equippedItems = await getEquippedItems(req.player.id);
const fight = await loadMonsterFromFight(req.player.id);
if(fight) {
};
const location = await getMonsterLocation(fight.ref_id);
- res.send(renderPlayerBar(req.player, equippedItems) + renderFightPreRound(data, true, location, req.player.city_id));
+ res.send(renderPlayerBar(req.player) + renderFightPreRound(data, true, location, req.player.city_id));
}
else {
const [city, locations, paths] = await Promise.all([
getAllPaths(req.player.city_id)
]);
- res.send(renderPlayerBar(req.player, equippedItems) + await renderMap({city, locations, paths}, req.player.city_id));
+ res.send(renderPlayerBar(req.player) + await renderMap({city, locations, paths}, req.player.city_id));
}
import {FightRound} from '../shared/fight';
import { clearFight, loadMonster, getMonsterList, saveFightState, loadMonsterFromFight } from './monster';
-import { Player, expToLevel, maxHp } from '../shared/player';
+import { Player, expToLevel, maxHp, totalDefence, maxVigor } from '../shared/player';
import { clearTravelPlan } from './map';
import { updatePlayer } from './player';
import { getEquippedItems, updateAp, deleteInventoryItem } from './inventory';
// 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;
- }
- });
-
const boost = {
+ defence: totalDefence(equippedItems, player),
strength: 0,
constitution: 0,
dexterity: 0,
}
});
- // 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.
+ // @TODO implement flee based on dex + vigor
if(data.action === 'flee') {
roundData.roundDetails.push(`You managed to escape from the ${monster.name}!`)
roundData.winner = 'monster';
}
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 primaryStat = attackType === 'physical' ? player.strength : player.intelligence;
+ const boostStat = attackType === 'physical' ? boost.strength : boost.intelligence;
const playerDamage = Math.floor(((primaryStat + boostStat) * 1.3) + boost.damage);
const skillsUsed: Record<SkillID | any, number> = {};
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;
+ let monsterTakesDamage = playerFinalDamage - monster.defence;
+ if(monsterTakesDamage < 0) {
+ monsterTakesDamage = 0;
}
+ roundData.roundDetails.push(`You dealt ${monsterTakesDamage} damage to the ${monster.name}!`);
- 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;
- }
+ monster.hp -= monsterTakesDamage;
if(monster.hp <= 0) {
roundData.monster.hp = 0;
roundData.roundDetails.push(`You gained ${statPointsGained} stat points!`);
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') {
});
}
+ player.vigor -= 1;
+ if(player.vigor < 0) {
+ player.vigor = 0;
+ }
+
+ await updateAp(player.id, 1, equippedItems.map(i => i.item_id));
await clearFight(player.id);
await updatePlayer(player);
return { roundData, monsters: potentialMonsters, player };
}
- roundData.roundDetails.push(`The ${monster.name} targeted your ${target}!`);
- const item = equipment.get(target);
- if(item) {
- // 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);
- }
-
- }
- else {
- roundData.roundDetails.push(`The ${monster.name} hit you for ${monster.strength} damage`);
- player.hp -= monster.strength;
+ let monsterDamage = (monster.strength*2) - boost.defence;
+ if(monsterDamage < 0) {
+ monsterDamage = 0;
}
+ roundData.roundDetails.push(`The ${monster.name} hit you for ${monsterDamage} damage`);
+ player.hp -= monsterDamage;
+
if(playerFinalHeal > 0) {
player.hp += playerFinalHeal;
if(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;
+ player.vigor = 0;
roundData.winner = 'monster';
roundData.roundDetails.push(`You were killed by the ${monster.name}`);
await clearFight(player.id);
+ await updateAp(player.id, 5, equippedItems.map(i => i.item_id));
await updatePlayer(player);
await clearTravelPlan(player.id);
intelligence: item.boosts.intelligence,
damage: item.boosts.damage,
damage_mitigation: item.boosts.damage_mitigation,
+ defence: item.boosts.defence
},
maxAp: item.maxAp,
currentAp: item.currentAp,
});
}
-export async function updateAp(player_id: string, item_id: string, currentAp: number, maxAp: number) {
- return db('inventory').update({
- currentAp,
- maxAp
- }).where({
- player_id,
- item_id
- })
+export async function updateAp(player_id: string, apDamage: number, itemIds: string[]) {
+ return db('inventory').where({
+ player_id
+ }).whereIn('item_id', itemIds).update({
+ 'currentAp': db.raw(`GREATEST("currentAp" - ${apDamage}, 0)`)
+ });
}
export async function deleteInventoryItem(player_id: string, item_id: string) {
-import { Request, Response, Router } from "express";
-import { maxHp, Player } from "../../../shared/player";
+import { Response, Router } from "express";
+import { maxHp, maxVigor } from "../../../shared/player";
import { authEndpoint, AuthRequest } from '../../auth';
import { logger } from "../../lib/logger";
-import { loadPlayer, updatePlayer } from "../../player";
+import { updatePlayer } from "../../player";
import { getCityDetails, getService } from '../../map';
import { sample } from 'lodash';
import { City, Location } from "../../../shared/map";
import { renderPlayerBar } from "../../views/player-bar";
-import { getEquippedItems } from "../../inventory";
-import { EquippedItemDetails } from "../../../shared/equipped";
export const router = Router();
text.push(`<p>"${getText('intro', service, city)}"</p>`);
- if(req.player.hp === maxHp(req.player.constitution, req.player.level)) {
- text.push(`<p>You're already at full health?</p>`);
+ if(req.player.hp === maxHp(req.player.constitution, req.player.level) && req.player.vigor === maxVigor(req.player.constitution, req.player.level)) {
+ text.push(`<p>You're already in peak condition!</p>`);
}
else {
if(req.player.gold <= (healCost * 2)) {
const text: string[] = [];
const cost = req.player.gold <= (healCost * 2) ? 0 : healCost;
- let inventory: EquippedItemDetails[];
if(req.player.gold < cost) {
text.push(`<p>${getText('insufficient_money', service, city)}</p>`)
}
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);
- inventory = await getEquippedItems(req.player.id);
text.push(`<p>${getText('heal_successful', service, city)}</p>`);
text.push('<p><button hx-get="/player/explore" hx-target="#explore">Back to Town</button></p>');
${text.join("\n")}
</div>
</div>
-${inventory ? renderPlayerBar(req.player, inventory) : ''}
+${renderPlayerBar(req.player)}
`);
});
import * as Alert from "../views/alert";
import { changeProfession } from "../player";
import { renderPlayerBar } from "../views/player-bar";
-import { getEquippedItems } from "../inventory";
function p(str: string) {
return `<p>${str}</p>`;
}
if(update) {
- const equipped = await getEquippedItems(req.player.id);
req.player.level = update.level;
req.player.exp = update.exp;
- res.send(renderPlayerBar(req.player, equipped) + `<div id="recruiter-target" class="service-in-town" hx-swap-oob="true">Congrats! You are now a ${req.player.profession}</div>`);
+ res.send(renderPlayerBar(req.player) + `<div id="recruiter-target" class="service-in-town" hx-swap-oob="true">Congrats! You are now a ${req.player.profession}</div>`);
}
});
level: monster.level,
gold: monster.gold,
hp: monster.hp,
- helmAp: monster.helmAp,
- chestAp: monster.chestAp,
- legsAp: monster.legsAp,
- armsAp: monster.armsAp,
+ defence: monster.defence,
maxHp: monster.maxHp,
ref_id: monster.id,
fight_trigger: fightTrigger
id: player.id
}).update({
hp: player.hp,
+ vigor: player.vigor,
strength: player.strength,
constitution: player.constitution,
dexterity: player.dexterity,
export interface ProgressBarOptions {
startingColor: string;
- endingColor: string;
+ endingColor?: string;
+ title?: string
+ displayPercent?: boolean;
}
export function ProgressBar(current: number, max: number, id: string, opts: ProgressBarOptions) {
+ const endingColor = opts.endingColor ?? opts.startingColor;
+ const title = opts.title ?? '';
+ const display = [`${current}/${max}`];
let percent = 0;
+
if(max > 0) {
percent = Math.floor((current / max) * 100);
}
- return `<div class="progress-bar" id="${id}" style="background: linear-gradient(to right, ${opts.startingColor}, ${opts.endingColor} ${percent}%, transparent ${percent}%, transparent)"
-title="${percent}% - ${current}/${max}">${current}/${max} - ${percent}%</div>`;
+ if(opts.displayPercent) {
+ display.push(`${percent}%`);
+ }
+
+ return `<div class="progress-bar" id="${id}" style="background: linear-gradient(to right, ${opts.startingColor}, ${endingColor} ${percent}%, transparent ${percent}%, transparent)"
+title="${title} ${display.join(" - ")}">${title} ${display.join(" - ")}</div>`;
}
${renderRequirement('PRF', item.profession)}
</div>
<div class="stat-mods">
+ ${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) : ''}
-import { EquippedItemDetails } from "shared/equipped";
-import { EquipmentSlot } from "shared/inventory";
-import { expToLevel, maxHp, Player } from "../../shared/player";
+import { expToLevel, maxHp, maxVigor, Player } from "../../shared/player";
+import { ProgressBar } from "./components/progress-bar";
function displayLoginSignupForm(): string {
return `
}
-function generateProgressBar(current: number, max: number, opts: ProgressBarOptions): string {
- let percent = 0;
- if(max > 0) {
- percent = Math.floor((current / max) * 100);
- }
- const display = `${percent}% - `;
- return `<div class="progress-bar" style="background: linear-gradient(to right, ${opts.startingColor}, ${opts.endingColor} ${percent}%, transparent ${percent}%, transparent)" title="${display}${current}/${max}">${display}${current}/${max}</div>`;
-}
-
-function calcAp(inventoryItem: EquippedItemDetails[]): string {
- const ap: Record<any | EquipmentSlot, {currentAp: number, maxAp: number}> = {};
- inventoryItem.forEach(item => {
- if(item.is_equipped && item.type === 'ARMOUR') {
- ap[item.equipment_slot] = {
- currentAp: item.currentAp,
- maxAp: item.maxAp
- };
- }
- });
-
- return `
- <div>
- <img src="/assets/img/helm.png" class="icon">
- ${generateProgressBar(ap.HEAD?.currentAp || 0, ap.HEAD?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})}
- </div>
- <div>
- <img src="/assets/img/arms.png" class="icon">
- ${generateProgressBar(ap.ARMS?.currentAp || 0, ap.ARMS?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})}
- </div>
- <div>
- <img src="/assets/img/chest.png" class="icon">
- ${generateProgressBar(ap.CHEST?.currentAp || 0, ap.CHEST?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})}
- </div>
- <div>
- <img src="/assets/img/legs.png" class="icon">
- ${generateProgressBar(ap.LEGS?.currentAp || 0, ap.LEGS?.maxAp || 0, { startingColor: '#5ebb5e', endingColor: '#7be67b'})}
- </div>
-`;
-}
-
-interface ProgressBarOptions {
- startingColor: string;
- endingColor: string;
-}
-
-function progressBar(current: number, max: number, id: string, opts: ProgressBarOptions) {
- let percent = 0;
- if(max > 0) {
- percent = Math.floor((current / max) * 100);
- }
-
- return `<div class="progress-bar" id="${id}" style="background: linear-gradient(to right, ${opts.startingColor}, ${opts.endingColor} ${percent}%, transparent ${percent}%, transparent)"
-title="${percent}% - ${current}/${max}">${current}/${max} - ${percent}%</div>`;
-}
-
-export function renderPlayerBar(player: Player, inventory: EquippedItemDetails[]): string {
+export function renderPlayerBar(player: Player): string {
return `
<div id="stat-bars" hx-swap-oob="true">
<div id="player-section">
<div id="username">${player.username}, level ${player.level} ${player.profession}</div>
<div class="gold">${player.gold.toLocaleString()}</div>
</div>
- <div id="ap-bar">${calcAp(inventory)}</div>
- ${progressBar(player.hp, maxHp(player.constitution, player.level), 'hp-bar', { endingColor: '#ff7070', startingColor: '#d62f2f' })}
- ${progressBar(player.exp, expToLevel(player.level + 1), 'exp-bar', { endingColor: '#5997f9', startingColor: '#1d64d4'})}
+ ${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() : ''}
`;
-import { Player, StatDef, StatDisplay } from "../../shared/player";
+import { EquippedItemDetails } from "../../shared/equipped";
+import { expToLevel, maxHp, maxVigor, 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>`;
}
-export function renderProfilePage(player: Player): string {
+
+export function renderProfilePage(player: Player, equipment: EquippedItemDetails[]): string {
+
let statBreakdown = '';
StatDef.forEach(stat => {
statBreakdown += `<tr>
- <th>${stat.display}</th>
+ <th title="${stat.description}" tabindex="0">${stat.display}</th>
<td class="${stat.id}">
- ${player[stat.id]}
+ ${player[stat.id].toLocaleString()}
${player.stat_points ? statPointIncreaser(stat) : ''}
</td>
</tr>`;
});
const html = `<div id="extra-inventory-info">
- <table id="stat-breakdown">
+ <table class="stat-breakdown">
+ <tr>
+ <th title="The total amount of damage you can take before you pass out" tabindex="0">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">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">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">Defence</th>
+ <td>${totalDefence(equipment, player, false).toLocaleString()} (${totalDefence(equipment, player).toLocaleString()})</td>
+ </tr>
+ <tr>
+ <th title="You can use these to increase the base stats below" tabindex="0">Stat Points</th>
+ <td class="stat_points">${player.stat_points}</td>
+ </tr>
${statBreakdown}
- <tr><th>Stat Points</th><td class="stat_points">${player.stat_points}</td></tr>
</table>
- </div>`;
+ </div>
+ <div id="announcements">
+<p>Hi, thanks for checking out this VERY early build of Rising Legends.</p>
+<p>If you have any questions or run into any bugs, feel free to drop an email on our mailing list: <a href="mailto:~xangelo/rising-legends-discuss@lists.sr.ht">~xangelo/rising-legends-discuss@lists.sr.ht</a>
+ </div>
+`;
return html;
}
const percent = skill.exp / definition.expToLevel(skill.level + 1);
return `
<tr>
- <td class="skill-level">${skill.level.toLocaleString()}</td>
- <td class="skill-description" title="Total Exp: ${skill.exp.toLocaleString()}/${definition.expToLevel(skill.level + 1).toLocaleString()}">
- <span class="skill-exp">${(percent * 100).toPrecision(2)}% to next level</span>
- <b>${definition.display}</b>
- <p>${definition.description}</p>
- </td>
+ <td class="skill-level">${skill.level.toLocaleString()}</td>
+ <td class="skill-details">
+ <table>
+ <tr>
+ <th class="skill-title" title="Total Exp: ${skill.exp.toLocaleString()}/${definition.expToLevel(skill.level + 1).toLocaleString()}">
+ ${definition.display}
+ </th>
+ <td class="skill-exp">
+ ${(percent * 100).toPrecision(2)}% to next level
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2" class="skill-description">${definition.description}</td>
+ </tr>
+ </table>
+ </td>
</tr>
`;
}).join("\n")}
${renderRequirement('PRF', item.profession)}
</div>
<div class="stat-mods">
+ ${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) : ''}
intelligence: number;
damage: number;
damage_mitigation: number;
+ defence: number;
}
currentAp: number;
maxAp: number;
gold: number;
exp: number;
hp: number;
- helmAp: number;
- chestAp: number;
- armsAp: number;
- legsAp: number;
+ defence: number;
maxHp: number;
location_id: number;
faction_id: number;
import { Profession } from './profession';
import { Stat } from './stats';
import { SkillDefinition, Skill } from './skills';
+import { EquippedItemDetails } from './equipped';
export type Player = {
id: string,
hp: number;
city_id: number;
stat_points: number;
+ vigor: number;
}
export type PlayerWithSkills = Player & {
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;
}
}
+export function totalDefence(equippedItems: EquippedItemDetails[], player: Player, accountForVigor: boolean = true): number {
+ const vigorPercent = player.vigor / maxVigor(player.constitution, player.level);
+
+ 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;
+ }
+}
+
export type StatDisplay = {
id: Stat,
display: string;
abbrv: string;
+ description: string;
}
export const StatDef: Map<Stat, StatDisplay> = new Map<Stat, StatDisplay>();
StatDef.set(Stat.strength, {
id: Stat.strength,
display: 'Strength',
- abbrv: 'STR'
+ abbrv: 'STR',
+ description: 'Affects your melee damage'
});
StatDef.set(Stat.constitution, {
id: Stat.constitution,
display: 'Constitution',
- abbrv: 'CON'
+ abbrv: 'CON',
+ description: 'Affects your max HP and Vigor'
});
StatDef.set(Stat.dexterity, {
id: Stat.dexterity,
display: 'Dexterity',
- abbrv: 'DEX'
+ abbrv: 'DEX',
+ description: 'Affects you ability to dodge attacks double-hit'
});
StatDef.set(Stat.intelligence, {
id: Stat.intelligence,
display: 'Intelligence',
- abbrv: 'INT'
+ abbrv: 'INT',
+ description: 'Affects your magical damage'
});