Development Discussion - crnormand/gurps GitHub Wiki

Create pages under this top-level page for discussion of various development issues.

Key Information

  • 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)

Architecture Overview

Document Structure

The system follows Foundry's dual-architecture pattern:

  1. Legacy V1 Documents (GurpsActor, GurpsItem) - Original JavaScript implementation
  2. Modern V2 Documents (GurpsActorV2, GurpsItemV2) - New TypeScript implementation

Module Organization

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

Key Libraries and Dependencies

  • 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

Coding Standards and Patterns

TypeScript/JavaScript Guidelines

Key Guidelines

  1. Follow TypeScript best practices and idiomatic patterns
  2. Maintain existing code structure and organization
  3. Write unit tests for new functionality. Use table-driven unit tests when possible.
  4. Document public APIs and complex logic. Suggest changes to the docs/ folder when appropriate

File Extensions and Types

  • .ts for new TypeScript files (preferred)
  • .js for legacy JavaScript files
  • Use ES modules (import/export) throughout
  • Maintain backward compatibility when updating legacy code

Naming Conventions

  • 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)

Commenting Standards

  • 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

Component Architecture

Actor Components (Legacy System)

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
  }
}

Item System V2 (Modern)

// 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 }),
    }
  }
}

GGA Module Structure

  • Related code is strongly encouraged to be grouped together and encapsulated in a "module".
  • Modules are declared as directories in the ./module directory and named as the module's name.
  • Modules declare an index.ts file 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 GurpsModule from ./module/gurps-module.ts.
  • Modules are responsible for all their required initialization, for example:
    • Any Foundry Hooks listeners.
    • Any Foundry settings in the module's domain (even if used outside the module -- but read on).
    • Any version migrations.
  • Modules are "registered" and stored on the global GURPS object in the modules variable in the gurps.js file. The code in gurps.js controls 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.js calls the module's migrate() method during Foundry's ready Hook 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.
  • i18n language 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 in GURPS.foo-bar.*.
    • Within that tag, further structure is encouraged -- for example, grouping tags by UI element or function.

Import/Export Patterns

ES Module Imports

// 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 Registration

// Module pattern for feature organization
export const Combat: GurpsModule = {
  init() {
    CONFIG.Combat.documentClass = GurpsCombat
    CONFIG.Combatant.documentClass = GurpsCombatant
  },
}

Null Safety

// Use optional chaining and nullish coalescing
const level = item.system?.ski?.level ?? 0
const name = actor.name || 'Unknown Actor'

Testing Standards

Unit Tests

  • Use Jest with TypeScript support
  • Place tests in test/ directory
  • Mock Foundry globals in test/jest.setup.js
  • Test files should end with .test.ts or .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)
  })
})

Test Commands

npm run test      # Run tests once
npm run tdd       # Run tests with coverage and watch mode

Development Workflow

Developer responsibilities

  • 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 tdd or npm 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.

git branching

  • There are several dedicated branches:
    • main - This contains the current, working code targeted for the next release. No development directly on main. We always merge working, tested code into main from another branch.
    • release - This branch has exactly the code that was released to the public. release is created at the time of releasing the code by merging main into 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 named develop/<purpose> where <purpose> describes the purpose of the development branch.
  • Individual developers always create a branch that follows the naming convention bugfix/<github issue> or feature/<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.

Build System

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

File Organization

Module Boundaries

  • Keep Foundry-independent code in lib/
  • Place Foundry-dependent utilities in utils/
  • Organize features by domain in module/

Common Patterns and Anti-Patterns

✅ Preferred Patterns

Data Model Usage

// 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 }),
    }
  }
}

Async/Await

// Use async/await instead of Promises
async function updateActor(actor: GurpsActor, data: object) {
  await actor.update(data)
  return actor
}

Type Safety

// 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
}

Measurement Systems

  • Support both Imperial and Metric units
  • Use the Length class for distance calculations
  • Follow GURPS conversion rules (not real-world)

Localization

i18n Patterns

// Use Foundry's localization system
const label = game.i18n.localize('GURPS.SkillLevel')
const formatted = game.i18n.format('GURPS.DamageFormula', { damage: '2d+1' })

Supported Languages

  • English (en) - Primary
  • German (de)
  • French (fr)
  • Portuguese/Brazil (pt_br)
  • Russian (ru)

Documentation Standards

Code Comments

/**
 * 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
}

JSDoc for JavaScript

/**
 * @param {GurpsActor} actor
 * @param {string} skillName
 * @returns {number|undefined}
 */
function getSkillLevel(actor, skillName) {
  // Implementation
}

Debugging and Development Tools

Coverage and Testing

  • Run coverage with npm run tdd
  • Use Chrome DevTools coverage for manual testing
  • Maintain high test coverage for critical paths

Quick Reference

Build Commands

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
⚠️ **GitHub.com Fallback** ⚠️