In general the game code is completely object-oriented and it has no global state, therefore we need to carry around both engine interface as well as game interface. This allows the dedicated server to handle multiple servers at the same time enabling dynamic game lobbies etc. The most global state must be the variables on the ServerGameAPI object or on the WorldspawnEntity.
This repository provides a clean, modern framework to build Quake mods using JavaScript/ES6 modules.
Want to create a mod? Here's what you need to know:
- Everything is an Entity - Players, monsters, items, doors, triggers - all extend
BaseEntity - No Global State - All game state lives in
ServerGameAPIor individual entities - Object-Oriented - Use classes, inheritance, and composition (helper classes)
- Type-Safe - JSDoc comments provide autocomplete and type checking
- Modern JavaScript - ES6 modules, classes, arrow functions
Common modding tasks:
- Create a new monster → Extend
BaseMonster(seeentity/monster/for examples) - Create a new weapon → Add to
entity/Weapons.mjsandweaponConfig - Create a new item → Extend
BaseItemEntity(seeentity/Items.mjs) - Create a new trigger → Extend
BaseTriggerEntity(seeentity/Triggers.mjs) - Create a custom entity → Just pick one of the misc entities, they are an easy start.
File structure:
source/game/id1/
├── entity/ # All entity classes
│ ├── monster/ # Monster AI and behaviors
│ ├── props/ # Doors, platforms, buttons
│ ├── BaseEntity.mjs
│ ├── Items.mjs
│ ├── Weapons.mjs
│ ├── Triggers.mjs
│ └── ...
├── helper/ # Helper classes (AI, utilities)
├── client/ # Client-side code (HUD, effects)
├── GameAPI.mjs # Server game state and entity registry
└── Defs.mjs # Constants and enums
Right now QuakeJS is a clean reimplementation of the Quake game logic. It might not be perfect though, some idiosyncrasis will be sorted out as part of the code restructuring. Some idiosyncrasis will remain due to compatibility.
During the reimplementation I noticed some bugs/issues within the original Quake game logic that I sorted out. Always trying to keep the actual game play unaffected.
Originally, Quake did not support client-side game code. In this project we also move game related logic from the engine to the game code. However, this APIs are not fully specified yet and change as the client-side game code is being ported over from the engine.
A couple of things I spotted or I’m unhappy with
- applyBackpack: currentammo not updated --> fixed by the new client code
- cvars: move game related cvars to PR and QuakeJS game, less game duties on the engine
- BaseEntity: make state definitions static, right now it’s bloating up the memory footprint
A few NPCs and features from the original game are still missing and yet to be implemented:
- player: Finale screen
- monster_fish
- monster_oldone
- monster_tarbaby
- monster_wizard
- monster_boss
- monster_boss: event_lightning
Note: Most monsters are now implemented! Only the final bosses (monster_oldone and monster_boss) remain.
- implement a more lean Sbar/HUD
- implement intermission, finale etc. screens
- move more of the effect handling from the engine to the game code
- implement damage effects (red flash)
- implement powerup effects (quad, invis etc.)
- handle things like gibbing, bubbles etc. on the client-side only
- air_bubbles (implemented as
StaticBubbleSpawnerEntity) - GibEntity (implemented in
Player.mjs) - MeatSprayEntity (implemented in
monster/BaseMonster.mjs)
- air_bubbles (implemented as
- handle screen flashes like bonus flash (
bf) through events
Note: Most client-side entities are implemented. Consider moving more visual-only effects to client-side code.
RFC 2119 applies.
- Every entity must have a unique classname.
- Every entity class should end with “Entity” in their name.
- Every entity class must not change their
classnameduring their lifetime. - Every entity class must have a
QUAKEDjsdoc. - The game code must not spawn “naked” entities using
spawn()and simply setting fields during runtime. - The game code must not assume internal structures of the engine.
- The game code must not use global variables.
- The game code should not hardcode
classnamewhen used for spawning entities, the game code should useExampleEntity.classnameinstead of'misc_example'. - Entity properties starting with
_are considered protected and must and will not be set by the map loading code. If you intend to modify these properties outside of the class defining it, you must mark with with jsdoc’s@publicannotation. - Entity properties intended to be read-only must be annotated with jsdoc’s
@readonlyannotation and should be declared throw a getter without a setter. - Entities must declare properties in the
_declareFields()method only. - Entities must declare assets to be precached in the
_precache()method only. - Entities must declare states in the
_initStates()method only. - Assets required during run-time must be precached by the
WorldspawnEntity. - Numbers related to map units should be formated like this:
1234.5. - Do not use private methods. Allow mods to extend and reuse entities by extending the classes.
- When porting over QuakeC almost verbatim, comments must be copied over as well in order to give context.
- Settings and/or properties that are considered extensions to the original should be prefixed with
qs_.
The server keeps a list of things in the world in a structure called an Edict.
Edicts will hold information only relevant to the engine such as position in the world data tree.
Furthermore, an Edict provides many methods to interact with the world and the game engine related to that Edict. See ServerEdict in the engine code.
An Entity is sitting on top of an Edict. The Entity class will provide logic and keeps track of states. There are also client entities which are not related to these Entity structures.
Entities have a classname apart from the JavaScript class name. This classname will be used by the editor to place entities into the world.
However, the engine reads from a set of must be defined properties. BaseEntity is defining all of them.
| Class | Purpose |
|---|---|
ServerGameAPI |
Holds the whole server game state. It will be instantiated by the engine’s spawn server code and only lasts exactly one level. The class holds information such as the skill level and exposes methods for engine game updates. Also the engine asks the ServerGameAPI to spawn map objects. |
ClientGameAPI |
Not completely designed yet. It is supposed to handle anything supposed to run on the client side such as HUD, temporary entities, etc. |
BaseEntity |
Every entity derives from this class. It provides all necessary information for the engine to place objects in the world. Also the engine will write back certain information directly into an entity. This class provides lots of helpers such as the state machine, thinking scheduler and also provides core concepts of for instance damage handling. |
PlayerEntity |
The player entity not just represents a player in the world, but it also handles impulse commands, weapon interaction, jumping, partially swimming, effects of having certain items. Some logic is outsourced to helper classes such as the PlayerWeapons class. |
WorldspawnEntity |
Defines the world, but is mainly used to precache resources that can be used from anywhere. |
Helper classes extend EntityWrapper and are found in entity/Weapons.mjs and entity/Subs.mjs.
| Class | Purpose | Location |
|---|---|---|
EntityWrapper |
Base wrapper for a BaseEntity. Adds shortcuts for engine API and game API instances. All helpers below extend this. |
helper/MiscHelpers.mjs |
Sub |
Brings all the target/killtarget/targetname handling to an entity. Also provides movement related helpers. The name is based on the SUB_CalcMove, SUB_UseTargets etc. prefix from QuakeC. |
entity/Subs.mjs |
DamageHandler |
Brings all logic related to receiving and handling damage to an entity. Used by monsters, players, and breakable objects. | entity/Weapons.mjs |
DamageInflictor |
Brings more complex logic related to giving out damage. This is optional - every entity will expose damage() to inflict basic damage to another entity. |
entity/Weapons.mjs |
Explosions |
A streamlined way to turn any entity into an explosion with proper effects and damage radius. | entity/Weapons.mjs |
These base classes make it easy to create new entities with common behaviors:
| Class | Purpose | Location |
|---|---|---|
BaseItemEntity |
Allows easily creating entities containing an item or ammo. This base class provides all logic connected to target handling, respawning (multiplayer games), sound effects etc. | entity/Items.mjs |
BaseKeyEntity |
Base for keys. Main differences from items are sounds, regeneration behavior, and keys not being removed after pickup. | entity/Items.mjs |
BaseWeaponEntity |
Weapons are based on items, only the sound is different. | entity/Items.mjs |
BaseAmmoEntity |
Base class for ammunition pickups (shells, nails, rockets, cells). | entity/Items.mjs |
BaseProjectile |
A moving object that will cause something upon impact. Used for spikes, rockets, grenades. | entity/Weapons.mjs |
BaseTriggerEntity |
Convenient base class to make any kind of triggers. | entity/Triggers.mjs |
BaseLightEntity |
Handles anything related to light entities (torches, globes, fluorescent lights, etc.). | entity/Misc.mjs |
BasePropEntity |
Base class to support platforms, doors, trains etc. Provides movement state machine. | entity/props/BasePropEntity.mjs |
BaseDoorEntity |
Base class to handle doors and secret doors with key support and linking. | entity/props/Doors.mjs |
BaseMonster |
Base class for all monsters. Provides AI, damage handling, gibbing, and common monster behaviors. | entity/monster/BaseMonster.mjs |
- Access through properties
- Engine may write to things like
groundentity,effectsetc. - Engine will read from things like
origin,anglesetc.
- Engine may write to things like
- Access through methods
- Engine will communicate with the game through
ServerGameAPIcalling methods likeClientConnectandClientDisconnect, but also with entities directly through methods such astouchandthink. - Game will communicate mainly through the
ServerEngineAPIobject which is augmented by lots of methods declared onBaseEntity.
- Engine will communicate with the game through
Server-side initialization:
PR.Initimports the server game codeServerGameAPI.Init()is called (static) - register console variables here- When server spawns,
new ServerGameAPI(engineAPI)is instantiated - Map loads, entities spawn via
entityRegistry
Client-side initialization:
CL.Initimports the client game codeClientGameAPI.Init()is called (static) - client-side setup- When connecting,
new ClientGameAPI(engineAPI)is instantiated - HUD and effects are initialized
There’s a way to store information across maps. This is done by Spawn Parameters. Classic Quake uses SetSpawnParms, SetNewParms, SetChangeParms, parm0..15`.
By enabling the CAP_SPAWNPARMS_DYNAMIC flag, the engine will not use the classic API, but a modern API:
- Engine can call:
saveSpawnParameters(): stringfor clientsrestoreSpawnParameters(data: string)for clients
That API will allow for more complex serialization/deserialization of spawn parameters.
A game is limited by a map. Every map starts a new game. The engine may prepare the game state by filling parm0 to parm15 and calling SetSpawnParms.
The server has to run every edict and it will run every edict, when certain conditions are met (e.g. nextthink is due).
ServerGameAPI.StartFrame- Server goes over all active entities, for each of them:
- if it’s a player, it will go the Player Think route instead
- it will execute physics engine code
- invoke
entity.think()afterwards.
ServerGameAPI.PlayerPreThink- Server executes the physics engine code.
ServerGameAPI.PlayerPostThink
ClientEntities.thinkexecute client-side thinkingClientEntities.emitentity is staged for rendering in this frame
-
Whenever a client connects, the server is calling:
- Set a new
playerentity, settingnetname(player name),colormap,team. (Subject to change) Spawn parameters (parm0..15) are copied from client to the game object. (Subject to change)- Spawn parameters are restored by invoking
restoreSpawnParameters. ServerGameAPI.ClientConnectServerGameAPI.PutClientInServer
- Set a new
-
When a client disconnects or drops, the server is calling:
ServerGameAPI.ClientDisconnect