Development Discussion - crnormand/gurps GitHub Wiki
Create pages under this top-level page for discussion of various development issues.
-
System ID:
gurps - Primary Languages: TypeScript (modern modules), JavaScript (legacy)
- License: Steve Jackson Games Online Policy compliant
- Main Authors: Chris Normand (nose66, @crnormand), M. Jeff Wilson (nick.coffin.pi, @mjeffw), Mikolaj Tomczynski (sasiedny, @rinickolous)
The system follows Foundry's dual-architecture pattern:
-
Legacy V1 Documents (
GurpsActor,GurpsItem) - Original JavaScript implementation -
Modern V2 Documents (
GurpsActorV2,GurpsItemV2) - New TypeScript implementation
module/
├── actor/ # Actor documents, sheets, and components
├── item/ # Item documents, sheets, and data models
├── action/ # Attack and action system
├── data/ # Data models and schemas (TypeScript) not directly tied to Foundry documents
├── combat/ # Combat and initiative system
├── damage/ # Damage calculation and application
├── effects/ # Active effects and status management
├── gcs-importer/ # GCS (GURPS Character Sheet) import system
├── pdf/ # PDF reference integration
├── token/ # Token enhancements and HUD
├── ui/ # User interface components
└── utilities/ # Shared utility functions
- assets/: Static assets (images, fonts)
- dev-utilities/: Development utilities
- exportutils/: Export utilities for external character sheet programs
- lang/: Internationalization files (en, de, fr, pt_br, ru)
- lib/: Third-party JavaScript libraries independent of Foundry
- utils/: Foundry-dependent utilities
- scripts/: Javascript libraries dependent on Foundry but not part of the module
- test/: Unit tests using Jest with TypeScript support
- Follow TypeScript best practices and idiomatic patterns
- Maintain existing code structure and organization
- Write unit tests for new functionality. Use table-driven unit tests when possible.
- Document public APIs and complex logic. Suggest changes to the
docs/folder when appropriate
-
.tsfor new TypeScript files (preferred) -
.jsfor legacy JavaScript files - Use ES modules (
import/export) throughout - Maintain backward compatibility when updating legacy code
-
Classes: PascalCase (
GurpsActor,MeleeAttackModel) -
Files: kebab-case for TypeScript and JavaScript (
gurps-actor.ts,actor-importer.js) -
Variables/Functions: camelCase (
calculateDamage,isEnabled) -
Constants: SCREAMING_SNAKE_CASE (
SETTING_USE_FOUNDRY_ITEMS) -
Foundry Extensions: Prefix with
Gurps(GurpsToken,GurpsTokenHUD)
- Use JSDoc for public APIs and complex functions
- Inline comments for non-obvious logic
- Comments begin with a capital letter and end with a period or question mark.
- Maintain existing comment styles in legacy code
The legacy system uses component-based architecture:
// Actor components are plain objects with methods
export class Skill extends Leveled {
static fromObject(data, actor) {
// Factory pattern for creating components
}
}// Modern items use typed data models
class SkillModel extends BaseItemModel {
static defineSchema() {
return {
...super.defineSchema(),
difficulty: new fields.StringField(),
points: new fields.NumberField({ min: 0 }),
}
}
}- Related code is strongly encouraged to be grouped together and encapsulated in a "module".
- Modules are declared as directories in the
./moduledirectory and named as the module's name. - Modules declare an
index.tsfile which is the public interface to the system.- No other files in the module directory should be imported from outside the module directory.
- Each module must export an object that implements
GurpsModulefrom./module/gurps-module.ts. - Modules are responsible for all their required initialization, for example:
- Any Foundry
Hookslisteners. - Any Foundry settings in the module's domain (even if used outside the module -- but read on).
- Any version migrations.
- Any Foundry
- Modules are "registered" and stored on the global
GURPSobject in themodulesvariable in thegurps.jsfile. The code ingurps.jscontrols the lifecycle of the module.-
Module.init()is called immediately, which means the module cannot depend on the Foundry environment configuration being complete. The module typically registers its own Hook listeners to execute code at various points in the Foundry lifecycle. -
gurps.jscalls the module'smigrate()method during Foundry'sreadyHook event.
-
- Modules should expose Foundry settings via the module interface as a set of getter style functions, and external code should use the module's interface instead of calling
game.settings.get()directly. -
i18nlanguage file entries related to the module should be placed in a substructure of the lang file named for the module. For example, a module named 'foo-bar' would declare all of its tags inGURPS.foo-bar.*.- Within that tag, further structure is encouraged -- for example, grouping tags by UI element or function.
// Relative imports with extensions
import { GurpsActor } from './actor/gurps-actor.js'
import { Length } from '../data/common/length.js'
// Re-exports in index files
export * from './combat.js'
export * from './combatant.js'// Module pattern for feature organization
export const Combat: GurpsModule = {
init() {
CONFIG.Combat.documentClass = GurpsCombat
CONFIG.Combatant.documentClass = GurpsCombatant
},
}// Use optional chaining and nullish coalescing
const level = item.system?.ski?.level ?? 0
const name = actor.name || 'Unknown Actor'- Use Jest with TypeScript support
- Place tests in
test/directory - Mock Foundry globals in
test/jest.setup.js - Test files should end with
.test.tsor.test.js
// Example test structure
describe('Length', () => {
it('parses inches correctly', () => {
const length = Length.fromString('12 in', Length.Unit.Inch)
expect(length?.value).toBe(12)
expect(length?.unit).toBe(Length.Unit.Inch)
})
})npm run test # Run tests once
npm run tdd # Run tests with coverage and watch mode- Everyone who works on GGA needs to pick up Bug issues for the current release. If you want to contribute, start with resolving a bug first, and then add new features.
- All changes should be submitted as a PR and reviewed by either Chris (Nose) or Jeff (Nick Coffin, PI).
- No one should have a branch that is not frequently merged via a PR into the current dev branch. Changes have to be made in small, standalone, and logically consistent steps such that on each PR, the whole platform continues to work even if you are in the "middle" of a big refactor.
- Unit tests can be run via
npm run tddornpm run test. If using Visual Studio Code, I would prefer you install the Node TDD extension and configure it to run on every file save. Never push code to the dev branch if any unit test is failing, or they can't run for any reason. - Use this wiki page for development discussions, especially about refactoring or new features. Add a new subpage below this one for each discussion.
- There are several dedicated branches:
-
main- This contains the current, working code targeted for the next release. No development directly onmain. We always merge working, tested code intomainfrom another branch. -
release- This branch has exactly the code that was released to the public.releaseis created at the time of releasing the code by mergingmaininto this branch. - The current development branch. Normally named
develop. There can be at most two development branches. If more than one, development branches will be nameddevelop/<purpose>where<purpose>describes the purpose of the development branch.
-
- Individual developers always create a branch that follows the naming convention
bugfix/<github issue>orfeature/<new or updated feature description>. - Branches are always deleted both locally and on the remote when the bug fix or feature is complete and merged into the development branch.
npm run build # Full build (TypeScript + styles + static files)
npm run build:code # TypeScript compilation only
npm run build:styles # SCSS compilation
npm run dev # Development mode with watchers
npm run watch # Watch all file types- Keep Foundry-independent code in
lib/ - Place Foundry-dependent utilities in
utils/ - Organize features by domain in
module/
// Use Foundry DataModel for structured data
class MyDataModel extends foundry.abstract.DataModel {
static defineSchema() {
return {
name: new fields.StringField({ required: true }),
value: new fields.NumberField({ initial: 0 }),
}
}
}// Use async/await instead of Promises
async function updateActor(actor: GurpsActor, data: object) {
await actor.update(data)
return actor
}// Use TypeScript overloads for better type safety
function getItemAttacks(options: { attackType: 'melee' }): MeleeAttackModel[]
function getItemAttacks(options: { attackType: 'ranged' }): RangedAttackModel[]
function getItemAttacks(options = { attackType: 'both' }) {
// Implementation
}- Support both Imperial and Metric units
- Use the
Lengthclass for distance calculations - Follow GURPS conversion rules (not real-world)
// Use Foundry's localization system
const label = game.i18n.localize('GURPS.SkillLevel')
const formatted = game.i18n.format('GURPS.DamageFormula', { damage: '2d+1' })- English (en) - Primary
- German (de)
- French (fr)
- Portuguese/Brazil (pt_br)
- Russian (ru)
/**
* Calculate effective skill level including modifiers
* @param baseLevel The base skill level
* @param modifiers Array of modifier objects
* @returns Effective skill level
*/
function calculateEffectiveLevel(baseLevel: number, modifiers: Modifier[]): number {
// Implementation
}/**
* @param {GurpsActor} actor
* @param {string} skillName
* @returns {number|undefined}
*/
function getSkillLevel(actor, skillName) {
// Implementation
}- Run coverage with
npm run tdd - Use Chrome DevTools coverage for manual testing
- Maintain high test coverage for critical paths
npm run build # Full production build
npm run dev # Development mode with watchers
npm run test # Run unit tests
npm run tdd # Test-driven development mode with coverage