Add development tooling: ESLint, Prettier, Husky, and Quench tests

Set up complete development environment with:
- ESLint with Foundry VTT globals (game, CONFIG, Actor, etc.)
- Prettier for consistent code formatting
- Husky + lint-staged for pre-commit hooks
- Quench test framework structure with sanity checks

Documentation:
- DEVELOPMENT.md with tooling decisions and rationale
- README.md updated with development setup instructions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-12-12 14:47:14 -06:00
parent 37300ccf90
commit 44dbd00e1b
19 changed files with 3331 additions and 110 deletions

118
.eslintrc.json Normal file
View File

@ -0,0 +1,118 @@
{
"env": {
"browser": true,
"es2022": true
},
"extends": ["eslint:recommended"],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"globals": {
"game": "readonly",
"canvas": "readonly",
"ui": "readonly",
"CONFIG": "readonly",
"CONST": "readonly",
"Hooks": "readonly",
"Application": "readonly",
"FormApplication": "readonly",
"ActorSheet": "readonly",
"ItemSheet": "readonly",
"Actor": "readonly",
"Item": "readonly",
"ChatMessage": "readonly",
"Roll": "readonly",
"Dialog": "readonly",
"TextEditor": "readonly",
"FilePicker": "readonly",
"Handlebars": "readonly",
"foundry": "readonly",
"renderTemplate": "readonly",
"loadTemplates": "readonly",
"getTemplate": "readonly",
"fromUuid": "readonly",
"fromUuidSync": "readonly",
"duplicate": "readonly",
"mergeObject": "readonly",
"setProperty": "readonly",
"getProperty": "readonly",
"hasProperty": "readonly",
"expandObject": "readonly",
"flattenObject": "readonly",
"isObjectEmpty": "readonly",
"invertObject": "readonly",
"filterObject": "readonly",
"diffObject": "readonly",
"randomID": "readonly",
"debounce": "readonly",
"deepClone": "readonly",
"isEmpty": "readonly",
"getType": "readonly",
"ActiveEffect": "readonly",
"Token": "readonly",
"TokenDocument": "readonly",
"Scene": "readonly",
"User": "readonly",
"Folder": "readonly",
"Compendium": "readonly",
"CompendiumCollection": "readonly",
"DocumentSheetConfig": "readonly",
"ContextMenu": "readonly",
"DragDrop": "readonly",
"SearchFilter": "readonly",
"Tabs": "readonly",
"quench": "readonly"
},
"rules": {
"no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"no-shadow": [
"error",
{
"builtinGlobals": true,
"hoist": "all",
"allow": ["event", "name", "status", "parent", "top", "close", "open", "print"]
}
],
"no-var": "error",
"prefer-const": "warn",
"eqeqeq": ["error", "smart"],
"curly": ["error", "multi-line"],
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-debugger": "warn",
"no-duplicate-imports": "error",
"no-template-curly-in-string": "warn",
"no-unreachable-loop": "error",
"no-use-before-define": [
"error",
{
"functions": false,
"classes": true,
"variables": true
}
],
"arrow-body-style": ["warn", "as-needed"],
"no-else-return": "warn",
"no-lonely-if": "warn",
"no-unneeded-ternary": "warn",
"prefer-arrow-callback": "warn",
"prefer-template": "warn",
"object-shorthand": "warn",
"spaced-comment": ["warn", "always", { "markers": ["/"] }]
},
"overrides": [
{
"files": ["**/*.test.mjs", "**/tests/**/*.mjs"],
"rules": {
"no-unused-expressions": "off"
}
}
],
"ignorePatterns": ["node_modules/", "foundrydata/", "styles/*.css", "*.min.js"]
}

1
.husky/pre-commit Executable file
View File

@ -0,0 +1 @@
npx lint-staged

27
.prettierignore Normal file
View File

@ -0,0 +1,27 @@
# Dependencies
node_modules/
# Foundry data
foundrydata/
# Build artifacts
styles/*.css
styles/*.css.map
# Package lock
package-lock.json
# Compendium packs (LevelDB format)
packs/*/*.db
packs/*/CURRENT
packs/*/LOCK
packs/*/LOG*
packs/*/MANIFEST*
# IDE
.idea/
.vscode/
# Minified files
*.min.js
*.min.css

34
.prettierrc Normal file
View File

@ -0,0 +1,34 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf",
"overrides": [
{
"files": "*.json",
"options": {
"tabWidth": 2
}
},
{
"files": "*.hbs",
"options": {
"parser": "html",
"printWidth": 120
}
},
{
"files": "*.scss",
"options": {
"singleQuote": false
}
}
]
}

View File

