Skip to content

Commit

Permalink
Begin mocking out a generic API for deathmatch games
Browse files Browse the repository at this point in the history
  • Loading branch information
RussellLVP committed Jun 30, 2020
1 parent 084b7de commit 1247675
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 1 deletion.
3 changes: 3 additions & 0 deletions javascript/entities/player.js
Expand Up @@ -64,6 +64,9 @@ export class Player extends Supplementable {
// Default gravity value
static kDefaultGravity = 0.008;

// Default lag compensation mode.
static kDefaultLagCompensationMode = 2;

// Constants applicable to the `Player.specialAction` property.
static kSpecialActionNone = 0;
static kSpecialActionCrouching = 1; // read-only
Expand Down
5 changes: 5 additions & 0 deletions javascript/features/games/game_description.js
Expand Up @@ -40,6 +40,7 @@ export class GameDescription {
static kScoreTime = 1;

gameConstructor_ = null;
options_ = null;

name_ = null;
goal_ = null;
Expand All @@ -64,6 +65,9 @@ export class GameDescription {
// Gets the constructor which can be used to instantiate the game.
get gameConstructor() { return this.gameConstructor_; }

// Gets the unprocessed options that were used for initializing this game.
get options() { return this.options_; }

// ---------------------------------------------------------------------------------------------
// Required configuration
// ---------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -157,6 +161,7 @@ export class GameDescription {
throw new Error('Each game must override the `Game` base class in this feature.');

this.gameConstructor_ = gameConstructor;
this.options_ = options;

// -----------------------------------------------------------------------------------------
// Section: required options
Expand Down
29 changes: 29 additions & 0 deletions javascript/features/games_deathmatch/README.md
@@ -0,0 +1,29 @@
# Games Deathmatch API
This feature contains extended functionality on top of the [Games API](../games/) that makes it
easier to build deathmatch games for Las Venturas Playground, by providing well considered options
that apply to most sort of contests.

## How to use the Games Deathmatch API?
Where you would normally register a game with the `Games.registerGame()` function, you will be using
the `GamesDeathmatch.registerGame()` function instead. Pass in a class that extends
[DeathmatchGame](deathmatch_game.js), and you'll be able to use all of the extra functionality and
[options][1] provided by this feature.

Your feature must depend on the `games_deathmatch` instead of the `games` feature.

## Options when registering a game
The following options will be made available in addition to the [default ones][1].

Option | Description
--------------------|--------------
`lagCompensation` | Whether lag compensation should be enabled. Defaults to `true`.

## Settings when starting a game
The following settings will be made available to all deathmatch games, and can be customized by
players as they see fit. Specialized interfaces will be offered where appropriate.

Setting | Description
--------------------|--------------
`Lag compensation` | Whether lag compensation should be enabled. Defaults to the option's value.

[1]: ../games#options-when-registering-a-game
30 changes: 30 additions & 0 deletions javascript/features/games_deathmatch/deathmatch_description.js
@@ -0,0 +1,30 @@
// Copyright 2020 Las Venturas Playground. All rights reserved.
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.

// Specialised version of the `GameDescription` class that controls and validates all deathmatch-
// related functionality added by this feature.
export class DeathmatchDescription {
#description_ = null;

lagCompensation_ = false;

// ---------------------------------------------------------------------------------------------

// Gets whether players should be subject to lag compensation during this game.
get lagCompensation() { return this.lagCompensation_; }

// ---------------------------------------------------------------------------------------------

constructor(description, manualOptions = null) {
this.#description_ = description;

const options = manualOptions || description.options;
if (options.hasOwnProperty('lagCompensation')) {
if (typeof options.lagCompensation !== 'boolean')
throw new Error(`[${this.name}] The lag compensation flag must be a boolean.`);

this.lagCompensation_ = options.lagCompensation;
}
}
}
@@ -0,0 +1,21 @@
// Copyright 2020 Las Venturas Playground. All rights reserved.
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.

import { DeathmatchDescription } from 'features/games_deathmatch/deathmatch_description.js';

describe('DeathmatchDescription', it => {
it('should have sensible default values', assert => {
const description = new DeathmatchDescription(/* description= */ null, /* options= */ {});

assert.isFalse(description.lagCompensation);
});

it('should be able to take configuration from an object of options', assert => {
const description = new DeathmatchDescription(/* description= */ null, {
lagCompensation: true,
});

assert.isTrue(description.lagCompensation);
});
});
33 changes: 33 additions & 0 deletions javascript/features/games_deathmatch/deathmatch_game.js
@@ -0,0 +1,33 @@
// Copyright 2020 Las Venturas Playground. All rights reserved.
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.

import { Game } from 'features/games/game.js';

// Implementation of the `Game` interface which extends it with deathmatch-related functionality. It
// exposes methods that should be called before game-specific behaviour, i.e. through super calls.
export class DeathmatchGame extends Game {
// Whether lag compensation mode should be enabled for this game.
#lagCompensation_ = false;

async onInitialized(settings) {
await super.onInitialized(settings);

// Import the settings from the |settings|, which may have been customised by the player.
this.#lagCompensation_ = settings.get('deathmatch/lag_compensation');
}

async onPlayerAdded(player) {
await super.onPlayerAdded(player);

if (!this.#lagCompensation_)
player.syncedData.lagCompensationMode = /* disabled= */ 0;
}

async onPlayerRemoved(player) {
await super.onPlayerRemoved(player);

if (!this.#lagCompensation_)
player.syncedData.lagCompensationMode = Player.kDefaultLagCompensationMode;
}
}
115 changes: 115 additions & 0 deletions javascript/features/games_deathmatch/games_deathmatch.js
@@ -0,0 +1,115 @@
// Copyright 2020 Las Venturas Playground. All rights reserved.
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.

import { DeathmatchDescription } from 'features/games_deathmatch/deathmatch_description.js';
import { Feature } from 'components/feature_manager/feature.js';
import { Setting } from 'entities/setting.js';

import { clone } from 'base/clone.js';

// Determines if the given |gameConstructor| has a class named "DeathmatchGame" in its prototype
// chain. We cannot use `isPrototypeOf` here, since the actual instances might be subtly different
// when live reload has been used on the server.
function hasDeathmatchGameInPrototype(gameConstructor) {
let currentConstructor = gameConstructor;
while (currentConstructor.name && currentConstructor.name !== 'DeathmatchGame')
currentConstructor = currentConstructor.__proto__;

return currentConstructor.name === 'DeathmatchGame';
}

// Feature class for the GamesDeathmatch feature, which adds a deathmatch layer of functionality on
// top of the common Games API. The public API of this feature is identical to that offered by the
// Games class, but with additional verification and preparation in place.
export default class GamesDeathmatch extends Feature {
gameConstructors_ = new Map();
games_ = null;
settings_ = null;

constructor() {
super();

// This feature is a layer on top of the Games feature, which provides core functionality.
this.games_ = this.defineDependency('games');
this.games_.addReloadObserver(this, () => this.registerGames());

// Various aspects of the games framework are configurable through `/lvp settings`.
this.settings_ = this.defineDependency('settings');
}

// ---------------------------------------------------------------------------------------------

// Registers the given |gameConstructor|, which will power the game declaratively defined in the
// |options| dictionary. An overview of the available |options| is available in README.md.
registerGame(gameConstructor, options) {
if (!hasDeathmatchGameInPrototype(gameConstructor))
throw new Error(`The given |gameConstructor| must extend the DeathmatchGame class.`);

const amendedOptions = clone(options);

// Construct a `DeathmatchDescription` instance to verify the |options|. This will throw an
// exception when it fails, informing the caller of the issue.
const description = new DeathmatchDescription(/* description= */ null, options);

// Store the |gameConstructor| so that we can silently reload all the games when the Games
// feature reloads. Each user of this class wouldn't necessarily be aware of that.
this.gameConstructors_.set(gameConstructor, options);

// Add the settings to the |options| with default values sourced from the |description|. The
// stored options for re-registering games after reloading will refer to the original ones.
if (!amendedOptions.hasOwnProperty('settings'))
amendedOptions.settings = [];

amendedOptions.settings.push(
// Option: Lag compensation (boolean)
new Setting(
'deathmatch', 'lag_compensation', Setting.TYPE_BOOLEAN, description.lagCompensation,
'Lag compensation'),
);

return this.games_().registerGame(gameConstructor, amendedOptions);
}

// Starts the |gameConstructor| game for the |player|, which must have been registered with the
// game registry already. When set, the |custom| flag will enable the player to customize the
// game's settings when available. Optionally the |registrationId| may be given as well.
startGame(gameConstructor, player, custom = false, registrationId = null) {
return this.games_().startGame(gameConstructor, player, custom, registrationId);
}

// Removes the game previously registered with |gameConstructor| from the list of games that
// are available on the server. In-progress games will be stopped immediately.
removeGame(gameConstructor) {
if (!this.gameConstructors_.has(gameConstructor))
throw new Error(`The given |gameConstructor| is not known to this feature.`);

this.gameConstructors_.delete(gameConstructor);

return this.games_().removeGame(gameConstructor);
}

// ---------------------------------------------------------------------------------------------

// Re-registers all known games with the Games feature, which has been reloaded. This way the
// individual deathmatch games do not have to observe multiple features.
registerGames() {
for (const [ gameConstructor, options ] of this.gameConstructors_)
this.registerGame(gameConstructor, options);
}

// ---------------------------------------------------------------------------------------------

dispose() {
for (const gameConstructor of this.gameConstructors_.keys())
this.games_().removeGame(gameConstructor);

this.gameConstructors_.clear();
this.gameConstructors_ = null;

this.settings_ = null;

this.games_.removeReloadObserver(this);
this.games_ = null;
}
}
33 changes: 33 additions & 0 deletions javascript/features/games_deathmatch/games_deathmatch.test.js
@@ -0,0 +1,33 @@
// Copyright 2020 Las Venturas Playground. All rights reserved.
// Use of this source code is governed by the MIT license, a copy of which can
// be found in the LICENSE file.

import { DeathmatchGame } from 'features/games_deathmatch/deathmatch_game.js';

describe('GamesDeathmatch', (it, beforeEach) => {
let feature = null;
let gunther = null;

beforeEach(() => {
feature = server.featureManager.loadFeature('games_deathmatch');
gunther = server.playerManager.getById(/* Gunther= */ 0);
});

it('automatically re-registers games when the Games feature reloads', async (assert) => {
class BubbleGame extends DeathmatchGame {}

assert.isFalse(server.commandManager.hasCommand('bubble'));

feature.registerGame(BubbleGame, {
name: 'Bubble Fighting Game',
goal: 'Fight each other with bubbles',
command: 'bubble',
});

assert.isTrue(server.commandManager.hasCommand('bubble'));

await server.featureManager.liveReload('games');

assert.isTrue(server.commandManager.hasCommand('bubble'));
});
});
7 changes: 6 additions & 1 deletion javascript/main.js
Expand Up @@ -59,7 +59,7 @@ testRunner.run('.*\.test\.js').then(time => {

// Low level features, which may only depend on each other and foundational features.
'abuse', 'announce', 'economy', 'location', 'minigames',
'streamer', 'games',
'streamer',

// Gang-related features
'gang_chat', 'gang_zones', 'gangs',
Expand All @@ -82,6 +82,11 @@ testRunner.run('.*\.test\.js').then(time => {
// Games and minigames
// -----------------------------------------------------------------------------------------

// 1. Runtime
'games',
'games_deathmatch',

// 2. Individual games
'haystack',

// -----------------------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions javascript/mock_server.js
Expand Up @@ -43,6 +43,7 @@ import Decorations from 'features/decorations/decorations.js';
import Finance from 'features/finance/finance.js';
import Friends from 'features/friends/friends.js';
import Games from 'features/games/games.js';
import GamesDeathmatch from 'features/games_deathmatch/games_deathmatch.js';
import Gangs from 'features/gangs/gangs.js';
import Haystack from 'features/haystack/haystack.js';
import Leaderboard from 'features/leaderboard/leaderboard.js';
Expand Down Expand Up @@ -104,6 +105,7 @@ class MockServer {
finance: Finance,
friends: Friends,
games: Games,
games_deathmatch: GamesDeathmatch,
gangs: Gangs,
haystack: Haystack,
leaderboard: Leaderboard,
Expand Down

0 comments on commit 1247675

Please sign in to comment.