Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Begin mocking out a generic API for deathmatch games
- Loading branch information
1 parent
084b7de
commit 1247675
Showing
10 changed files
with
277 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
30
javascript/features/games_deathmatch/deathmatch_description.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
javascript/features/games_deathmatch/deathmatch_description.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
115
javascript/features/games_deathmatch/games_deathmatch.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
33
javascript/features/games_deathmatch/games_deathmatch.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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')); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters