V1.11.1: HP and money are the DM's to change
- A player can no longer heal or hurt their own character: the Damage, Heal and Temp HP controls on the sheet are the DM's, and the server refuses the change from anyone else. Players still roll their own death saves. - A player can no longer add money to their own purse or write it off: recording a gain or a spend is the DM's, in any coin or in cash, on the sheet and on the server. - Players still exchange gold, silver and copper on the sheet, give money to another character, and buy from or change money with an NPC. - All packages at version 1.11.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"main": "src/index.js",
|
||||
|
||||
@@ -311,11 +311,10 @@ describe('copy character to another campaign', () => {
|
||||
await request(app).post(`/characters/${characterId}/weapons`).set(source.dmAuth).send({ weaponId: longsword.body.weapon.id, level: 2 });
|
||||
await request(app).post(`/characters/${characterId}/spells`).set(source.dmAuth).send({ spellId: spell.body.spell.id });
|
||||
await request(app).post(`/characters/${characterId}/inventory`).set(source.dmAuth).send({ itemId: rope.body.item.id, quantity: 2 });
|
||||
await request(app).post(`/characters/${characterId}/gold`).set(playerAuth).send({ amount: 42, reason: 'Savings' });
|
||||
await request(app).post(`/characters/${characterId}/gold`).set(source.dmAuth).send({ amount: 42, reason: 'Savings' });
|
||||
await request(app).patch(`/characters/${characterId}`).set(source.dmAuth).send({ level: 3 });
|
||||
const original = (
|
||||
await request(app).patch(`/characters/${characterId}`).set(playerAuth).send({ currentHp: 1, privateNotes: 'secret' })
|
||||
).body.character;
|
||||
await request(app).patch(`/characters/${characterId}`).set(source.dmAuth).send({ currentHp: 1 });
|
||||
const original = (await request(app).patch(`/characters/${characterId}`).set(playerAuth).send({ privateNotes: 'secret' })).body.character;
|
||||
|
||||
const copyRes = await request(app).post(`/characters/${characterId}/copy`).set(playerAuth).send({ targetCampaignId: target.campaign.id });
|
||||
expect(copyRes.status).toBe(201);
|
||||
@@ -402,6 +401,9 @@ describe('who can change what on a character', () => {
|
||||
const location = await request(app).post(`/campaigns/${campaign.id}/locations`).set(dmAuth).send({ name: 'Keep' });
|
||||
|
||||
const dmOnly = [
|
||||
// A player cannot heal or hurt their own character.
|
||||
{ currentHp: 1 },
|
||||
{ tempHp: 2 },
|
||||
{ level: 20 },
|
||||
{ xp: 9000 },
|
||||
{ currentLocationId: location.body.location.id },
|
||||
@@ -419,7 +421,8 @@ describe('who can change what on a character', () => {
|
||||
expect((await request(app).patch(`/characters/${characterId}`).set(dmAuth).send({ gold: 999 })).status).toBe(400);
|
||||
|
||||
// The player can still edit the rest of their own sheet.
|
||||
expect((await request(app).patch(`/characters/${characterId}`).set(playerAuth).send({ tempHp: 2 })).status).toBe(200);
|
||||
expect((await request(app).patch(`/characters/${characterId}`).set(playerAuth).send({ privateNotes: 'Mine.' })).status).toBe(200);
|
||||
expect((await request(app).patch(`/characters/${characterId}`).set(dmAuth).send({ currentHp: 1, tempHp: 2 })).status).toBe(200);
|
||||
|
||||
const dmRes = await request(app)
|
||||
.patch(`/characters/${characterId}`)
|
||||
@@ -581,12 +584,12 @@ describe('hit points', () => {
|
||||
const patch = (auth, body) => request(app).patch(`/characters/${characterId}`).set(auth).send(body);
|
||||
|
||||
const level5 = (await patch(dmAuth, { level: 5 })).body.character;
|
||||
const full = (await patch(playerAuth, { currentHp: 9999 })).body.character;
|
||||
const full = (await patch(dmAuth, { currentHp: 9999 })).body.character;
|
||||
expect(full.currentHp).toBe(level5.sheet.hp.max);
|
||||
|
||||
expect((await patch(playerAuth, { currentHp: -1 })).body.character.currentHp).toBe(0);
|
||||
expect((await patch(dmAuth, { currentHp: -1 })).body.character.currentHp).toBe(0);
|
||||
|
||||
await patch(playerAuth, { currentHp: 9999 });
|
||||
await patch(dmAuth, { currentHp: 9999 });
|
||||
const level1 = (await patch(dmAuth, { level: 1 })).body.character;
|
||||
expect(level1.sheet.hp.max).toBeLessThan(level5.sheet.hp.max);
|
||||
expect(level1.currentHp).toBe(level1.sheet.hp.max);
|
||||
@@ -609,7 +612,7 @@ describe('hit points', () => {
|
||||
expect(await sheetHp()).toBe(maxHp - 3);
|
||||
|
||||
// Healing on the sheet shows in the tracker.
|
||||
await request(app).patch(`/characters/${characterId}`).set(playerAuth).send({ currentHp: maxHp - 1 });
|
||||
await request(app).patch(`/characters/${characterId}`).set(dmAuth).send({ currentHp: maxHp - 1 });
|
||||
expect((await combatant()).currentHp).toBe(maxHp - 1);
|
||||
|
||||
// The tracker is clamped like the sheet, and can't give a character a different max.
|
||||
@@ -645,14 +648,19 @@ describe('money tracker', () => {
|
||||
expect(reward.status).toBe(201);
|
||||
expect(reward.body.gold).toBe(65);
|
||||
|
||||
const spend = await gold(playerAuth, { amount: -20, reason: 'New boots' });
|
||||
const spend = await gold(dmAuth, { amount: -20, reason: 'New boots' });
|
||||
expect(spend.status).toBe(201);
|
||||
expect(spend.body.gold).toBe(45);
|
||||
|
||||
const overspend = await gold(playerAuth, { amount: -46 });
|
||||
// A player can neither add to their own money nor write it off, in any coin.
|
||||
expect((await gold(playerAuth, { amount: 5, reason: 'Found it' })).status).toBe(403);
|
||||
expect((await gold(playerAuth, { amount: 5, currency: 'SILVER' })).status).toBe(403);
|
||||
expect((await gold(playerAuth, { amount: -5, reason: 'Lost it' })).status).toBe(403);
|
||||
|
||||
const overspend = await gold(dmAuth, { amount: -46 });
|
||||
expect(overspend.status).toBe(400);
|
||||
expect((await gold(playerAuth, { amount: 0 })).status).toBe(400);
|
||||
expect((await gold(playerAuth, { amount: 1.5 })).status).toBe(400);
|
||||
expect((await gold(dmAuth, { amount: 0 })).status).toBe(400);
|
||||
expect((await gold(dmAuth, { amount: 1.5 })).status).toBe(400);
|
||||
|
||||
// Another player can neither write to the ledger nor read it.
|
||||
const stranger = await request(app)
|
||||
@@ -669,7 +677,7 @@ describe('money tracker', () => {
|
||||
expect(ledger.status).toBe(200);
|
||||
expect(ledger.body.gold).toBe(45);
|
||||
expect(ledger.body.transactions.map((t) => [t.amount, t.balanceAfter, t.reason, t.createdBy.displayName])).toEqual([
|
||||
[-20, 45, 'New boots', 'Player'],
|
||||
[-20, 45, 'New boots', 'DM'],
|
||||
[50, 65, 'Bounty on the wolf pack', 'DM'],
|
||||
[15, 15, 'Starting gold', 'Player'],
|
||||
]);
|
||||
@@ -679,7 +687,8 @@ describe('money tracker', () => {
|
||||
const { dmAuth, playerAuth, campaign } = await setupCampaign('cash');
|
||||
const create = (name) => request(app).post(`/campaigns/${campaign.id}/characters`).set(playerAuth).send({ name });
|
||||
const characterId = (await create('Drifter')).body.character.id;
|
||||
const money = (body) => request(app).post(`/characters/${characterId}/gold`).set(playerAuth).send(body);
|
||||
// Only the DM records a gain or a spend.
|
||||
const money = (body) => request(app).post(`/characters/${characterId}/gold`).set(dmAuth).send(body);
|
||||
|
||||
await money({ amount: 5 });
|
||||
const paid = await money({ amount: 40, currency: 'CASH', reason: 'Odd job' });
|
||||
@@ -2079,17 +2088,17 @@ describe('temporary hit points', () => {
|
||||
expect((await sheet()).currentHp).toBe(max);
|
||||
|
||||
// On the sheet: 5 temp, 3 damage — all of it soaked.
|
||||
await request(app).patch(ch).set(playerAuth).send({ tempHp: 5 });
|
||||
let now = (await request(app).patch(ch).set(playerAuth).send({ currentHp: max - 3 })).body.character;
|
||||
await request(app).patch(ch).set(dmAuth).send({ tempHp: 5 });
|
||||
let now = (await request(app).patch(ch).set(dmAuth).send({ currentHp: max - 3 })).body.character;
|
||||
expect([now.currentHp, now.tempHp]).toEqual([max, 2]);
|
||||
|
||||
// More damage than is left: the rest reaches real HP.
|
||||
now = (await request(app).patch(ch).set(playerAuth).send({ currentHp: max - 3 })).body.character;
|
||||
now = (await request(app).patch(ch).set(dmAuth).send({ currentHp: max - 3 })).body.character;
|
||||
expect([now.currentHp, now.tempHp]).toEqual([max - 1, 0]);
|
||||
|
||||
// Healing doesn't touch temp HP, and setting temp HP doesn't touch HP.
|
||||
await request(app).patch(ch).set(playerAuth).send({ tempHp: 4 });
|
||||
now = (await request(app).patch(ch).set(playerAuth).send({ currentHp: max })).body.character;
|
||||
await request(app).patch(ch).set(dmAuth).send({ tempHp: 4 });
|
||||
now = (await request(app).patch(ch).set(dmAuth).send({ currentHp: max })).body.character;
|
||||
expect([now.currentHp, now.tempHp]).toEqual([max, 4]);
|
||||
|
||||
// In an encounter the damage lands on the same sheet, the same way.
|
||||
@@ -2101,7 +2110,7 @@ describe('temporary hit points', () => {
|
||||
expect([now.currentHp, now.tempHp]).toEqual([max - 2, 0]);
|
||||
|
||||
// A blow far past zero still only takes what there is.
|
||||
await request(app).patch(ch).set(playerAuth).send({ tempHp: 2 });
|
||||
await request(app).patch(ch).set(dmAuth).send({ tempHp: 2 });
|
||||
await request(app).patch(`${e}/participants/${row.id}`).set(dmAuth).send({ currentHp: -500 });
|
||||
now = await sheet();
|
||||
expect([now.currentHp, now.tempHp]).toEqual([0, 0]);
|
||||
@@ -2121,7 +2130,7 @@ describe('death saves', () => {
|
||||
|
||||
// No saves while standing, and a save is a d20.
|
||||
expect((await save(playerAuth, { action: 'roll', roll: 12 })).status).toBe(400);
|
||||
await hp(playerAuth, 0);
|
||||
await hp(dmAuth, 0);
|
||||
expect((await sheet()).deathSaves).toEqual({ state: 'DYING', successes: 0, failures: 0 });
|
||||
expect((await save(playerAuth, { action: 'roll', roll: 21 })).status).toBe(400);
|
||||
|
||||
@@ -2131,10 +2140,10 @@ describe('death saves', () => {
|
||||
|
||||
// Being hit while down is a failed save; healing clears the lot.
|
||||
expect((await hp(dmAuth, -1)).body.character.deathSaves).toEqual({ state: 'DYING', successes: 1, failures: 2 });
|
||||
expect((await hp(playerAuth, 3)).body.character.deathSaves).toEqual({ state: 'UP', successes: 0, failures: 0 });
|
||||
expect((await hp(dmAuth, 3)).body.character.deathSaves).toEqual({ state: 'UP', successes: 0, failures: 0 });
|
||||
|
||||
// Three successes: stable, at 0 HP, with nothing left to roll.
|
||||
await hp(playerAuth, 0);
|
||||
await hp(dmAuth, 0);
|
||||
for (const roll of [15, 11, 19]) await save(playerAuth, { action: 'roll', roll });
|
||||
let now = await sheet();
|
||||
expect([now.currentHp, now.deathSaves]).toEqual([0, { state: 'STABLE', successes: 0, failures: 0 }]);
|
||||
@@ -2146,7 +2155,7 @@ describe('death saves', () => {
|
||||
expect([now.currentHp, now.deathSaves]).toEqual([1, { state: 'UP', successes: 0, failures: 0 }]);
|
||||
|
||||
// Someone else can steady them without a roll.
|
||||
await hp(playerAuth, 0);
|
||||
await hp(dmAuth, 0);
|
||||
expect((await save(playerAuth, { action: 'stabilize' })).body.character.deathSaves.state).toBe('STABLE');
|
||||
|
||||
// The tallies are the DM's to set by hand; a 1 on top of two failures kills.
|
||||
@@ -2155,7 +2164,7 @@ describe('death saves', () => {
|
||||
expect((await save(playerAuth, { action: 'roll', roll: 1 })).body.character.deathSaves.state).toBe('DEAD');
|
||||
|
||||
// The dead can't be healed or roll; only the DM brings them back.
|
||||
expect((await hp(playerAuth, max)).status).toBe(400);
|
||||
expect((await hp(dmAuth, max)).status).toBe(400);
|
||||
expect((await save(playerAuth, { action: 'roll', roll: 20 })).status).toBe(400);
|
||||
expect((await save(playerAuth, { action: 'revive' })).status).toBe(403);
|
||||
now = (await save(dmAuth, { action: 'revive' })).body.character;
|
||||
@@ -2173,7 +2182,7 @@ describe('death saves', () => {
|
||||
const combatant = async () => (await request(app).get(e).set(playerAuth)).body.encounter.participants[0];
|
||||
|
||||
// Temp HP that soaks the whole blow keeps them on their feet.
|
||||
await request(app).patch(ch).set(playerAuth).send({ tempHp: 2 });
|
||||
await request(app).patch(ch).set(dmAuth).send({ tempHp: 2 });
|
||||
await strike(max - 2);
|
||||
expect((await combatant()).characterCombatInfo.deathSaves.state).toBe('UP');
|
||||
|
||||
@@ -2280,7 +2289,8 @@ describe('silver and copper', () => {
|
||||
const { gold, silver, copper } = (await request(app).get(`/characters/${id}`).set(auth)).body.character;
|
||||
return [gold, silver, copper];
|
||||
};
|
||||
const money = (body) => request(app).post(`${ch}/gold`).set(playerAuth).send(body);
|
||||
// Only the DM records a gain or a spend.
|
||||
const money = (body) => request(app).post(`${ch}/gold`).set(dmAuth).send(body);
|
||||
const exchange = (body) => request(app).post(`${ch}/gold/exchange`).set(playerAuth).send(body);
|
||||
|
||||
expect(await purse()).toEqual([5, 0, 0]);
|
||||
@@ -2359,7 +2369,8 @@ describe('paying with change', () => {
|
||||
const { gold, silver, copper } = (await request(app).get(`/characters/${id}`).set(auth)).body.character;
|
||||
return [gold, silver, copper];
|
||||
};
|
||||
const money = (body) => request(app).post(`${ch}/gold`).set(playerAuth).send(body);
|
||||
// Only the DM records a gain or a spend.
|
||||
const money = (body) => request(app).post(`${ch}/gold`).set(dmAuth).send(body);
|
||||
const newest = async (count) =>
|
||||
(await request(app).get(`${ch}/gold`).set(playerAuth)).body.transactions.slice(0, count).map((t) => [t.currency, t.amount, t.balanceAfter, t.reason]);
|
||||
|
||||
|
||||
@@ -164,11 +164,12 @@ const patchCharacterSchema = z.object({
|
||||
|
||||
// Progression and placement are the DM's call, and so is rewriting what the
|
||||
// builder composed (special abilities, background, appearance): a player
|
||||
// picks those from lists and cannot type their own. The owning player can
|
||||
// picks those from lists and cannot type their own. So are HP and temp HP:
|
||||
// a player cannot heal or hurt their own character. The owning player can
|
||||
// edit the rest of their own sheet. Gold is not patchable at all — it only
|
||||
// moves through the ledger (POST /:id/gold). The armor bonus is the DM's
|
||||
// adjustment on top of what equipped gear gives, so it is the DM's too.
|
||||
const DM_ONLY_PATCH_FIELDS = ['level', 'xp', 'currentLocationId', 'speedOverride', 'armorBonus', 'specialAbilities', 'background', 'appearance'];
|
||||
const DM_ONLY_PATCH_FIELDS = ['currentHp', 'tempHp', 'level', 'xp', 'currentLocationId', 'speedOverride', 'armorBonus', 'specialAbilities', 'background', 'appearance'];
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
@@ -183,7 +184,7 @@ router.patch('/:id', requireCharacterAccess({ write: true }), async (req, res, n
|
||||
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
|
||||
|
||||
if (!req.isDM && DM_ONLY_PATCH_FIELDS.some((field) => parsed.data[field] !== undefined)) {
|
||||
return res.status(403).json({ error: 'Only the DM can change level, XP, speed, armor bonus, location, special abilities, background or appearance.' });
|
||||
return res.status(403).json({ error: 'Only the DM can change HP, temp HP, level, XP, speed, armor bonus, location, special abilities, background or appearance.' });
|
||||
}
|
||||
|
||||
// Damage is soaked by temp HP before it reaches real HP — unless this
|
||||
@@ -1006,9 +1007,10 @@ router.get('/:id/gold', requireCharacterAccess(), async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// amount is signed: positive = gained, negative = spent. The owning player
|
||||
// and the DM can both record entries; each one is stamped with who made it.
|
||||
// A spend is paid from the whole purse: if there aren't enough of the coin
|
||||
// amount is signed: positive = gained, negative = spent. Only the DM records
|
||||
// entries here — a player cannot add money to their own purse or write it
|
||||
// off; theirs moves by giving, buying and exchanging. Each entry is stamped
|
||||
// with who made it. A spend is paid from the whole purse: if there aren't enough of the coin
|
||||
// named, bigger coins are broken (or smaller ones counted out) to cover it.
|
||||
// Cash is only ever paid in cash.
|
||||
router.post('/:id/gold', requireCharacterAccess({ write: true }), async (req, res, next) => {
|
||||
@@ -1022,6 +1024,7 @@ router.post('/:id/gold', requireCharacterAccess({ write: true }), async (req, re
|
||||
.safeParse(req.body);
|
||||
if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() });
|
||||
const { amount, currency, reason } = parsed.data;
|
||||
if (!req.isDM) return res.status(403).json({ error: 'Only the DM can record a gain or a spend.' });
|
||||
|
||||
let transactions;
|
||||
try {
|
||||
|
||||
+13
-8
@@ -315,14 +315,15 @@ Example: a Fighter (d10) with CON 14 (+2) has 12 at level 1 and 28 at level 3.
|
||||
**Current HP** is a stored number, separate from max:
|
||||
|
||||
- Set to max when the build is finalized, and when a character is copied.
|
||||
- Changed with the Damage and Heal buttons on the sheet, or by the DM in an
|
||||
encounter (section 15). Both change the same number.
|
||||
- Changed by the **DM only**: with the Damage and Heal buttons on the sheet,
|
||||
or in an encounter (section 15). Both change the same number. A player sees
|
||||
their HP but has no buttons to change it, and the server refuses the change.
|
||||
- Always kept between **0 and max**: healing stops at max and damage stops at 0.
|
||||
- A level-up raises max but leaves current HP where it was; the DM clicks
|
||||
"Recompute Max HP" to set current HP to the new max (a full heal). If a
|
||||
level change lowers max below current HP, current HP drops to the new max.
|
||||
|
||||
**Temp HP** is a tracked number shown beside HP. Damage is not taken from it
|
||||
**Temp HP** is a tracked number shown beside HP, set by the DM. Damage is not taken from it
|
||||
automatically.
|
||||
|
||||
### At 0 HP: death saves
|
||||
@@ -585,8 +586,11 @@ behind it.
|
||||
- The sheet's Money card shows the three balances, what the purse is worth in
|
||||
gold altogether, and every change: the amount and coin, what it was for, who
|
||||
recorded it, when, and that coin's balance afterwards.
|
||||
- The **owning player and the DM** can both record a gain or a spend, choosing
|
||||
the coin. Other players cannot see the purse or the log.
|
||||
- Only the **DM** records a gain or a spend, choosing the coin: a player
|
||||
cannot add money to their own purse or write it off. A player's money
|
||||
moves by exchanging coins, giving to another character, and trading with
|
||||
an NPC; it arrives from the DM, an encounter reward, a gift or an NPC.
|
||||
Other players cannot see the purse or the log.
|
||||
- **Spending makes change for you.** A spend (or a gift to another character)
|
||||
is paid from the whole purse. Coins of the kind named go first; if there
|
||||
aren't enough, the nearest bigger coin is broken and the change comes back
|
||||
@@ -599,7 +603,7 @@ behind it.
|
||||
coins: silver into gold goes in tens, copper into silver in tens, copper
|
||||
into gold in hundreds.
|
||||
- The log is the only way a balance changes. Entries cannot be edited or
|
||||
deleted; to correct a mistake, record the opposite entry.
|
||||
deleted; to correct a mistake, the DM records the opposite entry.
|
||||
- Starting gold (a campaign setting) is gold only. Encounter rewards and gifts
|
||||
between characters can be in any coin, or in cash.
|
||||
- **Cash ($)** is a fourth balance for crossover worlds, counted in whole
|
||||
@@ -769,7 +773,7 @@ Another Campaign" card on its sheet.
|
||||
| View the sheet and quick stats | Yes | Yes | Yes |
|
||||
| View private notes | Yes | No | No |
|
||||
| Run the builder | Yes | Yes | No |
|
||||
| Change HP, temp HP | Yes | Yes | No |
|
||||
| Change HP, temp HP | No | Yes | No |
|
||||
| Set the extra AC | No | Yes | No |
|
||||
| Open the character panel with the full inventory | Yes | Yes | No (numbers and weapons only) |
|
||||
| Enter a death save, or stabilise | Yes | Yes | No |
|
||||
@@ -778,7 +782,8 @@ Another Campaign" card on its sheet.
|
||||
| Tick Equipped / Prepared | Yes | Yes | No |
|
||||
| Give, remove or re-count weapons, spells and items | No | Yes | No |
|
||||
| Level a weapon or item up or down | No | Yes | No |
|
||||
| Record a gold gain or spend | Yes | Yes | No |
|
||||
| Record a money gain or spend | No | Yes | No |
|
||||
| Exchange coins (gold, silver, copper) | Yes | Yes | No |
|
||||
| Give gold, weapons, items or spells to another character | Yes | Yes | No |
|
||||
| Change gold and cash with an NPC, or buy what is for sale, in a quest in progress | Yes | Yes | No |
|
||||
| View the money log, gold and inventory | Yes | Yes | No |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -485,7 +485,8 @@ export function CharacterSheetPage() {
|
||||
onError={(err) => setFormError(extractErrorMessage(err))}
|
||||
/>
|
||||
|
||||
{canEdit && (
|
||||
{/* HP is the DM's to change; a player reads it. */}
|
||||
{isDM && (
|
||||
<div className="flex-row mt-sm flex-wrap">
|
||||
<NumberInput
|
||||
min={1}
|
||||
@@ -515,20 +516,18 @@ export function CharacterSheetPage() {
|
||||
/>
|
||||
</label>
|
||||
{/* Armor is an equipped item now; this is the DM's own adjustment on top. */}
|
||||
{isDM && (
|
||||
<label className="flex-row gap-sm" title="Added to AC on top of what equipped gear gives. Can be negative.">
|
||||
Extra AC (DM) <InfoTip term="ARMOR_BONUS" />
|
||||
<NumberInput
|
||||
key={`armor:${character.armorBonus}`}
|
||||
min={-30}
|
||||
max={30}
|
||||
keepValue
|
||||
defaultValue={character.armorBonus}
|
||||
style={{ width: 70 }}
|
||||
onBlur={(e) => patchCharacter.mutate({ armorBonus: Number(e.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<label className="flex-row gap-sm" title="Added to AC on top of what equipped gear gives. Can be negative.">
|
||||
Extra AC (DM) <InfoTip term="ARMOR_BONUS" />
|
||||
<NumberInput
|
||||
key={`armor:${character.armorBonus}`}
|
||||
min={-30}
|
||||
max={30}
|
||||
keepValue
|
||||
defaultValue={character.armorBonus}
|
||||
style={{ width: 70 }}
|
||||
onBlur={(e) => patchCharacter.mutate({ armorBonus: Number(e.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -867,7 +866,8 @@ export function CharacterSheetPage() {
|
||||
change for you: if you are short of a coin, a bigger one is broken. Cash ($) is kept apart: it is never
|
||||
exchanged for coins.
|
||||
</p>
|
||||
{canEdit && (
|
||||
{/* Recording a gain or a spend is the DM's; a player gives, buys and exchanges. */}
|
||||
{isDM && (
|
||||
<div className="flex-row flex-wrap">
|
||||
<NumberInput
|
||||
min={1}
|
||||
|
||||
Generated
+4
-4
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "dnd-campaign-manager",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dnd-campaign-manager",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"workspaces": [
|
||||
"backend",
|
||||
"frontend"
|
||||
]
|
||||
},
|
||||
"backend": {
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.20.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
},
|
||||
"frontend": {
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.103.2",
|
||||
"axios": "^1.20.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dnd-campaign-manager",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.1",
|
||||
"private": true,
|
||||
"description": "D&D 5e campaign manager — monorepo root",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user