Ringworld 0.5.0: v13/v14 compatibility, Ringworld '84 theme, combat automation
- Foundry v13/v14 compatibility (verified on 14.364) - Complete "Ringworld '84" retheme: CSS custom properties, warm space-black + bronze + ring-arc amber palette, uniform across sheets, dialogs, chat cards, hit-location picker, and impulse HUD - Fix HP damage evaporating on re-prepare: maximums always derived, current values source-authoritative, with a one-time world migration - Ammunition tracking: per-attack consumption, reload button, ammo badge; empty weapons block attacks - Energy weapons auto-link a carried power pack instead of failing - Special Success rolls damage dice twice; fumbles flag GM adjudication (both toggleable world settings) - Defender Dodge prompt before damage (special hits need special dodge) - Damage Mod and derived rolls always recompute; shown read-only - Creature sheet rebuilt to full parity: characteristics, derived rolls, body-schema hit grid with AP totals, weapons/armor/skills - Seeded "Ringworld GM Reference" journal (outcomes, impulses, attack workflow, wounds, 1984 quirks, quick start) - Shared characteristic-check dialog between explorer and creature Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
# Ringworld Foundry System (v12-v13 MVP)
|
# Ringworld Foundry System (v13-v14)
|
||||||
|
|
||||||
Ringworld is a custom Foundry VTT game system for BRP-like percentile play with hit locations, location armor, and energy-tracked weapons.
|
Ringworld is a custom Foundry VTT game system for the classic 1984 Chaosium percentile RPG: BRP-style roll-under checks, seven hit locations with location armor, energy- and ammunition-tracked weapons, and the Ringworld impulse combat clock.
|
||||||
|
|
||||||
|
Version 0.5.0 is a full rework: Foundry v13/v14 compatibility, a uniform "Ringworld '84" visual theme (warm space-black, bronze structure, single ring-arc amber accent — inspired by Larry Niven's Ringworld and the original boxed set), combat automation for the system's quirks, and a seeded GM Reference journal.
|
||||||
|
|
||||||
## Online Install (GitHub)
|
## Online Install (GitHub)
|
||||||
|
|
||||||
@@ -16,26 +18,50 @@ Companion content module manifest URL:
|
|||||||
Starter world data archive:
|
Starter world data archive:
|
||||||
- `https://raw.githubusercontent.com/slaguru666/Ringworld/main/dist/ringworld-starter-world.zip`
|
- `https://raw.githubusercontent.com/slaguru666/Ringworld/main/dist/ringworld-starter-world.zip`
|
||||||
|
|
||||||
|
## What's in 0.5.0
|
||||||
|
|
||||||
|
Visual:
|
||||||
|
- Complete "Ringworld '84" retheme driven by CSS custom properties: one palette across actor sheets, item sheets, dialogs, chat cards, the hit-location picker, and the impulse HUD. No neon.
|
||||||
|
- Legible dark dialogs, amber active tabs, monospace numerals, uppercase display headers.
|
||||||
|
|
||||||
|
Combat automation:
|
||||||
|
- Special Success rolls damage dice twice (world setting `Automate Special Success Damage`).
|
||||||
|
- Fumbles flag a GM adjudication note on the chat card.
|
||||||
|
- Defender Dodge prompt before damage: a successful dodge evades a normal hit; only a special dodge evades a special hit (world setting `Prompt Defender Dodge`).
|
||||||
|
- Ammunition is consumed per attack (`Ammo Loaded / Ammo Max` on weapons), with a Reload button and ammo badge on sheet weapon rows. Empty weapons block the attack.
|
||||||
|
- Energy weapons auto-link to the best carried power pack when none is linked, instead of failing the attack.
|
||||||
|
|
||||||
|
Correctness fixes:
|
||||||
|
- Hit point damage now persists reliably: maximums are always derived (CON + MAS), current values are source-authoritative, and a one-time migration preserves what tables saw under the old logic. (Previously damage could silently heal back to full on unlinked tokens and fresh actors.)
|
||||||
|
- Damage Mod and derived rolls (Health/Reasoning/Luck/Dodge) always recompute from characteristics and can no longer go stale; they display read-only.
|
||||||
|
|
||||||
|
GM experience:
|
||||||
|
- A **Ringworld GM Reference** journal is seeded into every world: percentile outcomes, the impulse clock, the attack workflow, wound effects, the 1984 quirks (MAS, skill pools, gravity encumbrance, power packs), and a quick-start checklist.
|
||||||
|
- The creature sheet is now a full monster sheet: characteristics with multiplier checks, derived rolls, body-schema hit-location grid with stacked AP totals, weapons with ammo/reload, armor, skills, conditions reset, and portrait picker.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
MVP implemented:
|
Implemented:
|
||||||
- Actor types: `explorer`, `creature`, `vehicle` (stub)
|
- Actor types: `explorer`, `creature`, `vehicle` (stub)
|
||||||
- Item types: `skill`, `weapon`, `armor`, `gear`, `species`
|
- Item types: `skill`, `weapon`, `armor`, `gear`, `species`, `pursuit`
|
||||||
- Percentile checks with `Special Success` and `Fumble`
|
- Percentile checks with `Special Success` and `Fumble`
|
||||||
- 1d20 hit location roll with Ringworld 7-location mapping
|
- 1d20 hit location roll with per-species body schemas (humanoid, dolphin, puppeteer, trinoc, bandersnatchi, kzinti)
|
||||||
- Weapon workflow with target selection, hit location, armor subtraction, and damage to both location HP and general HP
|
- Weapon workflow: aim states, modifiers, ammo/energy consumption, defender dodge, hit location, armor stacking, damage to location + general HP
|
||||||
- Auto conditions at location 0 or less (`prone`, `unconscious`, limb unusable flags)
|
- Auto conditions at location 0 or less (`prone`, `unconscious`, limb unusable flags), synced to token status icons
|
||||||
- Armor overlap stacking across all equipped armor items covering a location
|
- Impulse combat tracker (Action Ranking clock) with floating HUD
|
||||||
- Energy use from actor pool or linked power pack item
|
- World catalog seeding: species, 55 base skills, 18 pursuits, weapons, armor, gear
|
||||||
- World catalog seeding for Ringworld equipment, armor, weapons, gear, and species
|
- Character tools: gravity-based characteristic rolls, random explorer generation, point-buy character builder
|
||||||
- Character tools: roll all characteristics and random explorer generation
|
|
||||||
|
Backlog:
|
||||||
|
- Vehicle combat and computer subsystems
|
||||||
|
- Compendium packs (content currently ships via the idempotent world seeder)
|
||||||
|
- Localization (UI strings are hardcoded English)
|
||||||
|
|
||||||
## Install / Build
|
## Install / Build
|
||||||
|
|
||||||
From this project folder:
|
From this project folder:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /Users/timevans/FoundryVTT/foundryvtt-ringworld
|
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
@@ -44,58 +70,36 @@ Build output:
|
|||||||
- `module/ringworld.mjs`
|
- `module/ringworld.mjs`
|
||||||
- `styles/ringworld.css`
|
- `styles/ringworld.css`
|
||||||
|
|
||||||
## Foundry placement
|
Install into a local Foundry data directory (macOS example):
|
||||||
|
|
||||||
Foundry loads systems from `.../data/systems` (or `.../Data/systems` on case-insensitive macOS paths).
|
|
||||||
|
|
||||||
Recommended live-dev symlink:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ln -s /Users/timevans/FoundryVTT/foundryvtt-ringworld /Users/timevans/FoundryVTT/data/systems/ringworld
|
rsync -a --delete \
|
||||||
|
--exclude node_modules --exclude .git --exclude src --exclude dist \
|
||||||
|
--exclude world-data --exclude seeds --exclude ringworld-content \
|
||||||
|
./ "$HOME/Library/Application Support/FoundryVTT/Data/systems/ringworld/"
|
||||||
```
|
```
|
||||||
|
|
||||||
If `ringworld` already exists, remove or rename it first.
|
|
||||||
|
|
||||||
## Using the system
|
## Using the system
|
||||||
|
|
||||||
1. In Foundry, create a world using **Ringworld** system.
|
1. In Foundry, create a world using the **Ringworld** system. The catalog and GM Reference journal seed automatically on first GM load.
|
||||||
2. Create an **Explorer** actor.
|
2. Read the **Ringworld GM Reference** journal for the rules the system automates.
|
||||||
3. In the **Skills** tab, click **Populate Ringworld Base Skills** to add all canonical Ringworld skills.
|
3. Create an **Explorer** actor (or use **Create Random Explorer** in the Actors directory).
|
||||||
4. Add extra custom skills or weapon items from the actor sheet as needed.
|
4. Target a token, then click **Use** on a weapon and follow the prompts.
|
||||||
5. For energy weapons:
|
5. For projectile weapons, set `Ammo Max` on the weapon and use the Reload button on the sheet.
|
||||||
- Create a `gear` item with `Is Power Pack` checked.
|
6. Check **System Settings** for the automation toggles.
|
||||||
- Set weapon `Power Source` to `Power Pack Item` and select the pack.
|
|
||||||
6. Target a token, then click **Use** on a weapon.
|
|
||||||
7. Choose hit location (or random 1d20) and confirm damage application in chat.
|
|
||||||
8. Use header tools on Explorer sheet:
|
|
||||||
- **Roll Characteristics**: rolls all characteristic values from gravity profile
|
|
||||||
- **Random Character**: random species + gravity + rolled stats + starter kit
|
|
||||||
|
|
||||||
## Catalog and Generators
|
## Rules implemented
|
||||||
|
|
||||||
- On first GM world load, the system seeds world Items with Ringworld catalog entries.
|
|
||||||
- You can reseed manually from **Items Directory** using `Seed Ringworld Items`.
|
|
||||||
- You can create a random explorer from **Actors Directory** using `Create Random Explorer`.
|
|
||||||
- If you install the companion module **Ringworld Content**, you can import everything with one button:
|
|
||||||
- **Items Directory** -> `Import Ringworld Content`
|
|
||||||
|
|
||||||
## Rules implemented for v1
|
|
||||||
|
|
||||||
- Roll-under percentile checks (`1d100 <= target`)
|
- Roll-under percentile checks (`1d100 <= target`)
|
||||||
- Special Success: roll `<= floor(target / 5)`
|
- Special Success: roll `<= floor(target / 5)` — damage dice doubled when automated
|
||||||
- Fumble:
|
- Fumble: target `< 50`: roll `96-100`; target `>= 50`: roll `100`
|
||||||
- target `< 50`: roll `96-100`
|
- Ranged aim states: unaimed quarter skill, partial aim half, full aim full skill
|
||||||
- target `>= 50`: roll `100`
|
- Dodge: DEX x5; success evades a normal hit, special dodge required against a special hit
|
||||||
- Hit locations (1d20):
|
- Hit locations (humanoid 1d20): `01-03 R Leg | 04-06 L Leg | 07-10 Abdomen | 11-15 Chest | 16-17 R Arm | 18-19 L Arm | 20 Head`
|
||||||
- `01-03 Right Leg`
|
|
||||||
- `04-06 Left Leg`
|
|
||||||
- `07-10 Abdomen`
|
|
||||||
- `11-15 Chest`
|
|
||||||
- `16-17 Right Arm`
|
|
||||||
- `18-19 Left Arm`
|
|
||||||
- `20 Head`
|
|
||||||
- Locational damage also reduces general HP (same final damage)
|
- Locational damage also reduces general HP (same final damage)
|
||||||
- Armor subtracts 1:1 damage and stacks across overlapping covered armor pieces
|
- Armor subtracts 1:1 and stacks across overlapping covered pieces
|
||||||
|
- General HP = CON + MAS; damage modifier from STR + MAS
|
||||||
|
- Encumbrance vs STR with gravity multipliers (light x0.5, heavy x2)
|
||||||
|
|
||||||
## Seeds
|
## Seeds
|
||||||
|
|
||||||
@@ -104,10 +108,8 @@ If `ringworld` already exists, remove or rename it first.
|
|||||||
- `example-items.json` sample skill/weapon/armor/power pack
|
- `example-items.json` sample skill/weapon/armor/power pack
|
||||||
- `example-explorer.json` sample explorer with items
|
- `example-explorer.json` sample explorer with items
|
||||||
|
|
||||||
These are JSON templates for manual import or scripted pack generation.
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Content is numeric/stat-driven only. No long rulebook text is embedded.
|
- Content is numeric/stat-driven only. No long rulebook text is embedded.
|
||||||
- Vehicle combat and computer subsystems remain backlog.
|
|
||||||
- `world-data/` contains a starter world shell and packaging files for server transfer.
|
- `world-data/` contains a starter world shell and packaging files for server transfer.
|
||||||
|
- `npm run typecheck` reports pre-existing errors from the v12 types package against v13/v14 APIs; the vite build is the authoritative check.
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
+1312
-968
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -2,14 +2,14 @@
|
|||||||
"id": "ringworld-content",
|
"id": "ringworld-content",
|
||||||
"title": "Ringworld Content",
|
"title": "Ringworld Content",
|
||||||
"description": "Companion content module for Ringworld: one-click import for skills, species, pursuits, weapons, armor, and gear.",
|
"description": "Companion content module for Ringworld: one-click import for skills, species, pursuits, weapons, armor, and gear.",
|
||||||
"version": "0.1.0",
|
"version": "0.5.0",
|
||||||
"url": "https://github.com/slaguru666/Ringworld",
|
"url": "https://github.com/slaguru666/Ringworld",
|
||||||
"manifest": "https://raw.githubusercontent.com/slaguru666/Ringworld/main/ringworld-content/module.json",
|
"manifest": "https://raw.githubusercontent.com/slaguru666/Ringworld/main/ringworld-content/module.json",
|
||||||
"download": "https://raw.githubusercontent.com/slaguru666/Ringworld/main/dist/ringworld-content.zip",
|
"download": "https://raw.githubusercontent.com/slaguru666/Ringworld/main/dist/ringworld-content.zip",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "12",
|
"minimum": "13",
|
||||||
"verified": "13.351",
|
"verified": "14.364",
|
||||||
"maximum": "13"
|
"maximum": "14"
|
||||||
},
|
},
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { recomputeHitPointMaximums, rollSkillForActor, rollStandaloneHitLocation, useWeaponWorkflow } from "../mechanics";
|
import { recomputeHitPointMaximums, reloadWeapon, rollSkillForActor, rollStandaloneHitLocation, useWeaponWorkflow } from "../mechanics";
|
||||||
import { normalizeSkillName, RINGWORLD_BASE_SKILLS } from "../data-base-skills";
|
import { normalizeSkillName, RINGWORLD_BASE_SKILLS } from "../data-base-skills";
|
||||||
import { randomizeExplorerActor, rollCharacteristicsForActor } from "../character-tools";
|
import { randomizeExplorerActor, rollCharacteristicsForActor } from "../character-tools";
|
||||||
import { skillIconPath } from "../skill-icons";
|
import { skillIconPath } from "../skill-icons";
|
||||||
@@ -12,10 +12,6 @@ function num(value: unknown, fallback = 0): number {
|
|||||||
return Number.isFinite(n) ? n : fallback;
|
return Number.isFinite(n) ? n : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasValue(value: unknown): boolean {
|
|
||||||
return value !== null && value !== undefined && String(value).trim() !== "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function clampCharacteristic(value: number): number {
|
function clampCharacteristic(value: number): number {
|
||||||
return Math.max(1, Math.min(99, Math.floor(value)));
|
return Math.max(1, Math.min(99, Math.floor(value)));
|
||||||
}
|
}
|
||||||
@@ -146,15 +142,15 @@ export class RingworldActor extends Actor {
|
|||||||
profile.physiologicalAge = num(profile.physiologicalAge, profile.chronologicalAge);
|
profile.physiologicalAge = num(profile.physiologicalAge, profile.chronologicalAge);
|
||||||
system.profile = profile;
|
system.profile = profile;
|
||||||
|
|
||||||
|
// Damage mod and derived rolls are always recomputed from characteristics so
|
||||||
|
// they can never go stale after a characteristic edit.
|
||||||
system.combat = system.combat ?? {};
|
system.combat = system.combat ?? {};
|
||||||
if (!hasValue(system.combat.damageMod)) {
|
system.combat.damageMod = computeDamageMod(num(characteristics.str, 10), num(characteristics.mas, 10));
|
||||||
system.combat.damageMod = computeDamageMod(num(characteristics.str, 10), num(characteristics.mas, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
system.derivedRolls = system.derivedRolls ?? {};
|
system.derivedRolls = system.derivedRolls ?? {};
|
||||||
const derivedRollDefaults = computeDerivedRolls(characteristics);
|
const derivedRollDefaults = computeDerivedRolls(characteristics);
|
||||||
for (const key of Object.keys(derivedRollDefaults) as DerivedCheckKey[]) {
|
for (const key of Object.keys(derivedRollDefaults) as DerivedCheckKey[]) {
|
||||||
system.derivedRolls[key] = num(system.derivedRolls[key], derivedRollDefaults[key]);
|
system.derivedRolls[key] = derivedRollDefaults[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
system.skillRootMax = system.skillRootMax ?? {};
|
system.skillRootMax = system.skillRootMax ?? {};
|
||||||
@@ -228,6 +224,12 @@ export class RingworldActor extends Actor {
|
|||||||
await useWeaponWorkflow(this, weapon);
|
await useWeaponWorkflow(this, weapon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reloadWeaponById(weaponId: string): Promise<void> {
|
||||||
|
const weapon = this.items.get(weaponId);
|
||||||
|
if (!weapon || weapon.type !== "weapon") return;
|
||||||
|
await reloadWeapon(this, weapon);
|
||||||
|
}
|
||||||
|
|
||||||
async ensureBaseSkills(): Promise<number> {
|
async ensureBaseSkills(): Promise<number> {
|
||||||
if (this.type !== "explorer" && this.type !== "creature") return 0;
|
if (this.type !== "explorer" && this.type !== "creature") return 0;
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,17 @@ export async function postAttackCard(data: {
|
|||||||
energySourceLabel: string;
|
energySourceLabel: string;
|
||||||
energyCost: number;
|
energyCost: number;
|
||||||
energyRemaining: number;
|
energyRemaining: number;
|
||||||
|
ammoUsed?: boolean;
|
||||||
|
ammoCost?: number;
|
||||||
|
ammoRemaining?: number;
|
||||||
|
dodge?: {
|
||||||
|
attempted: boolean;
|
||||||
|
roll: number;
|
||||||
|
target: number;
|
||||||
|
success: boolean;
|
||||||
|
special: boolean;
|
||||||
|
negated: boolean;
|
||||||
|
} | null;
|
||||||
notes: string;
|
notes: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { SYSTEM_ID } from "./constants";
|
||||||
|
|
||||||
|
const JOURNAL_NAME = "Ringworld GM Reference";
|
||||||
|
|
||||||
|
interface ReferencePage {
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPages(): ReferencePage[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "Percentile Outcomes",
|
||||||
|
content: `
|
||||||
|
<p>Every check is a d100 roll against a target percentage. The system classifies results automatically:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Success</strong> — roll ≤ target.</li>
|
||||||
|
<li><strong>Special Success</strong> — roll ≤ one fifth of the target (minimum 1). Attacks roll their damage dice twice when <em>Automate Special Success Damage</em> is enabled.</li>
|
||||||
|
<li><strong>Failure</strong> — roll > target.</li>
|
||||||
|
<li><strong>Fumble</strong> — roll of 96–100 when the target is below 50, or a natural 100 otherwise. The GM adjudicates a mishap: dropped or damaged weapon, hit an ally, drained power pack.</li>
|
||||||
|
</ul>
|
||||||
|
<p>Characteristic checks use Characteristic × Multiplier (usually ×5): click any characteristic button on a sheet to pick the multiplier.</p>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Combat Sequence (Impulses)",
|
||||||
|
content: `
|
||||||
|
<p>Ringworld does not use simple initiative. Combat rounds are split into <strong>impulses</strong> and each combatant acts on a rhythm set by their <strong>Action Ranking (AR)</strong>, which is derived from DEX (lower is faster) and worsened by encumbrance.</p>
|
||||||
|
<ol>
|
||||||
|
<li>Add combatants to the Combat Tracker as normal. The Ringworld impulse panel replaces the core tracker list.</li>
|
||||||
|
<li>Each combatant schedules an action: a <strong>major action</strong> (attack, full move, complex task) completes after AR impulses; a <strong>minor action</strong> (drop prone, shout, draw) completes after 1 impulse.</li>
|
||||||
|
<li>Advance the clock with <em>Next Completion</em> or <em>+1</em>. When a combatant's action completes, resolve it, then let them schedule the next one.</li>
|
||||||
|
</ol>
|
||||||
|
<p>The floating impulse HUD can be dragged anywhere on screen and mirrors the tracker for quick play.</p>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Attack Workflow",
|
||||||
|
content: `
|
||||||
|
<p>Target a token, then press the weapon's <strong>Use</strong> button on the attacker's sheet. The system walks through the 1984 rules in order:</p>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Aim state</strong> (ranged only): unaimed fires at quarter skill, partially aimed at half, fully aimed at full skill. Cover, movement, range and situational modifiers are applied before the aim multiplier.</li>
|
||||||
|
<li><strong>Ammunition and energy</strong> are spent. Projectile weapons with an ammo capacity are blocked when empty — use the <em>Reload</em> button on the weapon row. Energy weapons draw from the actor pool or a linked power pack.</li>
|
||||||
|
<li><strong>Dodge</strong>: on a hit, the defender is offered a Dodge roll (DEX ×5). A successful dodge evades a normal hit; only a <em>special</em> dodge evades a <em>special</em> hit. Disable with the <em>Prompt Defender Dodge</em> setting.</li>
|
||||||
|
<li><strong>Hit location</strong> is rolled on a d20 (or picked manually). Species use their own body schema — puppeteers, dolphins, trinocs, bandersnatchi and kzinti map differently.</li>
|
||||||
|
<li><strong>Damage</strong> is rolled, stacked armor at that location is subtracted, and the result reduces <em>both</em> the location HP and the general HP.</li>
|
||||||
|
</ol>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wounds & Conditions",
|
||||||
|
content: `
|
||||||
|
<p>When a hit location reaches 0 HP or less, conditions are applied automatically:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Head or Chest at 0</strong> — unconscious and prone.</li>
|
||||||
|
<li><strong>Abdomen at 0</strong> — prone, both legs unusable.</li>
|
||||||
|
<li><strong>Arm at 0</strong> — that arm is unusable.</li>
|
||||||
|
<li><strong>Leg at 0</strong> — that leg is unusable, and the character falls prone.</li>
|
||||||
|
<li><strong>General HP at 0</strong> — dying.</li>
|
||||||
|
<li><strong>Head, chest or abdomen at negative full value</strong> — dying; a head at negative full value is death outright.</li>
|
||||||
|
</ul>
|
||||||
|
<p>Limbs cannot go below their negative maximum. Use <em>Reset Conditions</em> on a sheet after treatment or recovery. Prone, unconscious and dead sync to token status icons.</p>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Running the 1984 Rules",
|
||||||
|
content: `
|
||||||
|
<p>Quirks of the original boxed set that the system encodes for you:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>General HP = CON + MAS.</strong> MAS (mass) replaces the familiar SIZ. Location HP scale from general HP by body schema.</li>
|
||||||
|
<li><strong>Damage modifier</strong> comes from STR + MAS and is recomputed automatically when characteristics change.</li>
|
||||||
|
<li><strong>Skill pools:</strong> Education points = (EDU − 10) × 20, Pursuit points = (Age − EDU − 6) × 20, Special points = INT × 10. The character builder on the Skills tab tracks all three.</li>
|
||||||
|
<li><strong>Skill category caps:</strong> each category is capped by a characteristic pair (e.g. Agility by STR + DEX). The builder enforces the caps.</li>
|
||||||
|
<li><strong>Gravity matters:</strong> carried weight is halved in light gravity and doubled in heavy gravity before comparing to the STR limit; over-limit explorers slow to Move 2 and suffer an Action Ranking penalty.</li>
|
||||||
|
<li><strong>Energy economy:</strong> most Ringworld tech runs on power packs (R20/R50/R100). Weapons can draw from a linked pack — keep spares, and remember fumbles can drain them.</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GM Quick Start",
|
||||||
|
content: `
|
||||||
|
<p>To get a table playing quickly:</p>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Seed the catalog</strong> (Items sidebar button) — species, 55 base skills, pursuits, weapons, armor and gear from the boxed set appear in organised folders. This runs automatically for new worlds.</li>
|
||||||
|
<li><strong>Create Random Explorer</strong> (Actors sidebar button) — rolls characteristics by species and gravity, adds base skills and a starter kit.</li>
|
||||||
|
<li>For bespoke characters, unlock the sheet tools (padlock, top right of the sheet) and use the <strong>Character Builder</strong> on the Skills tab: pick pursuits, then allocate the three point pools.</li>
|
||||||
|
<li>Creatures get the same automation — hit locations, dodge, armor stacking — via the creature sheet, with a body schema selector for non-humanoids.</li>
|
||||||
|
<li>Check <strong>System Settings</strong> for the automation toggles (special success damage, defender dodge prompts).</li>
|
||||||
|
</ol>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedGmReference(): Promise<boolean> {
|
||||||
|
if (!game.user?.isGM) return false;
|
||||||
|
|
||||||
|
const journals = (game as any).journal;
|
||||||
|
const existing = journals?.find((entry: any) => entry.getFlag?.(SYSTEM_ID, "gmReference") === true)
|
||||||
|
?? journals?.getName?.(JOURNAL_NAME);
|
||||||
|
if (existing) return false;
|
||||||
|
|
||||||
|
const pages = buildPages().map((page, index) => ({
|
||||||
|
name: page.name,
|
||||||
|
type: "text",
|
||||||
|
sort: (index + 1) * 100000,
|
||||||
|
text: {
|
||||||
|
format: 1,
|
||||||
|
content: page.content.trim()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
await (getDocumentClass("JournalEntry") as any).create({
|
||||||
|
name: JOURNAL_NAME,
|
||||||
|
pages,
|
||||||
|
flags: {
|
||||||
|
[SYSTEM_ID]: {
|
||||||
|
gmReference: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ownership: { default: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
+279
-26
@@ -222,6 +222,44 @@ async function promptAttackModifiers(attacker: any, weapon: any, baseSkillTarget
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function consumeWeaponAmmo(actor: any, weapon: any): Promise<{
|
||||||
|
used: boolean;
|
||||||
|
remaining: number;
|
||||||
|
cost: number;
|
||||||
|
insufficient: boolean;
|
||||||
|
}> {
|
||||||
|
const cost = Math.max(0, num(weapon.system.ammoUsedPerAttack));
|
||||||
|
if (cost <= 0) {
|
||||||
|
return { used: false, remaining: 0, cost: 0, insufficient: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = num(weapon.system.ammo?.value);
|
||||||
|
const max = num(weapon.system.ammo?.max);
|
||||||
|
if (max <= 0 && current <= 0) {
|
||||||
|
// Ammo tracking not configured for this weapon; treat as untracked.
|
||||||
|
return { used: false, remaining: 0, cost, insufficient: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current < cost) {
|
||||||
|
ui.notifications?.warn(`${weapon.name} is out of ammunition. Reload before attacking.`);
|
||||||
|
return { used: true, remaining: current, cost, insufficient: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = current - cost;
|
||||||
|
await actor.updateEmbeddedDocuments("Item", [{ _id: weapon.id, "system.ammo.value": next }]);
|
||||||
|
return { used: true, remaining: next, cost, insufficient: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reloadWeapon(actor: any, weapon: any): Promise<void> {
|
||||||
|
const max = num(weapon.system.ammo?.max);
|
||||||
|
if (max <= 0) {
|
||||||
|
ui.notifications?.info(`${weapon.name} has no ammunition capacity configured (set Ammo Max on the weapon).`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await actor.updateEmbeddedDocuments("Item", [{ _id: weapon.id, "system.ammo.value": max }]);
|
||||||
|
ui.notifications?.info(`${weapon.name} reloaded (${max}/${max}).`);
|
||||||
|
}
|
||||||
|
|
||||||
async function consumeWeaponEnergy(actor: any, weapon: any): Promise<{
|
async function consumeWeaponEnergy(actor: any, weapon: any): Promise<{
|
||||||
sourceLabel: string;
|
sourceLabel: string;
|
||||||
remaining: number;
|
remaining: number;
|
||||||
@@ -237,10 +275,25 @@ async function consumeWeaponEnergy(actor: any, weapon: any): Promise<{
|
|||||||
|
|
||||||
if (source === "powerPack") {
|
if (source === "powerPack") {
|
||||||
const packId = String(weapon.system.powerPackId || "");
|
const packId = String(weapon.system.powerPackId || "");
|
||||||
const pack = packId ? actor.items.get(packId) : null;
|
let pack = packId ? actor.items.get(packId) : null;
|
||||||
|
|
||||||
if (!pack || pack.type !== "gear" || !pack.system.isPowerPack) {
|
if (!pack || pack.type !== "gear" || !pack.system.isPowerPack) {
|
||||||
ui.notifications?.warn("Weapon is configured for a power pack but no valid power pack is linked.");
|
// Auto-link the best carried power pack rather than failing the attack.
|
||||||
|
pack = actor.items.find((item: any) =>
|
||||||
|
item.type === "gear"
|
||||||
|
&& item.system.isPowerPack
|
||||||
|
&& !item.system.stowed
|
||||||
|
&& num(item.system.energy?.value) >= cost
|
||||||
|
) ?? null;
|
||||||
|
|
||||||
|
if (pack) {
|
||||||
|
await actor.updateEmbeddedDocuments("Item", [{ _id: weapon.id, "system.powerPackId": pack.id }]);
|
||||||
|
ui.notifications?.info(`${weapon.name} linked to ${pack.name}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pack || pack.type !== "gear" || !pack.system.isPowerPack) {
|
||||||
|
ui.notifications?.warn("Weapon is configured for a power pack but no valid power pack is carried.");
|
||||||
return { sourceLabel: "Power Pack", remaining: 0, cost, insufficient: true };
|
return { sourceLabel: "Power Pack", remaining: 0, cost, insufficient: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,6 +429,147 @@ async function applyLocationDamage(targetActor: any, slug: HitLocationSlug, rawD
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function promptCharacteristicCheck(actor: any, characterKey: string, label: string): Promise<void> {
|
||||||
|
const validKeys = new Set(["str", "mas", "con", "int", "pow", "dex", "app", "edu", "age"]);
|
||||||
|
if (!validKeys.has(characterKey)) return;
|
||||||
|
|
||||||
|
const base = Math.max(0, num(actor.system?.characteristics?.[characterKey], 0));
|
||||||
|
const content = `
|
||||||
|
<form class="rw-stat-roll-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Characteristic</label>
|
||||||
|
<input type="text" value="${foundry.utils.escapeHTML(label)}" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Base Value</label>
|
||||||
|
<input type="number" value="${base}" disabled />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Multiplier</label>
|
||||||
|
<select name="multiplier">
|
||||||
|
<option value="1">x1</option>
|
||||||
|
<option value="2">x2</option>
|
||||||
|
<option value="3">x3</option>
|
||||||
|
<option value="4">x4</option>
|
||||||
|
<option value="5" selected>x5</option>
|
||||||
|
<option value="6">x6</option>
|
||||||
|
<option value="7">x7</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p class="hint">Roll d100 against Characteristic x Multiplier.</p>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const multiplier = await new Promise<number | null>((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
const done = (value: number | null) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
new Dialog({
|
||||||
|
title: `${label} Check`,
|
||||||
|
content,
|
||||||
|
buttons: {
|
||||||
|
roll: {
|
||||||
|
label: "Roll",
|
||||||
|
callback: (dlgHtml: JQuery) => {
|
||||||
|
const selected = Number((dlgHtml[0]?.querySelector("select[name='multiplier']") as HTMLSelectElement | null)?.value || 1);
|
||||||
|
const safeMultiplier = Math.max(1, Math.min(7, Math.floor(selected || 1)));
|
||||||
|
done(safeMultiplier);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
label: "Cancel",
|
||||||
|
callback: () => done(null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
default: "roll",
|
||||||
|
close: () => done(null)
|
||||||
|
}, {
|
||||||
|
classes: ["ringworld-attack-dialog"]
|
||||||
|
}).render(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!multiplier) return;
|
||||||
|
const target = clampPercentTarget(base * multiplier);
|
||||||
|
const result = await rollPercentile(target);
|
||||||
|
|
||||||
|
await postSkillRollCard({
|
||||||
|
actor,
|
||||||
|
actorName: actor.name,
|
||||||
|
actorImg: actor.img,
|
||||||
|
skillName: `${label} x${multiplier}`,
|
||||||
|
roll: result.roll,
|
||||||
|
target: result.target,
|
||||||
|
outcomeLabel: outcomeLabel(result.outcome),
|
||||||
|
isSuccess: result.isSuccess,
|
||||||
|
isSpecial: result.isSpecial,
|
||||||
|
isFumble: result.isFumble
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorldSetting(key: string, fallback: boolean): boolean {
|
||||||
|
try {
|
||||||
|
return Boolean(game.settings.get(SYSTEM_ID, key));
|
||||||
|
} catch (_err) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptDefenderDodge(targetActor: any, attackWasSpecial: boolean): Promise<{
|
||||||
|
attempted: boolean;
|
||||||
|
roll: number;
|
||||||
|
target: number;
|
||||||
|
success: boolean;
|
||||||
|
special: boolean;
|
||||||
|
negated: boolean;
|
||||||
|
} | null> {
|
||||||
|
const dodgeTarget = num(targetActor.system?.derivedRolls?.dodge, 0);
|
||||||
|
const safeName = foundry.utils.escapeHTML(String(targetActor.name || "Target"));
|
||||||
|
|
||||||
|
const attempt = await new Promise<boolean>((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
const done = (value: boolean) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
new Dialog({
|
||||||
|
title: `${targetActor.name} - Dodge Attempt`,
|
||||||
|
content: `
|
||||||
|
<div class="rw-stat-roll-form">
|
||||||
|
<p><strong>${safeName}</strong> has been hit${attackWasSpecial ? " by a special success" : ""}.</p>
|
||||||
|
<p class="hint">Dodge ${dodgeTarget}%. A successful dodge evades a normal hit; only a special dodge evades a special hit.</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
buttons: {
|
||||||
|
dodge: { label: `Dodge (${dodgeTarget}%)`, callback: () => done(true) },
|
||||||
|
none: { label: "No Dodge", callback: () => done(false) }
|
||||||
|
},
|
||||||
|
default: "dodge",
|
||||||
|
close: () => done(false)
|
||||||
|
}, {
|
||||||
|
classes: ["ringworld-attack-dialog"],
|
||||||
|
width: 420
|
||||||
|
}).render(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!attempt) return null;
|
||||||
|
|
||||||
|
const result = await rollPercentile(dodgeTarget);
|
||||||
|
const negated = result.isSuccess && (!attackWasSpecial || result.isSpecial);
|
||||||
|
return {
|
||||||
|
attempted: true,
|
||||||
|
roll: result.roll,
|
||||||
|
target: result.target,
|
||||||
|
success: result.isSuccess,
|
||||||
|
special: result.isSpecial,
|
||||||
|
negated
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function useWeaponWorkflow(attacker: any, weapon: any): Promise<void> {
|
export async function useWeaponWorkflow(attacker: any, weapon: any): Promise<void> {
|
||||||
const targetActor = getSingleTargetActor();
|
const targetActor = getSingleTargetActor();
|
||||||
const hasTarget = Boolean(targetActor);
|
const hasTarget = Boolean(targetActor);
|
||||||
@@ -385,6 +579,9 @@ export async function useWeaponWorkflow(attacker: any, weapon: any): Promise<voi
|
|||||||
const modifierResult = await promptAttackModifiers(attacker, weapon, linkedSkill.target);
|
const modifierResult = await promptAttackModifiers(attacker, weapon, linkedSkill.target);
|
||||||
if (!modifierResult) return;
|
if (!modifierResult) return;
|
||||||
|
|
||||||
|
const ammo = await consumeWeaponAmmo(attacker, weapon);
|
||||||
|
if (ammo.insufficient) return;
|
||||||
|
|
||||||
const energy = await consumeWeaponEnergy(attacker, weapon);
|
const energy = await consumeWeaponEnergy(attacker, weapon);
|
||||||
if (energy.insufficient) return;
|
if (energy.insufficient) return;
|
||||||
|
|
||||||
@@ -418,7 +615,55 @@ export async function useWeaponWorkflow(attacker: any, weapon: any): Promise<voi
|
|||||||
energySourceLabel: energy.sourceLabel,
|
energySourceLabel: energy.sourceLabel,
|
||||||
energyCost: energy.cost,
|
energyCost: energy.cost,
|
||||||
energyRemaining: energy.remaining,
|
energyRemaining: energy.remaining,
|
||||||
notes: `Attack failed before damage was rolled. ${modifierResult.summary}`
|
ammoUsed: ammo.used,
|
||||||
|
ammoCost: ammo.cost,
|
||||||
|
ammoRemaining: ammo.remaining,
|
||||||
|
dodge: null,
|
||||||
|
notes: attack.isFumble
|
||||||
|
? `Fumble! The GM should adjudicate a mishap (dropped or damaged weapon, hit an ally, drained power). ${modifierResult.summary}`
|
||||||
|
: `Attack failed before damage was rolled. ${modifierResult.summary}`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the defender a dodge attempt before damage is applied.
|
||||||
|
let dodge: Awaited<ReturnType<typeof promptDefenderDodge>> = null;
|
||||||
|
if (targetActor && getWorldSetting("promptDefenderDodge", true)) {
|
||||||
|
dodge = await promptDefenderDodge(targetActor, attack.isSpecial);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dodge?.negated) {
|
||||||
|
await postAttackCard({
|
||||||
|
actor: attacker,
|
||||||
|
actorName: attacker.name,
|
||||||
|
actorImg: attacker.img,
|
||||||
|
targetName,
|
||||||
|
hasTarget,
|
||||||
|
weaponName: weapon.name,
|
||||||
|
weaponImg: weapon.img,
|
||||||
|
skillName: linkedSkill.name,
|
||||||
|
skillImg: linkedSkill.img,
|
||||||
|
attackRoll: attack.roll,
|
||||||
|
attackTarget: attack.target,
|
||||||
|
attackOutcomeLabel: outcomeLabel(attack.outcome),
|
||||||
|
isSuccess: false,
|
||||||
|
isSpecial: attack.isSpecial,
|
||||||
|
isFumble: false,
|
||||||
|
hitLocationLabel: "-",
|
||||||
|
hitLocationRoll: null,
|
||||||
|
rawDamage: 0,
|
||||||
|
armor: 0,
|
||||||
|
finalDamage: 0,
|
||||||
|
remainingLocationHp: 0,
|
||||||
|
remainingGeneralHp: num(targetActor.system.generalHp?.value, 0),
|
||||||
|
energySourceLabel: energy.sourceLabel,
|
||||||
|
energyCost: energy.cost,
|
||||||
|
energyRemaining: energy.remaining,
|
||||||
|
ammoUsed: ammo.used,
|
||||||
|
ammoCost: ammo.cost,
|
||||||
|
ammoRemaining: ammo.remaining,
|
||||||
|
dodge,
|
||||||
|
notes: `${targetName} dodged the attack. ${modifierResult.summary}`
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -427,8 +672,14 @@ export async function useWeaponWorkflow(attacker: any, weapon: any): Promise<voi
|
|||||||
if (!hitSelection) return;
|
if (!hitSelection) return;
|
||||||
|
|
||||||
const damageFormula = String(weapon.system.damageFormula || "0").trim() || "0";
|
const damageFormula = String(weapon.system.damageFormula || "0").trim() || "0";
|
||||||
|
const automateSpecial = getWorldSetting("automateSpecialDamage", true);
|
||||||
|
const rollSpecialDamage = attack.isSpecial && automateSpecial;
|
||||||
const damageRoll = await evaluateRollWithDice(damageFormula);
|
const damageRoll = await evaluateRollWithDice(damageFormula);
|
||||||
const rawDamage = Math.max(0, num(damageRoll.total));
|
let rawDamage = Math.max(0, num(damageRoll.total));
|
||||||
|
if (rollSpecialDamage) {
|
||||||
|
const bonusRoll = await evaluateRollWithDice(damageFormula);
|
||||||
|
rawDamage += Math.max(0, num(bonusRoll.total));
|
||||||
|
}
|
||||||
|
|
||||||
const application = targetActor
|
const application = targetActor
|
||||||
? await applyLocationDamage(targetActor, hitSelection.slug, rawDamage)
|
? await applyLocationDamage(targetActor, hitSelection.slug, rawDamage)
|
||||||
@@ -465,9 +716,17 @@ export async function useWeaponWorkflow(attacker: any, weapon: any): Promise<voi
|
|||||||
energySourceLabel: energy.sourceLabel,
|
energySourceLabel: energy.sourceLabel,
|
||||||
energyCost: energy.cost,
|
energyCost: energy.cost,
|
||||||
energyRemaining: energy.remaining,
|
energyRemaining: energy.remaining,
|
||||||
notes: targetActor
|
ammoUsed: ammo.used,
|
||||||
? `Location and general HP were both reduced by final damage, per Ringworld rules. ${modifierResult.summary}`
|
ammoCost: ammo.cost,
|
||||||
: `No target selected: roll resolved with no damage application. ${modifierResult.summary}`
|
ammoRemaining: ammo.remaining,
|
||||||
|
dodge,
|
||||||
|
notes: [
|
||||||
|
rollSpecialDamage ? "Special success: damage dice rolled twice." : "",
|
||||||
|
targetActor
|
||||||
|
? "Location and general HP were both reduced by final damage, per Ringworld rules."
|
||||||
|
: "No target selected: roll resolved with no damage application.",
|
||||||
|
modifierResult.summary
|
||||||
|
].filter(Boolean).join(" ")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,24 +734,19 @@ export function recomputeHitPointMaximums(actor: any): void {
|
|||||||
if (actor.type !== "explorer" && actor.type !== "creature") return;
|
if (actor.type !== "explorer" && actor.type !== "creature") return;
|
||||||
|
|
||||||
// Ringworld general HP is CON + MAS. Location HP then scales from that general HP.
|
// Ringworld general HP is CON + MAS. Location HP then scales from that general HP.
|
||||||
|
// Maximums are always derived; current values are source-authoritative so damage
|
||||||
|
// survives re-preparation. A null source value means "never set": start at max.
|
||||||
const con = num(actor.system?.characteristics?.con, 10);
|
const con = num(actor.system?.characteristics?.con, 10);
|
||||||
const mas = num(actor.system?.characteristics?.mas, 10);
|
const mas = num(actor.system?.characteristics?.mas, 10);
|
||||||
const existingMax = num(actor.system.generalHp?.max, 0);
|
|
||||||
const existingValue = num(actor.system.generalHp?.value, 0);
|
|
||||||
const generalMax = Math.max(1, Math.floor(con + mas));
|
const generalMax = Math.max(1, Math.floor(con + mas));
|
||||||
|
const source = (actor._source ?? actor.toObject?.(false) ?? {}).system ?? {};
|
||||||
|
|
||||||
actor.system.generalHp = actor.system.generalHp ?? { value: generalMax, max: generalMax };
|
const sourceGeneralValue = source.generalHp?.value;
|
||||||
|
actor.system.generalHp = actor.system.generalHp ?? {};
|
||||||
actor.system.generalHp.max = generalMax;
|
actor.system.generalHp.max = generalMax;
|
||||||
const hadExisting = existingMax > 0;
|
actor.system.generalHp.value = (sourceGeneralValue === null || sourceGeneralValue === undefined)
|
||||||
const wasFull = hadExisting && existingValue >= existingMax;
|
|
||||||
const looksPlaceholder = existingMax <= 1 && existingValue <= 1;
|
|
||||||
const preservedDamage = hadExisting ? Math.max(0, existingMax - existingValue) : 0;
|
|
||||||
const recomputedValue = (!hadExisting || wasFull || looksPlaceholder)
|
|
||||||
? generalMax
|
? generalMax
|
||||||
: Math.max(0, generalMax - preservedDamage);
|
: Math.min(num(sourceGeneralValue, generalMax), generalMax);
|
||||||
|
|
||||||
actor.system.generalHp.value = Math.min(Math.max(0, recomputedValue), generalMax);
|
|
||||||
const generalAtFull = num(actor.system.generalHp.value, generalMax) >= generalMax;
|
|
||||||
|
|
||||||
actor.system.hitLocations = actor.system.hitLocations ?? {};
|
actor.system.hitLocations = actor.system.hitLocations ?? {};
|
||||||
actor.system.hitLocationSchema = actor.system.hitLocationSchema ?? "auto";
|
actor.system.hitLocationSchema = actor.system.hitLocationSchema ?? "auto";
|
||||||
@@ -502,20 +756,19 @@ export function recomputeHitPointMaximums(actor: any): void {
|
|||||||
const slug = entry.slug;
|
const slug = entry.slug;
|
||||||
const loc = actor.system.hitLocations[slug] ?? {
|
const loc = actor.system.hitLocations[slug] ?? {
|
||||||
label: slug,
|
label: slug,
|
||||||
hp: { value: 1, max: 1 },
|
hp: {},
|
||||||
armor: 0
|
armor: 0
|
||||||
};
|
};
|
||||||
loc.label = entry.label;
|
loc.label = entry.label;
|
||||||
const hpFactor = Math.max(0.05, num((entry as any).hpFactor, 0.3));
|
const hpFactor = Math.max(0.05, num((entry as any).hpFactor, 0.3));
|
||||||
const max = Math.max(1, Math.ceil(generalMax * hpFactor));
|
const max = Math.max(1, Math.ceil(generalMax * hpFactor));
|
||||||
|
const sourceLocValue = source.hitLocations?.[slug]?.hp?.value;
|
||||||
|
|
||||||
const previousMax = num(loc.hp?.max, 1);
|
loc.hp = loc.hp ?? {};
|
||||||
const previousValue = num(loc.hp?.value, previousMax);
|
|
||||||
const isPlaceholder = previousMax <= 1 && previousValue <= 1;
|
|
||||||
const shouldBackfillToFull = generalAtFull && previousValue <= 1 && max > 1;
|
|
||||||
|
|
||||||
loc.hp.max = max;
|
loc.hp.max = max;
|
||||||
loc.hp.value = (isPlaceholder || shouldBackfillToFull) ? max : Math.min(previousValue, max);
|
loc.hp.value = (sourceLocValue === null || sourceLocValue === undefined)
|
||||||
|
? max
|
||||||
|
: Math.min(num(sourceLocValue, max), max);
|
||||||
actor.system.hitLocations[slug] = loc;
|
actor.system.hitLocations[slug] = loc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
import { getActorHitLocationSchema } from "../../hit-location-schemas";
|
import { getActorHitLocationSchema, listHitLocationSchemas } from "../../hit-location-schemas";
|
||||||
|
import { promptCharacteristicCheck } from "../../mechanics";
|
||||||
|
|
||||||
const BaseActorSheet = (foundry as any).appv1.sheets.ActorSheet;
|
const BaseActorSheet = (foundry as any).appv1.sheets.ActorSheet;
|
||||||
|
|
||||||
|
function num(value: unknown, fallback = 0): number {
|
||||||
|
const n = Number(value);
|
||||||
|
return Number.isFinite(n) ? n : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemArmorAtLocation(item: any, slug: string): number {
|
||||||
|
if (item.type !== "armor" || !item.system.equipped || item.system.stowed) return 0;
|
||||||
|
if (!item.system.coverage?.[slug]) return 0;
|
||||||
|
const specific = num(item.system.perLocation?.[slug]);
|
||||||
|
if (specific > 0) return specific;
|
||||||
|
return num(item.system.value);
|
||||||
|
}
|
||||||
|
|
||||||
export class RingworldCreatureSheet extends BaseActorSheet {
|
export class RingworldCreatureSheet extends BaseActorSheet {
|
||||||
static get defaultOptions(): any {
|
static get defaultOptions(): any {
|
||||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
classes: ["ringworld", "sheet", "actor", "creature"],
|
classes: ["ringworld", "sheet", "actor", "creature"],
|
||||||
width: 760,
|
width: 860,
|
||||||
height: 700
|
height: 780
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,18 +32,33 @@ export class RingworldCreatureSheet extends BaseActorSheet {
|
|||||||
async getData(options?: any): Promise<any> {
|
async getData(options?: any): Promise<any> {
|
||||||
const data = await super.getData(options);
|
const data = await super.getData(options);
|
||||||
const actor = this.actor as any;
|
const actor = this.actor as any;
|
||||||
|
const schema = getActorHitLocationSchema(actor);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
system: actor.system,
|
system: actor.system,
|
||||||
skills: actor.items.filter((item: any) => item.type === "skill"),
|
skills: actor.items
|
||||||
|
.filter((item: any) => item.type === "skill")
|
||||||
|
.sort((a: any, b: any) => a.name.localeCompare(b.name)),
|
||||||
weapons: actor.items.filter((item: any) => item.type === "weapon"),
|
weapons: actor.items.filter((item: any) => item.type === "weapon"),
|
||||||
hitLocations: getActorHitLocationSchema(actor).entries.map((entry) => ({
|
armors: actor.items.filter((item: any) => item.type === "armor"),
|
||||||
slug: entry.slug,
|
hitSchemaId: schema.id,
|
||||||
rollRange: `${entry.min}-${entry.max}`,
|
hitSchemas: listHitLocationSchemas().map((entry: any) => ({ id: entry.id, label: entry.label })),
|
||||||
label: entry.label,
|
hitLocations: schema.entries.map((entry) => {
|
||||||
...(actor.system.hitLocations?.[entry.slug] ?? {})
|
const loc = actor.system.hitLocations?.[entry.slug] ?? {};
|
||||||
}))
|
const baseArmor = num(loc.armor);
|
||||||
|
const itemArmor = actor.items
|
||||||
|
.filter((item: any) => item.type === "armor")
|
||||||
|
.reduce((sum: number, item: any) => sum + itemArmorAtLocation(item, entry.slug), 0);
|
||||||
|
return {
|
||||||
|
slug: entry.slug,
|
||||||
|
rollRange: `${entry.min}-${entry.max}`,
|
||||||
|
label: entry.label,
|
||||||
|
armor: baseArmor,
|
||||||
|
apTotal: baseArmor + itemArmor,
|
||||||
|
hp: loc.hp ?? { value: 1, max: 1 }
|
||||||
|
};
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,31 +72,86 @@ export class RingworldCreatureSheet extends BaseActorSheet {
|
|||||||
});
|
});
|
||||||
|
|
||||||
html.find(".item-edit").on("click", (event) => {
|
html.find(".item-edit").on("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
const li = (event.currentTarget as HTMLElement).closest(".item");
|
const li = (event.currentTarget as HTMLElement).closest(".item");
|
||||||
const item = li ? this.actor.items.get(li.dataset.itemId ?? "") : null;
|
const item = li ? this.actor.items.get(li.dataset.itemId ?? "") : null;
|
||||||
item?.sheet?.render(true);
|
item?.sheet?.render(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
html.find(".item-delete").on("click", async (event) => {
|
html.find(".item-delete").on("click", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
const li = (event.currentTarget as HTMLElement).closest(".item");
|
const li = (event.currentTarget as HTMLElement).closest(".item");
|
||||||
if (!li?.dataset.itemId) return;
|
if (!li?.dataset.itemId) return;
|
||||||
await this.actor.deleteEmbeddedDocuments("Item", [li.dataset.itemId]);
|
await this.actor.deleteEmbeddedDocuments("Item", [li.dataset.itemId]);
|
||||||
});
|
});
|
||||||
|
|
||||||
html.find(".item-roll-skill").on("click", async (event) => {
|
html.find(".item-roll-skill").on("click", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
const li = (event.currentTarget as HTMLElement).closest(".item");
|
const li = (event.currentTarget as HTMLElement).closest(".item");
|
||||||
if (!li?.dataset.itemId) return;
|
if (!li?.dataset.itemId) return;
|
||||||
await (this.actor as any).rollSkillById(li.dataset.itemId);
|
await (this.actor as any).rollSkillById(li.dataset.itemId);
|
||||||
});
|
});
|
||||||
|
|
||||||
html.find(".item-use-weapon").on("click", async (event) => {
|
html.find(".item-use-weapon").on("click", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
const li = (event.currentTarget as HTMLElement).closest(".item");
|
const li = (event.currentTarget as HTMLElement).closest(".item");
|
||||||
if (!li?.dataset.itemId) return;
|
if (!li?.dataset.itemId) return;
|
||||||
await (this.actor as any).useWeaponById(li.dataset.itemId);
|
await (this.actor as any).useWeaponById(li.dataset.itemId);
|
||||||
});
|
});
|
||||||
|
|
||||||
html.find(".roll-hit-location").on("click", async () => {
|
html.find(".item-reload-weapon").on("click", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const li = (event.currentTarget as HTMLElement).closest(".item");
|
||||||
|
if (!li?.dataset.itemId) return;
|
||||||
|
await (this.actor as any).reloadWeaponById(li.dataset.itemId);
|
||||||
|
});
|
||||||
|
|
||||||
|
html.find(".roll-hit-location").on("click", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
await (this.actor as any).rollHitLocation();
|
await (this.actor as any).rollHitLocation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
html.find(".roll-characteristic").on("click", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const button = event.currentTarget as HTMLElement;
|
||||||
|
const key = String(button.dataset.char || "").trim().toLowerCase();
|
||||||
|
const label = String(button.dataset.label || key.toUpperCase());
|
||||||
|
if (!key) return;
|
||||||
|
await promptCharacteristicCheck(this.actor, key, label);
|
||||||
|
});
|
||||||
|
|
||||||
|
html.find(".roll-derived").on("click", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const key = String((event.currentTarget as HTMLElement).dataset.check || "");
|
||||||
|
if (!key) return;
|
||||||
|
await (this.actor as any).rollDerivedCheck(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
html.find(".reset-conditions").on("click", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
await this.actor.update({
|
||||||
|
"system.conditions.prone": false,
|
||||||
|
"system.conditions.unconscious": false,
|
||||||
|
"system.conditions.dead": false,
|
||||||
|
"system.conditions.dying": false,
|
||||||
|
"system.conditions.unusableLimbs.rArm": false,
|
||||||
|
"system.conditions.unusableLimbs.lArm": false,
|
||||||
|
"system.conditions.unusableLimbs.rLeg": false,
|
||||||
|
"system.conditions.unusableLimbs.lLeg": false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
html.find(".rw-portrait-img").on("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const FilePickerImpl = (foundry as any).applications?.apps?.FilePicker?.implementation ?? (globalThis as any).FilePicker;
|
||||||
|
const picker = new FilePickerImpl({
|
||||||
|
type: "image",
|
||||||
|
current: (this.actor as any).img,
|
||||||
|
callback: async (path: string) => {
|
||||||
|
await this.actor.update({ img: path });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
picker.render(true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getActorHitLocationSchema, listHitLocationSchemas } from "../../hit-loc
|
|||||||
import { PURSUIT_CATALOG } from "../../world-catalog";
|
import { PURSUIT_CATALOG } from "../../world-catalog";
|
||||||
import { outcomeLabel, rollPercentile } from "../../utils/percentile";
|
import { outcomeLabel, rollPercentile } from "../../utils/percentile";
|
||||||
import { postSkillRollCard } from "../../chat";
|
import { postSkillRollCard } from "../../chat";
|
||||||
|
import { promptCharacteristicCheck } from "../../mechanics";
|
||||||
|
|
||||||
const BaseActorSheet = (foundry as any).appv1.sheets.ActorSheet;
|
const BaseActorSheet = (foundry as any).appv1.sheets.ActorSheet;
|
||||||
|
|
||||||
@@ -777,86 +778,6 @@ export class RingworldExplorerSheet extends BaseActorSheet {
|
|||||||
await this.syncSkillsFromBuilderState(nextState);
|
await this.syncSkillsFromBuilderState(nextState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async rollCharacteristicCheck(characterKey: string, label: string): Promise<void> {
|
|
||||||
const validKeys = new Set(["str", "mas", "con", "int", "pow", "dex", "app", "edu", "age"]);
|
|
||||||
if (!validKeys.has(characterKey)) return;
|
|
||||||
|
|
||||||
const base = Math.max(0, toInt((this.actor as any).system?.characteristics?.[characterKey], 0));
|
|
||||||
const content = `
|
|
||||||
<form class="rw-stat-roll-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Characteristic</label>
|
|
||||||
<input type="text" value="${foundry.utils.escapeHTML(label)}" disabled />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Base Value</label>
|
|
||||||
<input type="number" value="${base}" disabled />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Multiplier</label>
|
|
||||||
<select name="multiplier">
|
|
||||||
<option value="1">x1</option>
|
|
||||||
<option value="2">x2</option>
|
|
||||||
<option value="3">x3</option>
|
|
||||||
<option value="4">x4</option>
|
|
||||||
<option value="5">x5</option>
|
|
||||||
<option value="6">x6</option>
|
|
||||||
<option value="7">x7</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<p class="hint">Roll d100 against Characteristic x Multiplier.</p>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const multiplier = await new Promise<number | null>((resolve) => {
|
|
||||||
let settled = false;
|
|
||||||
const done = (value: number | null) => {
|
|
||||||
if (settled) return;
|
|
||||||
settled = true;
|
|
||||||
resolve(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
new Dialog({
|
|
||||||
title: `${label} Check`,
|
|
||||||
content,
|
|
||||||
buttons: {
|
|
||||||
roll: {
|
|
||||||
label: "Roll",
|
|
||||||
callback: (dlgHtml: JQuery) => {
|
|
||||||
const selected = Number((dlgHtml[0]?.querySelector("select[name='multiplier']") as HTMLSelectElement | null)?.value || 1);
|
|
||||||
const safeMultiplier = Math.max(1, Math.min(7, Math.floor(selected || 1)));
|
|
||||||
done(safeMultiplier);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancel: {
|
|
||||||
label: "Cancel",
|
|
||||||
callback: () => done(null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
default: "roll",
|
|
||||||
close: () => done(null)
|
|
||||||
}).render(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!multiplier) return;
|
|
||||||
const target = clampPercent(base * multiplier);
|
|
||||||
const result = await rollPercentile(target);
|
|
||||||
|
|
||||||
await postSkillRollCard({
|
|
||||||
actor: this.actor,
|
|
||||||
actorName: this.actor.name,
|
|
||||||
actorImg: this.actor.img,
|
|
||||||
skillName: `${label} x${multiplier}`,
|
|
||||||
skillImg: DEFAULT_SKILL_ICON,
|
|
||||||
roll: result.roll,
|
|
||||||
target: result.target,
|
|
||||||
outcomeLabel: outcomeLabel(result.outcome),
|
|
||||||
isSuccess: result.isSuccess,
|
|
||||||
isSpecial: result.isSpecial,
|
|
||||||
isFumble: result.isFumble
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
activateListeners(html: JQuery): void {
|
activateListeners(html: JQuery): void {
|
||||||
super.activateListeners(html);
|
super.activateListeners(html);
|
||||||
|
|
||||||
@@ -873,7 +794,7 @@ export class RingworldExplorerSheet extends BaseActorSheet {
|
|||||||
const key = String(button.dataset.char || "").trim().toLowerCase();
|
const key = String(button.dataset.char || "").trim().toLowerCase();
|
||||||
const label = String(button.dataset.label || key.toUpperCase());
|
const label = String(button.dataset.label || key.toUpperCase());
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
await this.rollCharacteristicCheck(key, label);
|
await promptCharacteristicCheck(this.actor, key, label);
|
||||||
});
|
});
|
||||||
|
|
||||||
html.find(".item-create").on("click", async (event) => {
|
html.find(".item-create").on("click", async (event) => {
|
||||||
@@ -922,6 +843,13 @@ export class RingworldExplorerSheet extends BaseActorSheet {
|
|||||||
await (this.actor as any).useWeaponById(li.dataset.itemId);
|
await (this.actor as any).useWeaponById(li.dataset.itemId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
html.find(".item-reload-weapon").on("click", async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const li = (event.currentTarget as HTMLElement).closest(".item");
|
||||||
|
if (!li?.dataset.itemId) return;
|
||||||
|
await (this.actor as any).reloadWeaponById(li.dataset.itemId);
|
||||||
|
});
|
||||||
|
|
||||||
html.find(".item-toggle-equipped").on("click", async (event) => {
|
html.find(".item-toggle-equipped").on("click", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const li = (event.currentTarget as HTMLElement).closest(".item");
|
const li = (event.currentTarget as HTMLElement).closest(".item");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SYSTEM_ID } from "./constants";
|
import { SYSTEM_ID } from "./constants";
|
||||||
import { normalizeSkillName, RINGWORLD_BASE_SKILLS } from "./data-base-skills";
|
import { normalizeSkillName, RINGWORLD_BASE_SKILLS } from "./data-base-skills";
|
||||||
import { skillIconPath } from "./skill-icons";
|
import { skillIconPath } from "./skill-icons";
|
||||||
|
import { seedGmReference } from "./gm-reference";
|
||||||
|
|
||||||
export type GravityType = "light" | "normal" | "heavy";
|
export type GravityType = "light" | "normal" | "heavy";
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ interface CatalogSeed {
|
|||||||
img: string;
|
img: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RINGWORLD_CATALOG_VERSION = 9;
|
export const RINGWORLD_CATALOG_VERSION = 10;
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
speciesDefault: "systems/ringworld/assets/icons/species.svg",
|
speciesDefault: "systems/ringworld/assets/icons/species.svg",
|
||||||
@@ -962,8 +963,11 @@ export async function seedWorldCatalog(options?: { notify?: boolean }): Promise<
|
|||||||
await Item.updateDocuments(updatePayload as any[]);
|
await Item.updateDocuments(updatePayload as any[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addedReference = await seedGmReference();
|
||||||
|
|
||||||
if (options?.notify) {
|
if (options?.notify) {
|
||||||
ui.notifications?.info(`Ringworld catalog synced: +${createPayload.length} new, ${updatePayload.length} updated.`);
|
const referenceNote = addedReference ? " GM Reference journal added." : "";
|
||||||
|
ui.notifications?.info(`Ringworld catalog synced: +${createPayload.length} new, ${updatePayload.length} updated.${referenceNote}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { added: createPayload.length, updated: updatePayload.length };
|
return { added: createPayload.length, updated: updatePayload.length };
|
||||||
|
|||||||
@@ -18,6 +18,45 @@ import { initializeImpulseTracker } from "./module/impulse-tracker";
|
|||||||
import { DEFAULT_SKILL_ICON, baseSkillByName, resolveSkillIcon } from "./module/skill-icons";
|
import { DEFAULT_SKILL_ICON, baseSkillByName, resolveSkillIcon } from "./module/skill-icons";
|
||||||
|
|
||||||
const ENCUMBRANCE_DATA_MIGRATION_VERSION = 2;
|
const ENCUMBRANCE_DATA_MIGRATION_VERSION = 2;
|
||||||
|
const HP_DATA_MIGRATION_VERSION = 1;
|
||||||
|
|
||||||
|
// Under the pre-0.5 HP logic, any actor whose stored HP value was >= its stale
|
||||||
|
// stored max displayed as full (the value was healed to max on every prepare).
|
||||||
|
// Migrate those stored values to null ("never set" = derived full) so the new
|
||||||
|
// source-authoritative HP logic preserves what tables actually saw.
|
||||||
|
async function migrateActorHpData(): Promise<number> {
|
||||||
|
if (!game.user?.isGM) return 0;
|
||||||
|
let touched = 0;
|
||||||
|
|
||||||
|
const buildActorPatch = (sourceSystem: any): Record<string, unknown> | null => {
|
||||||
|
const patch: Record<string, unknown> = {};
|
||||||
|
const gv = sourceSystem?.generalHp?.value;
|
||||||
|
const gm = sourceSystem?.generalHp?.max;
|
||||||
|
if (gv !== null && gv !== undefined && Number(gv) >= Number(gm ?? 0)) {
|
||||||
|
patch["system.generalHp.value"] = null;
|
||||||
|
}
|
||||||
|
for (const [slug, loc] of Object.entries<any>(sourceSystem?.hitLocations ?? {})) {
|
||||||
|
const lv = loc?.hp?.value;
|
||||||
|
const lm = loc?.hp?.max;
|
||||||
|
if (lv === null || lv === undefined) continue;
|
||||||
|
if (Number(lv) >= Number(lm ?? 0) || (Number(lm ?? 0) <= 1 && Number(lv) <= 1)) {
|
||||||
|
patch[`system.hitLocations.${slug}.hp.value`] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.keys(patch).length ? patch : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const actor of game.actors ?? []) {
|
||||||
|
if ((actor as any).type !== "explorer" && (actor as any).type !== "creature") continue;
|
||||||
|
const patch = buildActorPatch((actor as any)._source?.system ?? {});
|
||||||
|
if (patch) {
|
||||||
|
await actor.update(patch);
|
||||||
|
touched += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return touched;
|
||||||
|
}
|
||||||
|
|
||||||
function normName(value: unknown): string {
|
function normName(value: unknown): string {
|
||||||
return String(value ?? "").trim().toLowerCase();
|
return String(value ?? "").trim().toLowerCase();
|
||||||
@@ -222,6 +261,28 @@ Hooks.once("init", async () => {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
});
|
});
|
||||||
|
game.settings.register("ringworld", "hpDataMigrationVersion", {
|
||||||
|
scope: "world",
|
||||||
|
config: false,
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
});
|
||||||
|
game.settings.register("ringworld", "automateSpecialDamage", {
|
||||||
|
name: "Automate Special Success Damage",
|
||||||
|
hint: "When an attack is a Special Success, roll the weapon damage dice twice and sum them.",
|
||||||
|
scope: "world",
|
||||||
|
config: true,
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
});
|
||||||
|
game.settings.register("ringworld", "promptDefenderDodge", {
|
||||||
|
name: "Prompt Defender Dodge",
|
||||||
|
hint: "After a successful attack on a target, offer a Dodge roll before damage is applied. A successful dodge evades a normal hit; only a special dodge evades a special hit.",
|
||||||
|
scope: "world",
|
||||||
|
config: true,
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
});
|
||||||
|
|
||||||
Handlebars.registerHelper("eq", (a: unknown, b: unknown) => a === b);
|
Handlebars.registerHelper("eq", (a: unknown, b: unknown) => a === b);
|
||||||
Handlebars.registerHelper("or", (a: unknown, b: unknown) => Boolean(a || b));
|
Handlebars.registerHelper("or", (a: unknown, b: unknown) => Boolean(a || b));
|
||||||
@@ -266,6 +327,15 @@ Hooks.once("ready", async () => {
|
|||||||
ui.notifications?.info(`Ringworld encumbrance migration updated ${touched} actor items.`);
|
ui.notifications?.info(`Ringworld encumbrance migration updated ${touched} actor items.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hpMigratedVersion = Number(game.settings.get("ringworld", "hpDataMigrationVersion") ?? 0);
|
||||||
|
if (hpMigratedVersion < HP_DATA_MIGRATION_VERSION) {
|
||||||
|
const touched = await migrateActorHpData();
|
||||||
|
await game.settings.set("ringworld", "hpDataMigrationVersion", HP_DATA_MIGRATION_VERSION);
|
||||||
|
if (touched > 0) {
|
||||||
|
ui.notifications?.info(`Ringworld HP migration updated ${touched} actors.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureSidebarDirectoryButtons();
|
ensureSidebarDirectoryButtons();
|
||||||
|
|||||||
+378
-161
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+4
-4
@@ -2,14 +2,14 @@
|
|||||||
"id": "ringworld",
|
"id": "ringworld",
|
||||||
"title": "Ringworld",
|
"title": "Ringworld",
|
||||||
"description": "Ringworld RPG-compatible percentile system (BRP-like)",
|
"description": "Ringworld RPG-compatible percentile system (BRP-like)",
|
||||||
"version": "0.1.0",
|
"version": "0.5.0",
|
||||||
"url": "https://github.com/slaguru666/Ringworld",
|
"url": "https://github.com/slaguru666/Ringworld",
|
||||||
"manifest": "https://raw.githubusercontent.com/slaguru666/Ringworld/main/system.json",
|
"manifest": "https://raw.githubusercontent.com/slaguru666/Ringworld/main/system.json",
|
||||||
"download": "https://raw.githubusercontent.com/slaguru666/Ringworld/main/dist/ringworld-system.zip",
|
"download": "https://raw.githubusercontent.com/slaguru666/Ringworld/main/dist/ringworld-system.zip",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "12",
|
"minimum": "13",
|
||||||
"verified": "13.351",
|
"verified": "14.364",
|
||||||
"maximum": "13"
|
"maximum": "14"
|
||||||
},
|
},
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
|
|||||||
+62
-16
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"unconsciousLevel": 0,
|
"unconsciousLevel": 0,
|
||||||
"generalHp": {
|
"generalHp": {
|
||||||
"value": 20,
|
"value": null,
|
||||||
"max": 20
|
"max": 20
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
@@ -95,37 +95,58 @@
|
|||||||
"hitLocations": {
|
"hitLocations": {
|
||||||
"rLeg": {
|
"rLeg": {
|
||||||
"label": "Right Leg",
|
"label": "Right Leg",
|
||||||
"hp": { "value": 6, "max": 6 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 6
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"lLeg": {
|
"lLeg": {
|
||||||
"label": "Left Leg",
|
"label": "Left Leg",
|
||||||
"hp": { "value": 6, "max": 6 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 6
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"abdomen": {
|
"abdomen": {
|
||||||
"label": "Abdomen",
|
"label": "Abdomen",
|
||||||
"hp": { "value": 6, "max": 6 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 6
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"chest": {
|
"chest": {
|
||||||
"label": "Chest",
|
"label": "Chest",
|
||||||
"hp": { "value": 7, "max": 7 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 7
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"rArm": {
|
"rArm": {
|
||||||
"label": "Right Arm",
|
"label": "Right Arm",
|
||||||
"hp": { "value": 5, "max": 5 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 5
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"lArm": {
|
"lArm": {
|
||||||
"label": "Left Arm",
|
"label": "Left Arm",
|
||||||
"hp": { "value": 5, "max": 5 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 5
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"head": {
|
"head": {
|
||||||
"label": "Head",
|
"label": "Head",
|
||||||
"hp": { "value": 6, "max": 6 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 6
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +215,7 @@
|
|||||||
},
|
},
|
||||||
"unconsciousLevel": 0,
|
"unconsciousLevel": 0,
|
||||||
"generalHp": {
|
"generalHp": {
|
||||||
"value": 20,
|
"value": null,
|
||||||
"max": 20
|
"max": 20
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
@@ -219,37 +240,58 @@
|
|||||||
"hitLocations": {
|
"hitLocations": {
|
||||||
"rLeg": {
|
"rLeg": {
|
||||||
"label": "Right Leg",
|
"label": "Right Leg",
|
||||||
"hp": { "value": 6, "max": 6 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 6
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"lLeg": {
|
"lLeg": {
|
||||||
"label": "Left Leg",
|
"label": "Left Leg",
|
||||||
"hp": { "value": 6, "max": 6 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 6
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"abdomen": {
|
"abdomen": {
|
||||||
"label": "Abdomen",
|
"label": "Abdomen",
|
||||||
"hp": { "value": 6, "max": 6 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 6
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"chest": {
|
"chest": {
|
||||||
"label": "Chest",
|
"label": "Chest",
|
||||||
"hp": { "value": 7, "max": 7 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 7
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"rArm": {
|
"rArm": {
|
||||||
"label": "Right Arm",
|
"label": "Right Arm",
|
||||||
"hp": { "value": 5, "max": 5 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 5
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"lArm": {
|
"lArm": {
|
||||||
"label": "Left Arm",
|
"label": "Left Arm",
|
||||||
"hp": { "value": 5, "max": 5 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 5
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
},
|
},
|
||||||
"head": {
|
"head": {
|
||||||
"label": "Head",
|
"label": "Head",
|
||||||
"hp": { "value": 6, "max": 6 },
|
"hp": {
|
||||||
|
"value": null,
|
||||||
|
"max": 6
|
||||||
|
},
|
||||||
"armor": 0
|
"armor": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,6 +332,10 @@
|
|||||||
},
|
},
|
||||||
"energyUsedPerAttack": 0,
|
"energyUsedPerAttack": 0,
|
||||||
"ammoUsedPerAttack": 0,
|
"ammoUsedPerAttack": 0,
|
||||||
|
"ammo": {
|
||||||
|
"value": 0,
|
||||||
|
"max": 0
|
||||||
|
},
|
||||||
"powerSource": "actor",
|
"powerSource": "actor",
|
||||||
"powerPackId": "",
|
"powerPackId": "",
|
||||||
"notes": ""
|
"notes": ""
|
||||||
|
|||||||
@@ -1,59 +1,193 @@
|
|||||||
<form class="ringworld-sheet creature-sheet" autocomplete="off">
|
<form class="ringworld-sheet creature-sheet" autocomplete="off">
|
||||||
<header class="sheet-header">
|
<header class="sheet-header rw-header">
|
||||||
<input name="name" type="text" value="{{actor.name}}" placeholder="Creature Name" />
|
<div class="rw-title-row">
|
||||||
<div class="resource-row">
|
<h1>Creature</h1>
|
||||||
<label>General HP</label>
|
|
||||||
<input type="number" name="system.generalHp.value" value="{{system.generalHp.value}}" />
|
|
||||||
<span>/ {{system.generalHp.max}}</span>
|
|
||||||
<button type="button" class="roll-hit-location">Roll Hit Location</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<input name="name" type="text" value="{{actor.name}}" placeholder="Creature Name" />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel">
|
<div class="rw-upper-grid">
|
||||||
<h3>Hit Locations</h3>
|
<div class="rw-left-stack">
|
||||||
<div class="location-list">
|
<section class="panel rw-profile">
|
||||||
{{#each hitLocations as |loc|}}
|
<label>Species / Kind
|
||||||
<div class="location-row">
|
<input type="text" name="system.profile.species" value="{{system.profile.species}}" />
|
||||||
<strong>{{loc.label}} <small>(d20 {{loc.rollRange}})</small></strong>
|
</label>
|
||||||
<label>HP <input type="number" name="system.hitLocations.{{loc.slug}}.hp.value" value="{{loc.hp.value}}" /></label>
|
<label>Hit Model
|
||||||
<span>/ {{loc.hp.max}}</span>
|
<select name="system.hitLocationSchema">
|
||||||
<label>Armor <input type="number" name="system.hitLocations.{{loc.slug}}.armor" value="{{loc.armor}}" /></label>
|
{{#each hitSchemas as |schema|}}
|
||||||
|
<option value="{{schema.id}}" {{#if (eq ../system.hitLocationSchema schema.id)}}selected{{/if}}>{{schema.label}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Move
|
||||||
|
<input type="number" name="system.derived.move" value="{{system.derived.move}}" />
|
||||||
|
</label>
|
||||||
|
<label>Action Ranking
|
||||||
|
<input type="number" value="{{system.derived.actionRanking}}" readonly />
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel rw-attribute-board">
|
||||||
|
<div class="attr-row">
|
||||||
|
<div class="lhs"><button type="button" class="roll-characteristic stat-roll-btn" data-char="str" data-label="STR">STR</button><input type="number" name="system.characteristics.str" value="{{system.characteristics.str}}" /></div>
|
||||||
|
<div class="rhs"><span>Damage Mod</span><input type="text" value="{{system.combat.damageMod}}" readonly /></div>
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
<div class="attr-row">
|
||||||
|
<div class="lhs"><button type="button" class="roll-characteristic stat-roll-btn" data-char="mas" data-label="MAS">MAS</button><input type="number" name="system.characteristics.mas" value="{{system.characteristics.mas}}" /></div>
|
||||||
|
<div class="rhs">
|
||||||
|
<span>General Hit Points</span>
|
||||||
|
<span class="inline-pair">
|
||||||
|
<input type="number" name="system.generalHp.value" value="{{system.generalHp.value}}" />
|
||||||
|
<span>/ {{system.generalHp.max}}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="attr-row">
|
||||||
|
<div class="lhs"><button type="button" class="roll-characteristic stat-roll-btn" data-char="con" data-label="CON">CON</button><input type="number" name="system.characteristics.con" value="{{system.characteristics.con}}" /></div>
|
||||||
|
<div class="rhs">
|
||||||
|
<span>Health Roll</span>
|
||||||
|
<span class="inline-pair">
|
||||||
|
<input type="number" value="{{system.derivedRolls.health}}" readonly />
|
||||||
|
<span>%</span>
|
||||||
|
<button type="button" class="roll-derived icon-btn" data-check="health" title="Roll Health" aria-label="Roll Health"><i class="fas fa-dice-d20"></i></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="attr-row">
|
||||||
|
<div class="lhs"><button type="button" class="roll-characteristic stat-roll-btn" data-char="int" data-label="INT">INT</button><input type="number" name="system.characteristics.int" value="{{system.characteristics.int}}" /></div>
|
||||||
|
<div class="rhs">
|
||||||
|
<span>Reasoning Roll</span>
|
||||||
|
<span class="inline-pair">
|
||||||
|
<input type="number" value="{{system.derivedRolls.reasoning}}" readonly />
|
||||||
|
<span>%</span>
|
||||||
|
<button type="button" class="roll-derived icon-btn" data-check="reasoning" title="Roll Reasoning" aria-label="Roll Reasoning"><i class="fas fa-dice-d20"></i></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="attr-row">
|
||||||
|
<div class="lhs"><button type="button" class="roll-characteristic stat-roll-btn" data-char="pow" data-label="POW">POW</button><input type="number" name="system.characteristics.pow" value="{{system.characteristics.pow}}" /></div>
|
||||||
|
<div class="rhs">
|
||||||
|
<span>Luck Roll</span>
|
||||||
|
<span class="inline-pair">
|
||||||
|
<input type="number" value="{{system.derivedRolls.luck}}" readonly />
|
||||||
|
<span>%</span>
|
||||||
|
<button type="button" class="roll-derived icon-btn" data-check="luck" title="Roll Luck" aria-label="Roll Luck"><i class="fas fa-dice-d20"></i></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="attr-row">
|
||||||
|
<div class="lhs"><button type="button" class="roll-characteristic stat-roll-btn" data-char="dex" data-label="DEX">DEX</button><input type="number" name="system.characteristics.dex" value="{{system.characteristics.dex}}" /></div>
|
||||||
|
<div class="rhs">
|
||||||
|
<span>Dodge Roll</span>
|
||||||
|
<span class="inline-pair">
|
||||||
|
<input type="number" value="{{system.derivedRolls.dodge}}" readonly />
|
||||||
|
<span>%</span>
|
||||||
|
<button type="button" class="roll-derived icon-btn" data-check="dodge" title="Roll Dodge" aria-label="Roll Dodge"><i class="fas fa-dice-d20"></i></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel rw-tools">
|
||||||
|
<label>Energy
|
||||||
|
<input type="number" name="system.resources.energy.value" value="{{system.resources.energy.value}}" />
|
||||||
|
</label>
|
||||||
|
<span>/</span>
|
||||||
|
<label>
|
||||||
|
<input type="number" name="system.resources.energy.max" value="{{system.resources.energy.max}}" />
|
||||||
|
</label>
|
||||||
|
<button type="button" class="reset-conditions">Reset Conditions</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel rw-notes">
|
||||||
|
<h3>Notes</h3>
|
||||||
|
<textarea name="system.notes.defects" rows="3">{{system.notes.defects}}</textarea>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
<div class="rw-right-stack">
|
||||||
|
<section class="panel rw-portrait-panel">
|
||||||
|
<img class="rw-portrait-img" src="{{actor.img}}" alt="{{actor.name}}" title="{{actor.name}}" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="rw-hit-toolbar">
|
||||||
|
<button type="button" class="roll-hit-location icon-btn" title="Roll Hit Location" aria-label="Roll Hit Location"><i class="fas fa-dice-d20"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="rw-hit-body schema-{{hitSchemaId}}">
|
||||||
|
{{#each hitLocations as |loc|}}
|
||||||
|
<div class="body-loc body-{{loc.slug}}">
|
||||||
|
<h4>{{loc.label}}</h4>
|
||||||
|
<span class="loc-roll">d20 {{loc.rollRange}}</span>
|
||||||
|
<label>AP <input type="number" name="system.hitLocations.{{loc.slug}}.armor" value="{{loc.armor}}" /></label>
|
||||||
|
<span class="loc-ap-total">AP Total: {{loc.apTotal}}</span>
|
||||||
|
<label>HP <input type="number" name="system.hitLocations.{{loc.slug}}.hp.value" value="{{loc.hp.value}}" /> / {{loc.hp.max}}</label>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="toolbar">
|
<div class="panel-header">
|
||||||
<button type="button" class="item-create" data-type="skill">Add Skill</button>
|
<h3>Weapons & Attacks</h3>
|
||||||
<button type="button" class="item-create" data-type="weapon">Add Weapon</button>
|
<button type="button" class="item-create" data-type="weapon">Add Weapon</button>
|
||||||
</div>
|
</div>
|
||||||
|
<ol class="item-list rw-weapon-list">
|
||||||
<h3>Skills</h3>
|
{{#each weapons as |item|}}
|
||||||
<ol class="item-list">
|
|
||||||
{{#each skills as |item|}}
|
|
||||||
<li class="item" data-item-id="{{item.id}}">
|
<li class="item" data-item-id="{{item.id}}">
|
||||||
<div>{{item.name}}</div>
|
<div class="item-title"><img class="item-icon" src="{{item.img}}" alt="" /><span>{{item.name}}</span></div>
|
||||||
<div>{{item.system.value}}%</div>
|
<div>{{item.system.weight}}kg</div>
|
||||||
|
<div>{{item.system.linkedSkill}}</div>
|
||||||
|
<div>{{item.system.damageFormula}}{{#if item.system.ammo.max}} <span class="rw-ammo-badge">{{item.system.ammo.value}}/{{item.system.ammo.max}}</span>{{/if}}</div>
|
||||||
|
<div>{{item.system.range.close}} / {{item.system.range.medium}} / {{item.system.range.long}}</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="item-roll-skill">Roll</button>
|
<button type="button" class="item-use-weapon icon-btn" title="Use Weapon" aria-label="Use Weapon"><i class="fas fa-dice-d20"></i></button>
|
||||||
<button type="button" class="item-edit">Edit</button>
|
{{#if item.system.ammo.max}}<button type="button" class="item-reload-weapon icon-btn" title="Reload" aria-label="Reload"><i class="fas fa-rotate"></i></button>{{/if}}
|
||||||
<button type="button" class="item-delete">Delete</button>
|
<button type="button" class="item-edit icon-btn" title="Edit" aria-label="Edit"><i class="fas fa-pen"></i></button>
|
||||||
|
<button type="button" class="item-delete icon-btn" title="Delete" aria-label="Delete"><i class="fas fa-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ol>
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
<h3>Weapons</h3>
|
<section class="panel">
|
||||||
<ol class="item-list">
|
<div class="panel-header">
|
||||||
{{#each weapons as |item|}}
|
<h3>Armor</h3>
|
||||||
|
<button type="button" class="item-create" data-type="armor">Add Armor</button>
|
||||||
|
</div>
|
||||||
|
<ol class="item-list rw-armor-list">
|
||||||
|
{{#each armors as |item|}}
|
||||||
<li class="item" data-item-id="{{item.id}}">
|
<li class="item" data-item-id="{{item.id}}">
|
||||||
<div>{{item.name}}</div>
|
<div class="item-title"><img class="item-icon" src="{{item.img}}" alt="" /><span>{{item.name}}</span></div>
|
||||||
<div>{{item.system.damageFormula}}</div>
|
<div>{{item.system.weight}}kg</div>
|
||||||
|
<div>Base AP {{item.system.value}}</div>
|
||||||
|
<div>{{#if item.system.equipped}}Equipped{{else}}Carried{{/if}}</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="item-use-weapon">Use</button>
|
<button type="button" class="item-edit icon-btn" title="Edit" aria-label="Edit"><i class="fas fa-pen"></i></button>
|
||||||
<button type="button" class="item-edit">Edit</button>
|
<button type="button" class="item-delete icon-btn" title="Delete" aria-label="Delete"><i class="fas fa-trash"></i></button>
|
||||||
<button type="button" class="item-delete">Delete</button>
|
</div>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h3>Skills</h3>
|
||||||
|
<button type="button" class="item-create" data-type="skill">Add Skill</button>
|
||||||
|
</div>
|
||||||
|
<ol class="item-list rw-creature-skill-list">
|
||||||
|
{{#each skills as |item|}}
|
||||||
|
<li class="item" data-item-id="{{item.id}}">
|
||||||
|
<div class="item-title"><img class="item-icon" src="{{item.img}}" alt="" /><span>{{item.name}}</span></div>
|
||||||
|
<div>{{item.system.value}}%</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button type="button" class="item-roll-skill icon-btn" title="Roll" aria-label="Roll"><i class="fas fa-dice-d20"></i></button>
|
||||||
|
<button type="button" class="item-edit icon-btn" title="Edit" aria-label="Edit"><i class="fas fa-pen"></i></button>
|
||||||
|
<button type="button" class="item-delete icon-btn" title="Delete" aria-label="Delete"><i class="fas fa-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<div class="rw-attribute-board">
|
<div class="rw-attribute-board">
|
||||||
<div class="attr-row">
|
<div class="attr-row">
|
||||||
<div class="lhs"><button type="button" class="roll-characteristic stat-roll-btn" data-char="str" data-label="STR">STR</button><input type="number" name="system.characteristics.str" value="{{system.characteristics.str}}" /></div>
|
<div class="lhs"><button type="button" class="roll-characteristic stat-roll-btn" data-char="str" data-label="STR">STR</button><input type="number" name="system.characteristics.str" value="{{system.characteristics.str}}" /></div>
|
||||||
<div class="rhs"><span>Damage Mod</span><input type="text" name="system.combat.damageMod" value="{{system.combat.damageMod}}" /></div>
|
<div class="rhs"><span>Damage Mod</span><input type="text" value="{{system.combat.damageMod}}" readonly /></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="attr-row">
|
<div class="attr-row">
|
||||||
<div class="lhs"><button type="button" class="roll-characteristic stat-roll-btn" data-char="mas" data-label="MAS">MAS</button><input type="number" name="system.characteristics.mas" value="{{system.characteristics.mas}}" /></div>
|
<div class="lhs"><button type="button" class="roll-characteristic stat-roll-btn" data-char="mas" data-label="MAS">MAS</button><input type="number" name="system.characteristics.mas" value="{{system.characteristics.mas}}" /></div>
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
<div class="rhs">
|
<div class="rhs">
|
||||||
<span>Health Roll</span>
|
<span>Health Roll</span>
|
||||||
<span class="inline-pair">
|
<span class="inline-pair">
|
||||||
<input type="number" name="system.derivedRolls.health" value="{{system.derivedRolls.health}}" />
|
<input type="number" value="{{system.derivedRolls.health}}" readonly />
|
||||||
<span>%</span>
|
<span>%</span>
|
||||||
<button type="button" class="roll-derived icon-btn" data-check="health" title="Roll Health" aria-label="Roll Health"><i class="fas fa-dice-d20"></i></button>
|
<button type="button" class="roll-derived icon-btn" data-check="health" title="Roll Health" aria-label="Roll Health"><i class="fas fa-dice-d20"></i></button>
|
||||||
</span>
|
</span>
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<div class="rhs">
|
<div class="rhs">
|
||||||
<span>Reasoning Roll</span>
|
<span>Reasoning Roll</span>
|
||||||
<span class="inline-pair">
|
<span class="inline-pair">
|
||||||
<input type="number" name="system.derivedRolls.reasoning" value="{{system.derivedRolls.reasoning}}" />
|
<input type="number" value="{{system.derivedRolls.reasoning}}" readonly />
|
||||||
<span>%</span>
|
<span>%</span>
|
||||||
<button type="button" class="roll-derived icon-btn" data-check="reasoning" title="Roll Reasoning" aria-label="Roll Reasoning"><i class="fas fa-dice-d20"></i></button>
|
<button type="button" class="roll-derived icon-btn" data-check="reasoning" title="Roll Reasoning" aria-label="Roll Reasoning"><i class="fas fa-dice-d20"></i></button>
|
||||||
</span>
|
</span>
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<div class="rhs">
|
<div class="rhs">
|
||||||
<span>Luck Roll</span>
|
<span>Luck Roll</span>
|
||||||
<span class="inline-pair">
|
<span class="inline-pair">
|
||||||
<input type="number" name="system.derivedRolls.luck" value="{{system.derivedRolls.luck}}" />
|
<input type="number" value="{{system.derivedRolls.luck}}" readonly />
|
||||||
<span>%</span>
|
<span>%</span>
|
||||||
<button type="button" class="roll-derived icon-btn" data-check="luck" title="Roll Luck" aria-label="Roll Luck"><i class="fas fa-dice-d20"></i></button>
|
<button type="button" class="roll-derived icon-btn" data-check="luck" title="Roll Luck" aria-label="Roll Luck"><i class="fas fa-dice-d20"></i></button>
|
||||||
</span>
|
</span>
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
<div class="rhs">
|
<div class="rhs">
|
||||||
<span>Dodge Roll</span>
|
<span>Dodge Roll</span>
|
||||||
<span class="inline-pair">
|
<span class="inline-pair">
|
||||||
<input type="number" name="system.derivedRolls.dodge" value="{{system.derivedRolls.dodge}}" />
|
<input type="number" value="{{system.derivedRolls.dodge}}" readonly />
|
||||||
<span>%</span>
|
<span>%</span>
|
||||||
<button type="button" class="roll-derived icon-btn" data-check="dodge" title="Roll Dodge" aria-label="Roll Dodge"><i class="fas fa-dice-d20"></i></button>
|
<button type="button" class="roll-derived icon-btn" data-check="dodge" title="Roll Dodge" aria-label="Roll Dodge"><i class="fas fa-dice-d20"></i></button>
|
||||||
</span>
|
</span>
|
||||||
@@ -426,10 +426,11 @@
|
|||||||
<div class="item-title"><img class="item-icon" src="{{item.img}}" alt="" /><span>{{item.name}}</span></div>
|
<div class="item-title"><img class="item-icon" src="{{item.img}}" alt="" /><span>{{item.name}}</span></div>
|
||||||
<div>{{item.system.weight}}kg</div>
|
<div>{{item.system.weight}}kg</div>
|
||||||
<div>{{#if item.system.stowed}}Stowed{{else}}{{#if item.system.equipped}}Equipped{{else}}Carried{{/if}}{{/if}} | {{item.system.linkedSkill}}</div>
|
<div>{{#if item.system.stowed}}Stowed{{else}}{{#if item.system.equipped}}Equipped{{else}}Carried{{/if}}{{/if}} | {{item.system.linkedSkill}}</div>
|
||||||
<div>{{item.system.damageFormula}}</div>
|
<div>{{item.system.damageFormula}}{{#if item.system.ammo.max}} <span class="rw-ammo-badge">{{item.system.ammo.value}}/{{item.system.ammo.max}}</span>{{/if}}</div>
|
||||||
<div>{{item.system.range.close}} / {{item.system.range.medium}} / {{item.system.range.long}}</div>
|
<div>{{item.system.range.close}} / {{item.system.range.medium}} / {{item.system.range.long}}</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="item-use-weapon icon-btn" title="Use Weapon" aria-label="Use Weapon"><i class="fas fa-dice-d20"></i></button>
|
<button type="button" class="item-use-weapon icon-btn" title="Use Weapon" aria-label="Use Weapon"><i class="fas fa-dice-d20"></i></button>
|
||||||
|
{{#if item.system.ammo.max}}<button type="button" class="item-reload-weapon icon-btn" title="Reload" aria-label="Reload"><i class="fas fa-rotate"></i></button>{{/if}}
|
||||||
<button type="button" class="item-toggle-equipped icon-btn" title="{{#if item.system.equipped}}Set Not Equipped{{else}}Set Equipped{{/if}}" aria-label="Toggle Equipped"><i class="fas fa-hand-paper"></i></button>
|
<button type="button" class="item-toggle-equipped icon-btn" title="{{#if item.system.equipped}}Set Not Equipped{{else}}Set Equipped{{/if}}" aria-label="Toggle Equipped"><i class="fas fa-hand-paper"></i></button>
|
||||||
<button type="button" class="item-toggle-stowed icon-btn" title="{{#if item.system.stowed}}Unstow{{else}}Stow{{/if}}" aria-label="Toggle Stowed"><i class="fas fa-box"></i></button>
|
<button type="button" class="item-toggle-stowed icon-btn" title="{{#if item.system.stowed}}Unstow{{else}}Stow{{/if}}" aria-label="Toggle Stowed"><i class="fas fa-box"></i></button>
|
||||||
<button type="button" class="item-edit icon-btn" title="Edit Weapon" aria-label="Edit Weapon"><i class="fas fa-pen"></i></button>
|
<button type="button" class="item-edit icon-btn" title="Edit Weapon" aria-label="Edit Weapon"><i class="fas fa-pen"></i></button>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<img class="rw-chat-actor-img" src="{{actorImg}}" alt="{{actorName}}" />
|
<img class="rw-chat-actor-img" src="{{actorImg}}" alt="{{actorName}}" />
|
||||||
<div class="rw-chat-header-text">
|
<div class="rw-chat-header-text">
|
||||||
<h3>{{actorName}} attacks {{targetName}}</h3>
|
<h3>{{actorName}} attacks {{targetName}}</h3>
|
||||||
<p class="rw-chat-outcome {{#if isSuccess}}success{{else}}failure{{/if}}">{{attackOutcomeLabel}}</p>
|
<p class="rw-chat-outcome {{#if isSuccess}}success{{else}}failure{{/if}} {{#if isSpecial}}special{{/if}} {{#if isFumble}}fumble{{/if}}">{{attackOutcomeLabel}}</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -20,6 +20,14 @@
|
|||||||
|
|
||||||
<p><strong>Attack:</strong> {{attackRoll}} vs {{attackTarget}}</p>
|
<p><strong>Attack:</strong> {{attackRoll}} vs {{attackTarget}}</p>
|
||||||
|
|
||||||
|
{{#if dodge}}
|
||||||
|
<p class="rw-chat-hitline rw-chat-dodge {{#if dodge.negated}}dodged{{/if}}">
|
||||||
|
<span class="rw-chat-hit-label">Dodge</span>
|
||||||
|
<span class="rw-chat-hit-roll">{{dodge.roll}} vs {{dodge.target}} —</span>
|
||||||
|
<span class="rw-chat-hit-value">{{#if dodge.negated}}Evaded{{else}}{{#if dodge.success}}Not enough{{else}}Failed{{/if}}{{/if}}</span>
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if isSuccess}}
|
{{#if isSuccess}}
|
||||||
<p class="rw-chat-hitline">
|
<p class="rw-chat-hitline">
|
||||||
<span class="rw-chat-hit-label">Hit Location</span>
|
<span class="rw-chat-hit-label">Hit Location</span>
|
||||||
@@ -28,7 +36,7 @@
|
|||||||
<p class="rw-chat-damage-line">
|
<p class="rw-chat-damage-line">
|
||||||
<span class="rw-chat-damage-label">Total Damage</span>
|
<span class="rw-chat-damage-label">Total Damage</span>
|
||||||
<span class="rw-chat-damage-value">{{finalDamage}}</span>
|
<span class="rw-chat-damage-value">{{finalDamage}}</span>
|
||||||
<span class="rw-chat-damage-breakdown">({{rawDamage}} - {{armor}})</span>
|
<span class="rw-chat-damage-breakdown">({{rawDamage}} - {{armor}} armor)</span>
|
||||||
</p>
|
</p>
|
||||||
{{#if hasTarget}}
|
{{#if hasTarget}}
|
||||||
<p><strong>Remaining Location HP:</strong> {{remainingLocationHp}}</p>
|
<p><strong>Remaining Location HP:</strong> {{remainingLocationHp}}</p>
|
||||||
@@ -36,6 +44,11 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<p><strong>Energy:</strong> {{energySourceLabel}} spent {{energyCost}}, remaining {{energyRemaining}}</p>
|
{{#if energyCost}}
|
||||||
|
<p><strong>Energy:</strong> {{energySourceLabel}} spent {{energyCost}}, remaining {{energyRemaining}}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if ammoUsed}}
|
||||||
|
<p><strong>Ammo:</strong> spent {{ammoCost}}, remaining {{ammoRemaining}}</p>
|
||||||
|
{{/if}}
|
||||||
<p class="rw-chat-notes">{{notes}}</p>
|
<p class="rw-chat-notes">{{notes}}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@
|
|||||||
<label>Range Long <input type="text" name="system.range.long" value="{{system.range.long}}" /></label>
|
<label>Range Long <input type="text" name="system.range.long" value="{{system.range.long}}" /></label>
|
||||||
<label>Energy Used / Attack <input type="number" name="system.energyUsedPerAttack" value="{{system.energyUsedPerAttack}}" /></label>
|
<label>Energy Used / Attack <input type="number" name="system.energyUsedPerAttack" value="{{system.energyUsedPerAttack}}" /></label>
|
||||||
<label>Ammo Used / Attack <input type="number" name="system.ammoUsedPerAttack" value="{{system.ammoUsedPerAttack}}" /></label>
|
<label>Ammo Used / Attack <input type="number" name="system.ammoUsedPerAttack" value="{{system.ammoUsedPerAttack}}" /></label>
|
||||||
|
<label>Ammo Loaded <input type="number" name="system.ammo.value" value="{{system.ammo.value}}" /></label>
|
||||||
|
<label>Ammo Max <input type="number" name="system.ammo.max" value="{{system.ammo.max}}" /></label>
|
||||||
<label>Power Source
|
<label>Power Source
|
||||||
<select name="system.powerSource">
|
<select name="system.powerSource">
|
||||||
<option value="actor" {{#if (eq system.powerSource "actor")}}selected{{/if}}>Actor Energy Pool</option>
|
<option value="actor" {{#if (eq system.powerSource "actor")}}selected{{/if}}>Actor Energy Pool</option>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
"title": "Ringworld Starter",
|
"title": "Ringworld Starter",
|
||||||
"id": "ringworld-starter",
|
"id": "ringworld-starter",
|
||||||
"system": "ringworld",
|
"system": "ringworld",
|
||||||
"coreVersion": "13.351",
|
"coreVersion": "14.364",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "12",
|
"minimum": "13",
|
||||||
"verified": "13"
|
"verified": "14"
|
||||||
},
|
},
|
||||||
"systemVersion": "0.1.0",
|
"systemVersion": "0.5.0",
|
||||||
"description": "Starter world shell for Ringworld. Use Ringworld Content module import button to populate catalog content.",
|
"description": "Starter world shell for Ringworld. Use Ringworld Content module import button to populate catalog content.",
|
||||||
"lastPlayed": "",
|
"lastPlayed": "",
|
||||||
"flags": {}
|
"flags": {}
|
||||||
|
|||||||
Reference in New Issue
Block a user