LegacyShell WikiLegacyShell Wiki
Back to LegacyShell
Home
Wiki
Plugins
Docs
Back to LegacyShell
Home
Wiki
Plugins
Docs
  • Documentation

    • Getting Started

      • What is LegacyShell?
      • Speed Setup
      • Requirements
      • Installation
      • First Run
      • Config Files
      • Making an Account
      • Troubleshooting (Getting Started)
    • Running a Server

      • Architecture Overview
      • The Database
      • Users and Ranks
      • Adding Game Servers
      • Client Mirrors
      • Perpetual
      • Backups
      • Rate Limiting
      • Moderation
      • Closed Mode
      • Deployment
      • Troubleshooting (Running a Server)
      • Hosting for Someone Else's Instance
    • Content Creation

      • Maps
      • Dealing with Babylon Models
      • Map Blocks
      • Items and Skins
      • Hats and Stamps
      • Sounds
      • Gamemodes
      • Seasonal Events
    • Plugin Development

      • Quickstart
      • I Want To...
      • Anatomy of a Plugin
      • Lifecycle
      • Dependencies
      • Events (Concept)
      • Event Reference

        • services: events
        • game: events — shared logic (src/shell/)
        • game: events — main-thread server process
        • game: events — per-connection client object
        • game: events — room lifecycle & tick loop
        • game: events — in-browser gameplay
        • client: events — client server & build pipeline
      • Commands
      • Client-Side Code
      • Static Assets
      • Content Packs
      • Networking
      • Workers and State
      • Prediction and Authority
      • Recipes

        • Recipe: Killstreaks
        • Recipe: New Pickup Item
        • Recipe: New Gamemode
        • Recipe: Custom Weapon
        • Recipe: UI Modification
        • Recipe: Discord Integration
        • Recipe: Replacing Core Behaviour
        • Recipe: Persistent Plugin Storage
        • Recipe: Rewarding Players with Currency
        • Recipe: Custom Per-Player Data
        • Recipe: Custom Theme
      • Publishing
      • Pitfalls
      • Modifiers
      • Sound and Apollo
    • Codebase Reference

      • Repo Layout
      • Shared Shell Layer
      • Server-Only Markers
      • The ss Object
      • Build Pipeline
      • Stamps and Babylons
      • Game Loop
      • Rooms and Workers
      • Wire Protocol
      • Generated

        • Wire Protocol Opcodes
        • Enums & Lookup Tables
        • Database Schema
        • Config Reference
        • Slash Command Reference
      • Services Internals
      • Catalog and Items
      • Permissions Internals
      • Physics and Collision
      • Known Quirks
      • Codebase Anecdotes
      • Development Timeline
    • Contributing

      • Documentation Style Guide
      • Generators
      • For AI Agents

Recipe: New Gamemode

Audience: Plugin authors · Prereqs: Events (concept)

Canonical source: src/shell/gametypes.js

Registering an actual new entry in the gamemode selector - distinct from Gamemodes, which covers per-room configuration of existing modes. This recipe adds "Sudden Death" - any hit is lethal.

The hook

gametypes.js builds its GameTypes array inside an async IIFE that awaits game:GameTypesInit before filling in defaults:

// src/shell/gametypes.js - real code
(async function () {
    await plugins.emit('GameTypesInit', { GameTypes, ItemTypes, defaultOptions });
    // ...fills in defaults, assigns .value/.shortNameDisplay/.longNameDisplay, builds AllMapPools and the GameType enum
})();

GameTypes is passed by reference - a listener pushes a new entry onto the same array the rest of the file then finishes processing.

The plugin

// plugins/suddendeath/index.js
import log from 'puppylog';

export const PluginMeta = {
    identifier: "suddendeath",
    name: 'Sudden Death',
    author: 'you',
    version: '1.0.0',
    descriptionShort: 'A gamemode where any hit is lethal.',
    descriptionLong: 'Adds a Sudden Death gamemode - resistanceModifier is set extremely low so any hit kills.',
    legacyShellVersion: 598,
};

export class Plugin {
    constructor(plugins, thisDir) {
        this.plugins = plugins;
        this.thisDir = thisDir;

        this.plugins.on('game:GameTypesInit', this.onGameTypesInit.bind(this));
    };

    onGameTypesInit(data) {
        data.GameTypes.push({
            shortName: "Sudden Death",
            longName: "Sudden Death",
            codeName: "suddendeath",
            mapPool: "FFA",
            options: {
                resistanceModifier: [0.01, 0.01, 0.01],   // [ffa, team1, team2]
            },
        });
    };
};

options only needs to specify what differs from defaultOptions - everything else (item spawn rates, gravity, etc.) is deep-merged from the default automatically, the same way the two built-in modes (FFA, Teams) only specify teamsEnabled and nothing else.

Why resistanceModifier, specifically

Damage resolution (player.js's hit method) computes damage = Math.ceil((damage / player.modifiers.resistanceModifier) / player.modifiers.scale) - resistance is a divisor, so a value far below 1 (the default) massively amplifies incoming damage rather than reducing it. 0.01 means damage gets multiplied roughly 100x, which one-shots a player from any hit that would normally do noticeable damage, without needing to touch the actual hit-resolution code at all - the existing per-team modifier system already does the work.

mapPool

Set to "FFA" here, reusing the existing free-for-all map pool rather than requiring maps to be specifically tagged for a brand-new pool name - see Gamemodes. If you want your mode restricted to a curated map subset instead, pick a new pool name and make sure at least some maps' modes field includes it (see Maps).

What we validated

Loaded against a real (isolated, scratch) game server: GameTypes grew to include the new entry on both the main thread and inside a room worker, confirmed via a log line reporting the array's length after registration (went from the baseline of built-in-plus-other-bundled-plugin modes to one more, in both threads, matching expectations). We did not verify the in-game damage behavior against live gameplay - see Killstreaks for the same category of limitation and why it's an honest one to state rather than paper over.

Common Issues

My gamemode doesn't appear in the mode list at all. Confirm your listener is actually registered before GameTypesInit fires - this happens once per process at gametypes.js's own module-evaluation time (early in boot, on both the main thread and every room worker independently - see Rooms and Workers), so a plugin loaded normally through the standard boot sequence is always in time; this would only bite you if something tried to import #gametypes unusually early, which no plugin should ever need to do directly.

Options I didn't specify aren't behaving like the defaults. Double-check you're using the exact same key names as defaultOptions (src/shell/gametypes.js) - a typo'd key doesn't override anything, it just becomes an unused extra field, silently.


This page was drafted with AI assistance and reviewed for accuracy. If something looks wrong, please open a PR or flag it.

Edit this page on GitHub
Prev
Recipe: New Pickup Item
Next
Recipe: Custom Weapon