@ -1,42 +1,49 @@
# Vagabond RPG Foundry VTT System - Development Context # Vagabond RPG Foundry VTT System - Development Context
## Project Overview ## Project Overview
This is a complete Foundry VTT v13 system implementation for Vagabond RPG (Pulp Fantasy TTRPG). This is a complete Foundry VTT v13 system implementation for Vagabond RPG (Pulp Fantasy TTRPG).
## Key Architecture Decisions ## Key Architecture Decisions
### Data Models (Foundry v13 style) ### Data Models (Foundry v13 style)
- Use TypeDataModel classes in `module/data/` for Actor and Item schemas - Use TypeDataModel classes in `module/data/` for Actor and Item schemas
- Character stats: Might, Dexterity, Awareness, Reason, Presence, Luck (range 2-7) - Character stats: Might, Dexterity, Awareness, Reason, Presence, Luck (range 2-7)
- Derived values calculated in `prepareData()`: HP, Speed, Save difficulties, Skill difficulties - Derived values calculated in `prepareData()`: HP, Speed, Save difficulties, Skill difficulties
### Roll System ### Roll System
- Base formula: d20 >= (20 - Stat) for untrained, d20 >= (20 - Stat*2) for trained
- Base formula: d20 >= (20 - Stat) for untrained, d20 >= (20 - Stat\*2) for trained
- Favor: +d6 bonus die - Favor: +d6 bonus die
- Hinder: -d6 penalty die - Hinder: -d6 penalty die
- Crit: Natural 20 by default, but threshold can be modified per-skill by Active Effects - Crit: Natural 20 by default, but threshold can be modified per-skill by Active Effects
- Exploding dice: d6! notation for certain abilities - Exploding dice: d6! notation for certain abilities
### Spell Casting ### Spell Casting
- Dynamic mana cost = base + delivery cost + duration cost + extra damage dice - Dynamic mana cost = base + delivery cost + duration cost + extra damage dice
- Delivery types: Touch(0), Remote(0), Imbue(0), Cube(1), Aura(2), Cone(2), Glyph(2), Line(2), Sphere(2) - Delivery types: Touch(0), Remote(0), Imbue(0), Cube(1), Aura(2), Cone(2), Glyph(2), Line(2), Sphere(2)
- Duration: Instant (free), Focus (ongoing), Continual (permanent) - Duration: Instant (free), Focus (ongoing), Continual (permanent)
- Cast dialog must calculate and display total mana cost before casting - Cast dialog must calculate and display total mana cost before casting
### Class System ### Class System
- Classes are Items with progression tables - Classes are Items with progression tables
- When dragged to character, creates Active Effects for current level - When dragged to character, creates Active Effects for current level
- On level up, update Active Effects to grant new features - On level up, update Active Effects to grant new features
- Supports future multiclassing by allowing multiple class items - Supports future multiclassing by allowing multiple class items
### Crit Threshold System ### Crit Threshold System
- Each skill/action has a `critThreshold` field (default 20) - Each skill/action has a `critThreshold` field (default 20)
- Active Effects from classes/perks can modify: `system.skills.melee.critThreshold` - Active Effects from classes/perks can modify: `system.skills.melee.critThreshold`
- Fighter's Valor reduces crit by 1/2/3 at levels 1/4/8 - Fighter's Valor reduces crit by 1/2/3 at levels 1/4/8
- Gunslinger's Deadeye dynamically reduces on consecutive hits - Gunslinger's Deadeye dynamically reduces on consecutive hits
### Resources ### Resources
- HP: max = Might * Level
- HP: max = Might \* Level
- Mana: class-dependent, max from class progression - Mana: class-dependent, max from class progression
- Luck: equals Luck stat, refreshes on rest - Luck: equals Luck stat, refreshes on rest
- Fatigue: 0-5, death at 5, each reduces item slots by 1 - Fatigue: 0-5, death at 5, each reduces item slots by 1
@ -44,6 +51,7 @@ This is a complete Foundry VTT v13 system implementation for Vagabond RPG (Pulp
- Custom resources can be added dynamically - Custom resources can be added dynamically
## File Naming Conventions ## File Naming Conventions
- Main system entry: `vagabond.mjs` - Main system entry: `vagabond.mjs`
- Document classes: `VagabondActor.mjs`, `VagabondItem.mjs` - Document classes: `VagabondActor.mjs`, `VagabondItem.mjs`
- Sheet classes: `VagabondCharacterSheet.mjs`, `VagabondNPCSheet.mjs` - Sheet classes: `VagabondCharacterSheet.mjs`, `VagabondNPCSheet.mjs`
@ -51,6 +59,7 @@ This is a complete Foundry VTT v13 system implementation for Vagabond RPG (Pulp
- Templates: `character-sheet.hbs`, `npc-sheet.hbs`, `spell-item.hbs` - Templates: `character-sheet.hbs`, `npc-sheet.hbs`, `spell-item.hbs`
## Testing Commands ## Testing Commands
```bash ```bash
# Start local Foundry # Start local Foundry
docker compose up -d docker compose up -d
@ -63,7 +72,9 @@ docker compose logs -f foundry
``` ```
## Reference Data Location ## Reference Data Location
Game rules and content are documented in NoteDiscovery under `gaming/vagabond-rpg/`: Game rules and content are documented in NoteDiscovery under `gaming/vagabond-rpg/`:
- `core-mechanics.md` - Stats, checks, dice, HP - `core-mechanics.md` - Stats, checks, dice, HP
- `combat.md` - Actions, movement, defending, zones - `combat.md` - Actions, movement, defending, zones
- `character-creation.md` - Ancestries, classes, leveling - `character-creation.md` - Ancestries, classes, leveling
@ -77,9 +88,11 @@ Original PDF at: `/mnt/NV2/Development/claude-home/gaming/Vagabond_RPG_-_Pulp_Fa
Character sheet reference: `/mnt/NV2/Development/claude-home/gaming/Vagabond_-_Hero_Record_Interactive_PDF.pdf` Character sheet reference: `/mnt/NV2/Development/claude-home/gaming/Vagabond_-_Hero_Record_Interactive_PDF.pdf`
## Project Roadmap ## Project Roadmap
See `PROJECT_ROADMAP.json` for complete task breakdown with dependencies. See `PROJECT_ROADMAP.json` for complete task breakdown with dependencies.
## Style Guidelines ## Style Guidelines
- Parchment color scheme with high contrast (WCAG AA compliant) - Parchment color scheme with high contrast (WCAG AA compliant)
- Match official Hero Record layout where possible - Match official Hero Record layout where possible
- Use CSS custom properties for theming - Use CSS custom properties for theming

299
DEVELOPMENT.md Normal file
View File

@ -0,0 +1,299 @@
# Development Guide
This document outlines the development toolchain, conventions, and testing strategy for the Vagabond RPG Foundry VTT system.
## Tooling Decisions
### Why These Tools?
| Tool | Decision | Rationale |
| ------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **ESLint** | ✅ Adopted | Industry standard for JavaScript linting. Catches bugs before runtime, enforces consistent patterns. |
| **@typhonjs-fvtt/eslint-config-foundry.js** | ✅ Adopted | Foundry-specific ESLint plugin that knows about Foundry globals (`game`, `CONFIG`, `canvas`, etc.). Prevents accidentally shadowing core APIs. |
| **Prettier** | ✅ Adopted | Opinionated formatter eliminates style debates. Consistent code regardless of contributor. |
| **Husky + lint-staged** | ✅ Adopted | Pre-commit hooks enforce quality gates. No unlinted code enters the repo. |
| **Quench** | ✅ Adopted | In-Foundry testing module powered by Mocha + Chai. Tests run with real Foundry API access - essential for testing sheets, rolls, and Active Effects. |
| **Vitest/Jest** | ❌ Deferred | Most system logic requires Foundry context. Pure unit tests have limited value here. May add later for formula calculations. |
| **TypeScript** | ❌ Deferred | Adds complexity for a solo/small team project. JSDoc comments provide type hints without build step overhead. May migrate later. |
| **Rollup/Vite** | ❌ Deferred | Foundry v13 has excellent ESM support. No bundling needed for our use case. SCSS compilation via sass CLI is sufficient. |
### Alternatives Considered
**Testing:**
- **Vitest with foundry-test-utils**: Provides mocked Foundry environment (~600 lines of mocks). Rejected because mocks drift from real API, and Quench gives us actual Foundry context.
- **Cypress/Playwright**: E2E browser testing. Overkill for a game system; Quench is purpose-built for Foundry.
**Linting:**
- **Biome**: Faster than ESLint+Prettier combined. Rejected because no Foundry-specific config exists yet.
- **Standard JS**: Zero-config linting. Rejected because we need Foundry global awareness.
**Build:**
- **Gulp**: Used by SR5-FoundryVTT. Rejected as unnecessary complexity; npm scripts + sass CLI suffice.
- **TyphonJS Svelte/Vite**: Modern but opinionated toward Svelte. We're using vanilla Handlebars.
---
## Setup
### Prerequisites
- Node.js 18+ (v20 has known issues with some Foundry tooling)
- Docker & Docker Compose
- Git
### Initial Setup
```bash
# Clone repository
git clone git@github.com:calcorum/vagabond-rpg-foundryvtt.git
cd vagabond-rpg-foundryvtt
# Install dependencies
npm install
# Copy environment file and add your Foundry credentials
cp .env.example .env
# Edit .env with your FOUNDRY_USERNAME, FOUNDRY_PASSWORD, FOUNDRY_LICENSE_KEY
# Start local Foundry instance
docker compose up -d
# Start SCSS watcher (in separate terminal)
npm run watch
# Access Foundry at http://localhost:30000
```
### Available Scripts
| Command | Description |
| ---------------------- | ------------------------------------------- |
| `npm run build` | Compile SCSS to CSS (production) |
| `npm run watch` | Watch SCSS and recompile on changes |
| `npm run lint` | Run ESLint on all JavaScript |
| `npm run lint:fix` | Auto-fix ESLint issues where possible |
| `npm run format` | Run Prettier on all files |
| `npm run format:check` | Check if files are formatted (CI) |
| `npm test` | Run Quench tests (requires running Foundry) |
---
## Code Style
### ESLint Rules
We use a Foundry-aware ESLint configuration that:
1. **Knows Foundry globals**: `game`, `CONFIG`, `canvas`, `ui`, `Hooks`, `Actor`, `Item`, etc. are recognized
2. **Prevents shadowing**: You can't accidentally create a local `game` variable
3. **Enforces ES2022+**: Modern JavaScript features encouraged
4. **Warns on common mistakes**: Unused variables, undefined references, etc.
### Prettier Configuration
```json
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always"
}
```
**Why these choices:**
- `printWidth: 100` - Balances readability with modern wide screens
- `semi: true` - Explicit semicolons prevent ASI edge cases
- `singleQuote: false` - Matches Foundry core style
- `trailingComma: "es5"` - Cleaner diffs, valid ES5+
### Pre-commit Hooks
Husky runs on every commit:
1. **lint-staged**: Runs ESLint and Prettier only on staged files
2. **Blocks commits** if linting fails or files aren't formatted
To bypass (use sparingly):
```bash
git commit --no-verify -m "emergency fix"
```
---
## Testing Strategy
### Quench Integration Tests
[Quench](https://github.com/Ethaks/FVTT-Quench) runs tests inside Foundry with full API access.
**What to test:**
- ✅ Data model validation (Actor/Item creation)
- ✅ Derived value calculations (HP, Speed, difficulties)
- ✅ Roll formula generation
- ✅ Active Effect application
- ✅ Sheet rendering (no JS errors)
- ✅ Compendium item import
**What NOT to test:**
- ❌ Foundry core functionality
- ❌ Third-party module interactions
- ❌ UI pixel-perfect rendering
### Test File Location
```
module/
├── tests/
│ ├── quench-init.mjs # Registers test batches with Quench
│ ├── actor.test.mjs # Actor data model tests
│ ├── item.test.mjs # Item data model tests
│ ├── rolls.test.mjs # Dice roll tests
│ └── effects.test.mjs # Active Effects tests
```
### Writing Tests
```javascript
// module/tests/actor.test.mjs
export function registerActorTests(quench) {
quench.registerBatch(
"vagabond.actors",
(context) => {
const { describe, it, expect } = context;
describe("Character Actor", () => {
it("calculates HP as Might × Level", async () => {
const actor = await Actor.create({
name: "Test Character",
type: "character",
system: { stats: { might: { value: 5 } }, level: 3 },
});
expect(actor.system.resources.hp.max).to.equal(15);
await actor.delete();
});
});
},
{ displayName: "Vagabond: Actor Tests" }
);
}
```
### Running Tests
1. Install the Quench module in Foundry (Module Management)
2. Enable Quench in your world
3. Click the flask icon in the scene controls
4. Select "Vagabond" test batches
5. Click "Run"
Or via macro:
```javascript
quench.runBatches("vagabond.*");
```
---
## Project Structure
```
vagabond-rpg-foundryvtt/
├── .husky/ # Git hooks
├── module/ # JavaScript source
│ ├── vagabond.mjs # System entry point
│ ├── data/ # Data models (TypeDataModel classes)
│ ├── documents/ # Document classes (VagabondActor, VagabondItem)
│ ├── sheets/ # Sheet classes
│ ├── helpers/ # Utility functions
│ ├── dice/ # Roll handling
│ └── tests/ # Quench test files
├── templates/ # Handlebars templates
├── styles/ # SCSS source and compiled CSS
├── lang/ # Localization
├── packs/ # Compendium data
├── assets/ # Images and icons
├── system.json # Foundry manifest
├── .eslintrc.json # ESLint configuration
├── .prettierrc # Prettier configuration
└── package.json # npm dependencies and scripts
```
---
## Continuous Integration
### GitHub Actions (Future)
When we set up CI, it will:
1. **On Pull Request:**
- Run `npm run lint`
- Run `npm run format:check`
- Verify SCSS compiles without errors
2. **On Release Tag:**
- Build production CSS
- Create release zip
- Publish to GitHub Releases
---
## Contributing
### Commit Message Format
```
<type>: <short description>
[optional body]
[optional footer]
```
**Types:**
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation only
- `style`: Formatting, no code change
- `refactor`: Code change that neither fixes nor adds
- `test`: Adding tests
- `chore`: Build/tooling changes
**Examples:**
```
feat: Add spell casting dialog with mana calculator
fix: Correct HP calculation for characters with Tough perk
docs: Update README with installation instructions
```
### Branch Naming
- `feature/spell-casting-dialog`
- `fix/hp-calculation`
- `docs/readme-update`
---
## Resources
- [Foundry VTT API Documentation](https://foundryvtt.com/api/)
- [Foundry Community Wiki](https://foundryvtt.wiki/)
- [Quench Testing Module](https://github.com/Ethaks/FVTT-Quench)
- [TyphonJS ESLint Config](https://github.com/typhonjs-fvtt/eslint-config-foundry.js)
- [Vagabond RPG Rules (NoteDiscovery)](https://notes.manticorum.com) - gaming/vagabond-rpg/

View File

@ -15,6 +15,7 @@ A complete Foundry VTT v13 system implementation for **Vagabond RPG** - Pulp Fan
## Installation ## Installation
### From Foundry ### From Foundry
1. Open Foundry VTT Setup 1. Open Foundry VTT Setup
2. Navigate to Game Systems 2. Navigate to Game Systems
3. Click "Install System" 3. Click "Install System"
@ -24,6 +25,7 @@ A complete Foundry VTT v13 system implementation for **Vagabond RPG** - Pulp Fan
``` ```
### Manual Installation ### Manual Installation
1. Download the latest release from GitHub 1. Download the latest release from GitHub
2. Extract to `Data/systems/vagabond/` 2. Extract to `Data/systems/vagabond/`
3. Restart Foundry VTT 3. Restart Foundry VTT
@ -31,10 +33,12 @@ A complete Foundry VTT v13 system implementation for **Vagabond RPG** - Pulp Fan
## Development Setup ## Development Setup
### Prerequisites ### Prerequisites
- Node.js 18+ - Node.js 18+
- Docker & Docker Compose (for local Foundry instance) - Docker & Docker Compose (for local Foundry instance)
### Quick Start ### Quick Start
```bash ```bash
# Clone the repository # Clone the repository
git clone https://github.com/calcorum/vagabond-rpg-foundryvtt.git git clone https://github.com/calcorum/vagabond-rpg-foundryvtt.git
@ -53,6 +57,7 @@ docker compose up -d
``` ```
### Project Structure ### Project Structure
``` ```
vagabond-rpg-foundryvtt/ vagabond-rpg-foundryvtt/
├── module/ # JavaScript modules ├── module/ # JavaScript modules
@ -76,6 +81,7 @@ vagabond-rpg-foundryvtt/
``` ```
### Building Styles ### Building Styles
```bash ```bash
# One-time build # One-time build
npm run build npm run build
@ -85,6 +91,7 @@ npm run watch
``` ```
### Creating a Release ### Creating a Release
```bash ```bash
npm run release npm run release
``` ```

View File

@ -14,7 +14,7 @@ VAGABOND.stats = {
awareness: "VAGABOND.StatAwareness", awareness: "VAGABOND.StatAwareness",
reason: "VAGABOND.StatReason", reason: "VAGABOND.StatReason",
presence: "VAGABOND.StatPresence", presence: "VAGABOND.StatPresence",
luck: "VAGABOND.StatLuck" luck: "VAGABOND.StatLuck",
}; };
/** /**
@ -26,7 +26,7 @@ VAGABOND.statsAbbr = {
awareness: "VAGABOND.StatAwarenessAbbr", awareness: "VAGABOND.StatAwarenessAbbr",
reason: "VAGABOND.StatReasonAbbr", reason: "VAGABOND.StatReasonAbbr",
presence: "VAGABOND.StatPresenceAbbr", presence: "VAGABOND.StatPresenceAbbr",
luck: "VAGABOND.StatLuckAbbr" luck: "VAGABOND.StatLuckAbbr",
}; };
/** /**
@ -44,7 +44,7 @@ VAGABOND.skills = {
mysticism: { label: "VAGABOND.SkillMysticism", stat: "awareness" }, mysticism: { label: "VAGABOND.SkillMysticism", stat: "awareness" },
performance: { label: "VAGABOND.SkillPerformance", stat: "presence" }, performance: { label: "VAGABOND.SkillPerformance", stat: "presence" },
sneak: { label: "VAGABOND.SkillSneak", stat: "dexterity" }, sneak: { label: "VAGABOND.SkillSneak", stat: "dexterity" },
survival: { label: "VAGABOND.SkillSurvival", stat: "awareness" } survival: { label: "VAGABOND.SkillSurvival", stat: "awareness" },
}; };
/** /**
@ -54,7 +54,7 @@ VAGABOND.attackTypes = {
melee: { label: "VAGABOND.AttackMelee", stat: "might" }, melee: { label: "VAGABOND.AttackMelee", stat: "might" },
brawl: { label: "VAGABOND.AttackBrawl", stat: "might" }, brawl: { label: "VAGABOND.AttackBrawl", stat: "might" },
ranged: { label: "VAGABOND.AttackRanged", stat: "awareness" }, ranged: { label: "VAGABOND.AttackRanged", stat: "awareness" },
finesse: { label: "VAGABOND.AttackFinesse", stat: "dexterity" } finesse: { label: "VAGABOND.AttackFinesse", stat: "dexterity" },
}; };
/** /**
@ -63,7 +63,7 @@ VAGABOND.attackTypes = {
VAGABOND.saves = { VAGABOND.saves = {
reflex: { label: "VAGABOND.SaveReflex", stats: ["dexterity", "awareness"] }, reflex: { label: "VAGABOND.SaveReflex", stats: ["dexterity", "awareness"] },
endure: { label: "VAGABOND.SaveEndure", stats: ["might", "might"] }, endure: { label: "VAGABOND.SaveEndure", stats: ["might", "might"] },
will: { label: "VAGABOND.SaveWill", stats: ["reason", "presence"] } will: { label: "VAGABOND.SaveWill", stats: ["reason", "presence"] },
}; };
/** /**
@ -78,7 +78,7 @@ VAGABOND.spellDelivery = {
cone: { label: "VAGABOND.DeliveryCone", cost: 2 }, cone: { label: "VAGABOND.DeliveryCone", cost: 2 },
glyph: { label: "VAGABOND.DeliveryGlyph", cost: 2 }, glyph: { label: "VAGABOND.DeliveryGlyph", cost: 2 },
line: { label: "VAGABOND.DeliveryLine", cost: 2 }, line: { label: "VAGABOND.DeliveryLine", cost: 2 },
sphere: { label: "VAGABOND.DeliverySphere", cost: 2 } sphere: { label: "VAGABOND.DeliverySphere", cost: 2 },
}; };
/** /**
@ -87,7 +87,7 @@ VAGABOND.spellDelivery = {
VAGABOND.spellDuration = { VAGABOND.spellDuration = {
instant: { label: "VAGABOND.DurationInstant", focus: false }, instant: { label: "VAGABOND.DurationInstant", focus: false },
focus: { label: "VAGABOND.DurationFocus", focus: true }, focus: { label: "VAGABOND.DurationFocus", focus: true },
continual: { label: "VAGABOND.DurationContinual", focus: false } continual: { label: "VAGABOND.DurationContinual", focus: false },
}; };
/** /**
@ -101,7 +101,7 @@ VAGABOND.damageTypes = {
cold: "VAGABOND.DamageCold", cold: "VAGABOND.DamageCold",
shock: "VAGABOND.DamageShock", shock: "VAGABOND.DamageShock",
poison: "VAGABOND.DamagePoison", poison: "VAGABOND.DamagePoison",
acid: "VAGABOND.DamageAcid" acid: "VAGABOND.DamageAcid",
}; };
/** /**
@ -115,7 +115,7 @@ VAGABOND.weaponProperties = {
loading: "VAGABOND.PropertyLoading", loading: "VAGABOND.PropertyLoading",
brawl: "VAGABOND.PropertyBrawl", brawl: "VAGABOND.PropertyBrawl",
crude: "VAGABOND.PropertyCrude", crude: "VAGABOND.PropertyCrude",
versatile: "VAGABOND.PropertyVersatile" versatile: "VAGABOND.PropertyVersatile",
}; };
/** /**
@ -125,7 +125,7 @@ VAGABOND.gripTypes = {
"1h": "VAGABOND.Grip1H", "1h": "VAGABOND.Grip1H",
"2h": "VAGABOND.Grip2H", "2h": "VAGABOND.Grip2H",
versatile: "VAGABOND.GripVersatile", versatile: "VAGABOND.GripVersatile",
fist: "VAGABOND.GripFist" fist: "VAGABOND.GripFist",
}; };
/** /**
@ -134,7 +134,7 @@ VAGABOND.gripTypes = {
VAGABOND.armorTypes = { VAGABOND.armorTypes = {
light: "VAGABOND.ArmorLight", light: "VAGABOND.ArmorLight",
heavy: "VAGABOND.ArmorHeavy", heavy: "VAGABOND.ArmorHeavy",
shield: "VAGABOND.ArmorShield" shield: "VAGABOND.ArmorShield",
}; };
/** /**
@ -146,7 +146,7 @@ VAGABOND.sizes = {
large: "VAGABOND.SizeLarge", large: "VAGABOND.SizeLarge",
huge: "VAGABOND.SizeHuge", huge: "VAGABOND.SizeHuge",
giant: "VAGABOND.SizeGiant", giant: "VAGABOND.SizeGiant",
colossal: "VAGABOND.SizeColossal" colossal: "VAGABOND.SizeColossal",
}; };
/** /**
@ -160,7 +160,7 @@ VAGABOND.beingTypes = {
beast: "VAGABOND.BeingBeast", beast: "VAGABOND.BeingBeast",
outer: "VAGABOND.BeingOuter", outer: "VAGABOND.BeingOuter",
primordial: "VAGABOND.BeingPrimordial", primordial: "VAGABOND.BeingPrimordial",
undead: "VAGABOND.BeingUndead" undead: "VAGABOND.BeingUndead",
}; };
/** /**
@ -169,7 +169,7 @@ VAGABOND.beingTypes = {
VAGABOND.zones = { VAGABOND.zones = {
frontline: "VAGABOND.ZoneFrontline", frontline: "VAGABOND.ZoneFrontline",
midline: "VAGABOND.ZoneMidline", midline: "VAGABOND.ZoneMidline",
backline: "VAGABOND.ZoneBackline" backline: "VAGABOND.ZoneBackline",
}; };
/** /**
@ -186,7 +186,7 @@ VAGABOND.speedByDex = {
4: 30, 4: 30,
5: 30, 5: 30,
6: 35, 6: 35,
7: 35 7: 35,
}; };
/** /**

165
module/tests/actor.test.mjs Normal file
View File

@ -0,0 +1,165 @@
/**
* Actor Data Model Tests
*
* Tests for VagabondActor class and character/NPC data models.
* These tests verify derived value calculations, resource tracking,
* and data integrity.
*/
/**
* Register actor tests with Quench
* @param {Quench} quenchRunner - The Quench test runner instance
*/
export function registerActorTests(quenchRunner) {
quenchRunner.registerBatch(
"vagabond.actors.character",
(context) => {
const { describe, it, expect, beforeEach, afterEach } = context;
let testActor = null;
beforeEach(async () => {
// Create a fresh test actor before each test
testActor = await Actor.create({
name: "Test Character",
type: "character",
system: {
stats: {
might: { value: 5 },
dexterity: { value: 4 },
awareness: { value: 3 },
reason: { value: 4 },
presence: { value: 3 },
luck: { value: 2 },
},
level: 1,
},
});
});
afterEach(async () => {
// Clean up test actor after each test
if (testActor) {
await testActor.delete();
testActor = null;
}
});
describe("Derived Values", () => {
it("calculates Max HP as Might × Level", async () => {
expect(testActor.system.resources.hp.max).to.equal(5); // 5 × 1
await testActor.update({ "system.level": 3 });
expect(testActor.system.resources.hp.max).to.equal(15); // 5 × 3
});
it("calculates Speed based on Dexterity", async () => {
// DEX 4 = 30 ft speed
expect(testActor.system.speed.value).to.equal(30);
await testActor.update({ "system.stats.dexterity.value": 6 });
expect(testActor.system.speed.value).to.equal(35);
await testActor.update({ "system.stats.dexterity.value": 2 });
expect(testActor.system.speed.value).to.equal(25);
});
it("calculates Item Slots as 8 + Might", async () => {
expect(testActor.system.itemSlots.max).to.equal(13); // 8 + 5
});
it("calculates Save difficulties correctly", async () => {
// Reflex = DEX + AWR = 4 + 3 = 7, Difficulty = 20 - 7 = 13
expect(testActor.system.saves.reflex.difficulty).to.equal(13);
// Endure = MIT + MIT = 5 + 5 = 10, Difficulty = 20 - 10 = 10
expect(testActor.system.saves.endure.difficulty).to.equal(10);
// Will = RSN + PRS = 4 + 3 = 7, Difficulty = 20 - 7 = 13
expect(testActor.system.saves.will.difficulty).to.equal(13);
});
it("calculates Skill difficulties based on training", async () => {
// Untrained: 20 - stat
// Trained: 20 - (stat × 2)
// Arcana (RSN 4), untrained: 20 - 4 = 16
expect(testActor.system.skills.arcana.difficulty).to.equal(16);
// Set trained
await testActor.update({ "system.skills.arcana.trained": true });
// Trained: 20 - (4 × 2) = 12
expect(testActor.system.skills.arcana.difficulty).to.equal(12);
});
});
describe("Resource Tracking", () => {
it("tracks Fatigue from 0 to 5", async () => {
expect(testActor.system.resources.fatigue.value).to.equal(0);
await testActor.update({ "system.resources.fatigue.value": 3 });
expect(testActor.system.resources.fatigue.value).to.equal(3);
// Fatigue reduces item slots
expect(testActor.system.itemSlots.max).to.equal(10); // 13 - 3
});
it("tracks Current Luck up to Luck stat", async () => {
expect(testActor.system.resources.luck.max).to.equal(2);
});
});
},
{ displayName: "Vagabond: Character Actors" }
);
quenchRunner.registerBatch(
"vagabond.actors.npc",
(context) => {
const { describe, it, expect, beforeEach, afterEach } = context;
let testNPC = null;
beforeEach(async () => {
testNPC = await Actor.create({
name: "Test Goblin",
type: "npc",
system: {
hd: 1,
hp: { value: 4, max: 4 },
tl: 0.8,
zone: "frontline",
morale: 6,
armor: 0,
},
});
});
afterEach(async () => {
if (testNPC) {
await testNPC.delete();
testNPC = null;
}
});
describe("NPC Stats", () => {
it("stores HD and HP independently", async () => {
expect(testNPC.system.hd).to.equal(1);
expect(testNPC.system.hp.max).to.equal(4);
});
it("stores Threat Level (TL)", async () => {
expect(testNPC.system.tl).to.equal(0.8);
});
it("stores Zone behavior", async () => {
expect(testNPC.system.zone).to.equal("frontline");
});
it("stores Morale score", async () => {
expect(testNPC.system.morale).to.equal(6);
});
});
},
{ displayName: "Vagabond: NPC Actors" }
);
}

View File

@ -0,0 +1,74 @@
/**
* Quench Test Registration
*
* This module registers all Vagabond RPG test batches with the Quench testing framework.
* Tests are organized by domain: actors, items, rolls, effects.
*
* @see https://github.com/Ethaks/FVTT-Quench
*/
// Import test modules
// import { registerActorTests } from "./actor.test.mjs";
// import { registerItemTests } from "./item.test.mjs";
// import { registerRollTests } from "./rolls.test.mjs";
// import { registerEffectTests } from "./effects.test.mjs";
/**
* Register all Vagabond test batches with Quench
* Called from the main system init when Quench is available
* @param {Quench} quenchRunner - The Quench test runner instance
*/
export function registerQuenchTests(quenchRunner) {
// Register placeholder test batch to verify Quench integration
quenchRunner.registerBatch(
"vagabond.sanity",
(context) => {
const { describe, it, expect } = context;
describe("Vagabond System Sanity Checks", () => {
it("should have CONFIG.VAGABOND defined", () => {
expect(CONFIG.VAGABOND).to.exist;
});
it("should have all six stats configured", () => {
const stats = CONFIG.VAGABOND.stats;
expect(stats).to.have.property("might");
expect(stats).to.have.property("dexterity");
expect(stats).to.have.property("awareness");
expect(stats).to.have.property("reason");
expect(stats).to.have.property("presence");
expect(stats).to.have.property("luck");
});
it("should have all twelve skills configured", () => {
const skills = CONFIG.VAGABOND.skills;
expect(Object.keys(skills)).to.have.length(12);
});
it("should have spell delivery types configured", () => {
const delivery = CONFIG.VAGABOND.spellDelivery;
expect(delivery.touch.cost).to.equal(0);
expect(delivery.aura.cost).to.equal(2);
expect(delivery.sphere.cost).to.equal(2);
});
it("should calculate correct speed by DEX", () => {
const speedByDex = CONFIG.VAGABOND.speedByDex;
expect(speedByDex[2]).to.equal(25);
expect(speedByDex[4]).to.equal(30);
expect(speedByDex[6]).to.equal(35);
});
});
},
{ displayName: "Vagabond: Sanity Checks" }
);
// Register domain-specific test batches (uncomment as implemented)
// registerActorTests(quenchRunner);
// registerItemTests(quenchRunner);
// registerRollTests(quenchRunner);
// registerEffectTests(quenchRunner);
// eslint-disable-next-line no-console
console.log("Vagabond RPG | Quench tests registered");
}

View File

@ -17,6 +17,9 @@ import { VAGABOND } from "./helpers/config.mjs";
// Import helper functions // Import helper functions
// import { preloadHandlebarsTemplates } from "./helpers/templates.mjs"; // import { preloadHandlebarsTemplates } from "./helpers/templates.mjs";
// Import test registration (for Quench)
import { registerQuenchTests } from "./tests/quench-init.mjs";
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Foundry VTT Initialization */ /* Foundry VTT Initialization */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -24,7 +27,8 @@ import { VAGABOND } from "./helpers/config.mjs";
/** /**
* Init hook - runs once when Foundry initializes * Init hook - runs once when Foundry initializes
*/ */
Hooks.once("init", function() { Hooks.once("init", () => {
// eslint-disable-next-line no-console
console.log("Vagabond RPG | Initializing Vagabond RPG System"); console.log("Vagabond RPG | Initializing Vagabond RPG System");
// Add custom constants for configuration // Add custom constants for configuration
@ -59,7 +63,8 @@ Hooks.once("init", function() {
/** /**
* Ready hook - runs when Foundry is fully loaded * Ready hook - runs when Foundry is fully loaded
*/ */
Hooks.once("ready", function() { Hooks.once("ready", () => {
// eslint-disable-next-line no-console
console.log("Vagabond RPG | System Ready"); console.log("Vagabond RPG | System Ready");
// Display welcome message for GMs // Display welcome message for GMs
@ -76,64 +81,68 @@ Hooks.once("ready", function() {
/** /**
* Define Handlebars helpers used throughout the system * Define Handlebars helpers used throughout the system
*/ */
Hooks.once("init", function() { Hooks.once("init", () => {
// Multiply helper for formulas // Multiply helper for formulas
Handlebars.registerHelper("multiply", function(a, b) { Handlebars.registerHelper("multiply", (a, b) => Number(a) * Number(b));
return Number(a) * Number(b);
});
// Subtract helper // Subtract helper
Handlebars.registerHelper("subtract", function(a, b) { Handlebars.registerHelper("subtract", (a, b) => Number(a) - Number(b));
return Number(a) - Number(b);
});
// Calculate difficulty (20 - stat or 20 - stat*2 if trained) // Calculate difficulty (20 - stat or 20 - stat*2 if trained)
Handlebars.registerHelper("difficulty", function(stat, trained) { Handlebars.registerHelper("difficulty", (stat, trained) => {
const statValue = Number(stat) || 0; const statValue = Number(stat) || 0;
return trained ? 20 - (statValue * 2) : 20 - statValue; return trained ? 20 - statValue * 2 : 20 - statValue;
}); });
// Check if value equals comparison // Check if value equals comparison
Handlebars.registerHelper("eq", function(a, b) { Handlebars.registerHelper("eq", (a, b) => a === b);
return a === b;
});
// Check if value is greater than // Check if value is greater than
Handlebars.registerHelper("gt", function(a, b) { Handlebars.registerHelper("gt", (a, b) => Number(a) > Number(b));
return Number(a) > Number(b);
});
// Check if value is less than // Check if value is less than
Handlebars.registerHelper("lt", function(a, b) { Handlebars.registerHelper("lt", (a, b) => Number(a) < Number(b));
return Number(a) < Number(b);
});
// Concatenate strings // Concatenate strings
Handlebars.registerHelper("concat", function(...args) { Handlebars.registerHelper("concat", (...args) => {
// Remove the Handlebars options object from args // Remove the Handlebars options object from args
args.pop(); args.pop();
return args.join(""); return args.join("");
}); });
// Capitalize first letter // Capitalize first letter
Handlebars.registerHelper("capitalize", function(str) { Handlebars.registerHelper("capitalize", (str) => {
if (typeof str !== "string") return ""; if (typeof str !== "string") return "";
return str.charAt(0).toUpperCase() + str.slice(1); return str.charAt(0).toUpperCase() + str.slice(1);
}); });
// Format number with sign (+/-) // Format number with sign (+/-)
Handlebars.registerHelper("signedNumber", function(num) { Handlebars.registerHelper("signedNumber", (num) => {
const n = Number(num) || 0; const n = Number(num) || 0;
return n >= 0 ? `+${n}` : `${n}`; return n >= 0 ? `+${n}` : `${n}`;
}); });
}); });
/* -------------------------------------------- */
/* Quench Test Registration */
/* -------------------------------------------- */
/**
* Register tests with the Quench testing framework if available.
* Quench provides in-Foundry testing using Mocha + Chai.
* @see https://github.com/Ethaks/FVTT-Quench
*/
Hooks.once("quenchReady", (quenchRunner) => {
registerQuenchTests(quenchRunner);
});
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Hot Reload Support (Development) */ /* Hot Reload Support (Development) */
/* -------------------------------------------- */ /* -------------------------------------------- */
if (import.meta.hot) { if (import.meta.hot) {
import.meta.hot.accept((newModule) => { import.meta.hot.accept((_newModule) => {
// eslint-disable-next-line no-console
console.log("Vagabond RPG | Hot reload triggered"); console.log("Vagabond RPG | Hot reload triggered");
}); });
} }

2421
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,10 @@
"build": "sass styles/scss/vagabond.scss styles/vagabond.css --style=compressed", "build": "sass styles/scss/vagabond.scss styles/vagabond.css --style=compressed",
"watch": "sass styles/scss/vagabond.scss styles/vagabond.css --watch --style=expanded --source-map", "watch": "sass styles/scss/vagabond.scss styles/vagabond.css --watch --style=expanded --source-map",
"lint": "eslint module/", "lint": "eslint module/",
"lint:fix": "eslint module/ --fix",
"format": "prettier --write \"module/**/*.mjs\" \"styles/**/*.scss\" \"lang/**/*.json\" \"*.json\" \"*.md\"",
"format:check": "prettier --check \"module/**/*.mjs\" \"styles/**/*.scss\" \"lang/**/*.json\" \"*.json\" \"*.md\"",
"prepare": "husky",
"release": "npm run build && zip -r vagabond.zip system.json module/ templates/ styles/vagabond.css lang/ packs/ assets/ LICENSE README.md" "release": "npm run build && zip -r vagabond.zip system.json module/ templates/ styles/vagabond.css lang/ packs/ assets/ LICENSE README.md"
}, },
"repository": { "repository": {
@ -19,8 +23,26 @@
}, },
"homepage": "https://github.com/calcorum/vagabond-rpg-foundryvtt#readme", "homepage": "https://github.com/calcorum/vagabond-rpg-foundryvtt#readme",
"devDependencies": { "devDependencies": {
"sass": "^1.69.0", "eslint": "^8.57.0",
"eslint": "^8.56.0" "husky": "^9.1.0",
"lint-staged": "^15.2.0",
"prettier": "^3.3.0",
"sass": "^1.69.0"
},
"lint-staged": {
"module/**/*.mjs": [
"eslint --fix",
"prettier --write"
],
"styles/**/*.scss": [
"prettier --write"
],
"lang/**/*.json": [
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}, },
"type": "module" "type": "module"
} }

View File

@ -10,7 +10,9 @@
color: $color-text-primary; color: $color-text-primary;
// Box sizing // Box sizing
*, *::before, *::after { *,
*::before,
*::after {
box-sizing: border-box; box-sizing: border-box;
} }
@ -29,7 +31,12 @@
} }
// Headings // Headings
h1, h2, h3, h4, h5, h6 { h1,
h2,
h3,
h4,
h5,
h6 {
font-family: $font-family-header; font-family: $font-family-header;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
line-height: $line-height-tight; line-height: $line-height-tight;
@ -37,12 +44,24 @@
margin: 0 0 $spacing-2 0; margin: 0 0 $spacing-2 0;
} }
h1 { font-size: $font-size-4xl; } h1 {
h2 { font-size: $font-size-3xl; } font-size: $font-size-4xl;
h3 { font-size: $font-size-2xl; } }
h4 { font-size: $font-size-xl; } h2 {
h5 { font-size: $font-size-lg; } font-size: $font-size-3xl;
h6 { font-size: $font-size-base; } }
h3 {
font-size: $font-size-2xl;
}
h4 {
font-size: $font-size-xl;
}
h5 {
font-size: $font-size-lg;
}
h6 {
font-size: $font-size-base;
}
// Paragraphs // Paragraphs
p { p {
@ -54,7 +73,8 @@
} }
// Lists // Lists
ul, ol { ul,
ol {
margin: 0 0 $spacing-4 0; margin: 0 0 $spacing-4 0;
padding-left: $spacing-6; padding-left: $spacing-6;
} }
@ -76,7 +96,8 @@
margin-bottom: $spacing-4; margin-bottom: $spacing-4;
} }
th, td { th,
td {
padding: $spacing-2 $spacing-3; padding: $spacing-2 $spacing-3;
text-align: left; text-align: left;
border-bottom: 1px solid $color-border-light; border-bottom: 1px solid $color-border-light;
@ -123,14 +144,13 @@
.sheet.vagabond { .sheet.vagabond {
@include custom-scrollbar; @include custom-scrollbar;
background: $color-parchment; background: $color-parchment;
background-image: background-image: linear-gradient(
linear-gradient( to bottom,
to bottom, rgba($color-parchment-light, 0.3) 0%,
rgba($color-parchment-light, 0.3) 0%, transparent 5%,
transparent 5%, transparent 95%,
transparent 95%, rgba($color-parchment-dark, 0.3) 100%
rgba($color-parchment-dark, 0.3) 100% );
);
} }
// Window header customization // Window header customization

View File

@ -11,29 +11,29 @@ $color-parchment-dark: #d4c4a8;
$color-parchment-darker: #c4b08c; $color-parchment-darker: #c4b08c;
// Text colors (high contrast) // Text colors (high contrast)
$color-text-primary: #2c2416; // Main text - dark brown/black $color-text-primary: #2c2416; // Main text - dark brown/black
$color-text-secondary: #4a3f2f; // Secondary text $color-text-secondary: #4a3f2f; // Secondary text
$color-text-muted: #6b5d4d; // Muted/disabled text $color-text-muted: #6b5d4d; // Muted/disabled text
$color-text-inverse: #f5f0e1; // Text on dark backgrounds $color-text-inverse: #f5f0e1; // Text on dark backgrounds
// Accent colors // Accent colors
$color-accent-primary: #8b4513; // Saddle brown - primary actions $color-accent-primary: #8b4513; // Saddle brown - primary actions
$color-accent-secondary: #654321; // Dark brown - secondary $color-accent-secondary: #654321; // Dark brown - secondary
$color-accent-highlight: #cd853f; // Peru - highlights/hover $color-accent-highlight: #cd853f; // Peru - highlights/hover
// Semantic colors // Semantic colors
$color-success: #2d5a27; // Dark green - success/healing $color-success: #2d5a27; // Dark green - success/healing
$color-danger: #8b0000; // Dark red - damage/danger $color-danger: #8b0000; // Dark red - damage/danger
$color-warning: #b8860b; // Dark goldenrod - warnings $color-warning: #b8860b; // Dark goldenrod - warnings
$color-info: #2f4f4f; // Dark slate gray - info $color-info: #2f4f4f; // Dark slate gray - info
// Stat colors (for visual distinction) // Stat colors (for visual distinction)
$color-might: #8b0000; // Red - strength/power $color-might: #8b0000; // Red - strength/power
$color-dexterity: #228b22; // Green - agility $color-dexterity: #228b22; // Green - agility
$color-awareness: #4169e1; // Royal blue - perception $color-awareness: #4169e1; // Royal blue - perception
$color-reason: #9932cc; // Purple - intellect $color-reason: #9932cc; // Purple - intellect
$color-presence: #daa520; // Goldenrod - charisma $color-presence: #daa520; // Goldenrod - charisma
$color-luck: #20b2aa; // Light sea green - fortune $color-luck: #20b2aa; // Light sea green - fortune
// Borders // Borders
$color-border: #8b7355; $color-border: #8b7355;
@ -46,19 +46,19 @@ $shadow-medium: rgba(0, 0, 0, 0.2);
$shadow-dark: rgba(0, 0, 0, 0.3); $shadow-dark: rgba(0, 0, 0, 0.3);
// Typography // Typography
$font-family-header: 'Modesto Condensed', 'Roboto Slab', 'Georgia', serif; $font-family-header: "Modesto Condensed", "Roboto Slab", "Georgia", serif;
$font-family-body: 'Signika', 'Roboto', 'Helvetica Neue', sans-serif; $font-family-body: "Signika", "Roboto", "Helvetica Neue", sans-serif;
$font-family-mono: 'Roboto Mono', 'Consolas', monospace; $font-family-mono: "Roboto Mono", "Consolas", monospace;
// Font sizes (using rem for accessibility) // Font sizes (using rem for accessibility)
$font-size-xs: 0.75rem; // 12px $font-size-xs: 0.75rem; // 12px
$font-size-sm: 0.875rem; // 14px $font-size-sm: 0.875rem; // 14px
$font-size-base: 1rem; // 16px $font-size-base: 1rem; // 16px
$font-size-lg: 1.125rem; // 18px $font-size-lg: 1.125rem; // 18px
$font-size-xl: 1.25rem; // 20px $font-size-xl: 1.25rem; // 20px
$font-size-2xl: 1.5rem; // 24px $font-size-2xl: 1.5rem; // 24px
$font-size-3xl: 1.875rem; // 30px $font-size-3xl: 1.875rem; // 30px
$font-size-4xl: 2.25rem; // 36px $font-size-4xl: 2.25rem; // 36px
// Font weights // Font weights
$font-weight-normal: 400; $font-weight-normal: 400;
@ -73,14 +73,14 @@ $line-height-relaxed: 1.75;
// Spacing scale // Spacing scale
$spacing-0: 0; $spacing-0: 0;
$spacing-1: 0.25rem; // 4px $spacing-1: 0.25rem; // 4px
$spacing-2: 0.5rem; // 8px $spacing-2: 0.5rem; // 8px
$spacing-3: 0.75rem; // 12px $spacing-3: 0.75rem; // 12px
$spacing-4: 1rem; // 16px $spacing-4: 1rem; // 16px
$spacing-5: 1.25rem; // 20px $spacing-5: 1.25rem; // 20px
$spacing-6: 1.5rem; // 24px $spacing-6: 1.5rem; // 24px
$spacing-8: 2rem; // 32px $spacing-8: 2rem; // 32px
$spacing-10: 2.5rem; // 40px $spacing-10: 2.5rem; // 40px
// Border radius // Border radius
$radius-sm: 2px; $radius-sm: 2px;

View File

@ -144,7 +144,8 @@
// Animation // Animation
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%,
100% {
transform: scale(1); transform: scale(1);
} }
50% { 50% {

View File

@ -97,7 +97,8 @@
margin-right: $spacing-2; margin-right: $spacing-2;
} }
input, select { input,
select {
flex: 1; flex: 1;
max-width: 200px; max-width: 200px;
} }

View File

@ -2,23 +2,23 @@
// ========================================== // ==========================================
// Configuration // Configuration
@import 'variables'; @import "variables";
@import 'mixins'; @import "mixins";
// Base styles // Base styles
@import 'base'; @import "base";
// Components // Components
@import 'components/buttons'; @import "components/buttons";
@import 'components/forms'; @import "components/forms";
@import 'components/tabs'; @import "components/tabs";
// Sheets // Sheets
@import 'sheets/actor-sheet'; @import "sheets/actor-sheet";
@import 'sheets/item-sheet'; @import "sheets/item-sheet";
// Dialogs // Dialogs
@import 'dialogs/roll-dialog'; @import "dialogs/roll-dialog";
// Chat // Chat
@import 'chat/chat-cards'; @import "chat/chat-cards";

View File

@ -109,7 +109,16 @@
{ {
"name": "Vagabond RPG", "name": "Vagabond RPG",
"sorting": "a", "sorting": "a",
"packs": ["ancestries", "classes", "spells", "perks", "weapons", "armor", "equipment", "bestiary"] "packs": [
"ancestries",
"classes",
"spells",
"perks",
"weapons",
"armor",
"equipment",
"bestiary"
]
} }
], ],
"socket": false, "socket": false,