Skip to content

Commit 3a33cca

Browse files
committed
Games Deathmatch API: Allow games with any # of lives
1 parent c063bee commit 3a33cca

File tree

7 files changed

+65
-141
lines changed

7 files changed

+65
-141
lines changed

javascript/features/games_deathmatch/deathmatch_description.js

+7-13
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import { SpawnWeaponsSetting } from 'features/games_deathmatch/settings/spawn_we
99
export class DeathmatchDescription {
1010
// Available values for some of the enumeration-based deathmatch options.
1111
static kMapMarkerOptions = ['Enabled', 'Team only', 'Disabled'];
12-
static kObjectiveOptions =
13-
['Last man standing', 'Best of...', 'First to..', 'Time limit...', 'Continuous'];
12+
static kObjectiveOptions = ['Continuous', 'Number of lives...', 'Time limit...'];
1413
static kTeamOptions = [ 'Balanced teams', 'Free for all', 'Randomized teams' ];
1514

1615
// Whether players should be subject to lag compensation during this game.
@@ -66,23 +65,19 @@ export class DeathmatchDescription {
6665
throw new Error(`[${this.name}] Invalid value given for the objective type.`);
6766

6867
switch (options.objective.type) {
69-
case 'Last man standing':
7068
case 'Continuous':
7169
break;
7270

73-
case 'Best of...':
74-
if (!options.objective.hasOwnProperty('rounds'))
75-
throw new Error(`[${this.name}] The objective.rounds option is missing.`);
76-
break;
71+
case 'Number of lives...':
72+
if (!options.objective.hasOwnProperty('lives'))
73+
throw new Error(`[${this.name}] The objective.lives option is missing.`);
7774

78-
case 'First to...':
79-
if (!options.objective.hasOwnProperty('kills'))
80-
throw new Error(`[${this.name}] The objective.kills option is missing.`);
8175
break;
82-
76+
8377
case 'Time limit...':
8478
if (!options.objective.hasOwnProperty('seconds'))
8579
throw new Error(`[${this.name}] The objective.seconds option is missing.`);
80+
8681
break;
8782
}
8883

@@ -92,8 +87,7 @@ export class DeathmatchDescription {
9287
type: settings.getValue('games/deathmatch_objective_default'),
9388

9489
// We don't know what the given |type| is, so just duplicate the value for each...
95-
rounds: settings.getValue('games/deathmatch_objective_value_default'),
96-
kills: settings.getValue('games/deathmatch_objective_value_default'),
90+
lives: settings.getValue('games/deathmatch_objective_value_default'),
9791
seconds: settings.getValue('games/deathmatch_objective_value_default'),
9892
};
9993
}

javascript/features/games_deathmatch/deathmatch_description.test.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ describe('DeathmatchDescription', it => {
2222
type: settings.getValue('games/deathmatch_objective_default'),
2323

2424
// The specific values are duplicated for the default value.
25-
rounds: settings.getValue('games/deathmatch_objective_value_default'),
26-
kills: settings.getValue('games/deathmatch_objective_value_default'),
25+
lives: settings.getValue('games/deathmatch_objective_value_default'),
2726
seconds: settings.getValue('games/deathmatch_objective_value_default'),
2827
});
2928

@@ -54,7 +53,7 @@ describe('DeathmatchDescription', it => {
5453
const description = new DeathmatchDescription(/* description= */ null, {
5554
lagCompensation: true,
5655
mapMarkers: 'Team only',
57-
objective: { type: 'Best of...', rounds: 3 },
56+
objective: { type: 'Time limit...', seconds: 180 },
5857
skin: 121,
5958
spawnArmour: true,
6059
spawnWeapons: [ { weapon: 16, ammo: 50 } ],
@@ -65,7 +64,7 @@ describe('DeathmatchDescription', it => {
6564

6665
assert.isTrue(description.lagCompensation);
6766
assert.equal(description.mapMarkers, 'Team only');
68-
assert.deepEqual(description.objective, { type: 'Best of...', rounds: 3 });
67+
assert.deepEqual(description.objective, { type: 'Time limit...', seconds: 180 });
6968
assert.equal(description.skin, 121);
7069
assert.isTrue(description.spawnArmour);
7170
assert.deepEqual(description.spawnWeapons, [ { weapon: 16, ammo: 50 } ]);

javascript/features/games_deathmatch/deathmatch_game.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,9 @@ export class DeathmatchGame extends GameBase {
2525
static kMapMarkersDisabled = 'Disabled';
2626

2727
// The objective which defines the winning conditions of this game.
28-
static kObjectiveLastManStanding = 'Last man standing';
29-
static kObjectiveBestOf = 'Best of...';
30-
static kObjectiveFirstTo = 'First to...';
31-
static kObjectiveTimeLimit = 'Time limit...';
3228
static kObjectiveContinuous = 'Continuous';
29+
static kObjectiveLives = 'Number of lives...';
30+
static kObjectiveTimeLimit = 'Time limit...';
3331

3432
// Indicates which team a player can be part of. Individuals are always part of team 0, whereas
3533
// players can be part of either Team Alpha or Team Bravo in team-based games.
@@ -120,7 +118,7 @@ export class DeathmatchGame extends GameBase {
120118
this.#objective_ = new ContinuousObjective();
121119
break;
122120

123-
case DeathmatchGame.kObjectiveLastManStanding:
121+
case DeathmatchGame.kObjectiveLives:
124122
this.#objective_ = new LivesObjective();
125123
break;
126124

javascript/features/games_deathmatch/objectives/lives_objective.test.js

+16-7
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { getGameInstance, runGameLoop } from 'features/games/game_test_helpers.j
99

1010
describe('LivesObjective', it => {
1111
const kObjectiveIndex = 5;
12-
const kObjectiveLastManStandingIndex = 0;
12+
const kObjectiveLivesIndex = 0;
1313

14-
it('should be possible to have games with a time limit', async (assert) => {
14+
it('should be possible to have games with a number of lives', async (assert) => {
1515
const feature = server.featureManager.loadFeature('games_deathmatch');
1616
const settings = server.featureManager.loadFeature('settings');
1717

@@ -34,7 +34,8 @@ describe('LivesObjective', it => {
3434
// Mimic the flow where the game's objective is decided by the player through the menu,
3535
// which is what will get least in-game coverage when doing manual testing.
3636
gunther.respondToDialog({ listitem: kObjectiveIndex }).then(
37-
() => gunther.respondToDialog({ listitem: kObjectiveLastManStandingIndex })).then(
37+
() => gunther.respondToDialog({ listitem: kObjectiveLivesIndex })).then(
38+
() => gunther.respondToDialog({ inputtext: '2' /* lives */ })).then(
3839
() => gunther.respondToDialog({ listitem: 0 /* start the game */ }));
3940

4041
assert.isTrue(await gunther.issueCommand('/bubble custom'));
@@ -47,14 +48,22 @@ describe('LivesObjective', it => {
4748
assert.doesNotThrow(() => getGameInstance());
4849
assert.instanceOf(getGameInstance().objectiveForTesting, LivesObjective);
4950

50-
// TODO: Support multiple lives.
51+
// The game ends when either Russell or Gunther dies twice. We kill Gunther first.
52+
gunther.die();
53+
54+
await runGameLoop(); // allow the game to finish, if it would.
55+
assert.doesNotThrow(() => getGameInstance());
56+
57+
// Kill Russell, after which the game will be in sudden death mode.
58+
russell.die();
5159

52-
// The game ends when either Russell or Gunther dies. We kill Gunther, so this should mark
53-
// Russell as the winner of the game, and Gunther as the loser.
60+
await runGameLoop(); // allow the game to finish, if it would.
61+
assert.doesNotThrow(() => getGameInstance());
62+
63+
// Now kill Gunther again. This will mark Russell as the winner of the match.
5464
gunther.die();
5565

5666
await runGameLoop(); // allow the game to finish
57-
5867
assert.throws(() => getGameInstance());
5968
});
6069
});

javascript/features/games_deathmatch/settings/objective_setting.js

+18-52
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,8 @@ export class ObjectiveSetting extends GameCustomSetting {
2626
// with the specialization given by the player, e.g. best of X.
2727
getCustomizationDialogValue(currentValue) {
2828
switch (currentValue.type) {
29-
case 'Best of...':
30-
return format('Best of %d rounds', currentValue.kills);
31-
32-
case 'First to...':
33-
return format('First to %d kills', currentValue.kills);
29+
case 'Number of lives...':
30+
return format('Lives (%d)', currentValue.lives);
3431

3532
case 'Time limit...':
3633
return format('Time limit (%s)', timeDifferenceToString(currentValue.seconds));
@@ -49,17 +46,9 @@ export class ObjectiveSetting extends GameCustomSetting {
4946

5047
const options = [
5148
[
52-
'Last man standing',
53-
ObjectiveSetting.prototype.applyOption.bind(this, settings, 'Last man standing')
54-
],/*
55-
[
56-
'Best of...',
57-
ObjectiveSetting.prototype.handleBestOfSetting.bind(this, settings, player)
49+
'Number of lives...',
50+
ObjectiveSetting.prototype.handleNumberOfLivesSetting.bind(this, settings, player)
5851
],
59-
[
60-
'First to...',
61-
ObjectiveSetting.prototype.handleFirstToSetting.bind(this, settings, player)
62-
],*/
6352
[
6453
'Time limit...',
6554
ObjectiveSetting.prototype.handleTimeLimitSetting.bind(this, settings, player)
@@ -83,45 +72,25 @@ export class ObjectiveSetting extends GameCustomSetting {
8372
// Applies the given |option| as the desired objective.
8473
applyOption(settings, option) { settings.set('deathmatch/objective', { type: option }); }
8574

86-
// Handles the case where the objective should be "Best of X rounds".
87-
async handleBestOfSetting(settings, player) {
88-
const rounds = await Question.ask(player, {
89-
question: 'Game objective',
90-
message: `Please enter the number of rounds for the game.`,
91-
constraints: {
92-
validation: isNumberInRange.bind(null, 2, 50),
93-
explanation: 'The number of rounds must be between 2 and 50.',
94-
abort: 'You need to give a reasonable number of rounds for the game.',
95-
}
96-
});
97-
98-
if (!rounds)
99-
return null;
100-
101-
settings.set('deathmatch/objective', {
102-
type: 'Best of...',
103-
kills: parseInt(rounds, 10),
104-
});
105-
}
106-
107-
// Handles the case where the objective should be "First to X kills".
108-
async handleFirstToSetting(settings, player) {
109-
const kills = await Question.ask(player, {
75+
// Handles the case where the objective should be "Number of lives...". This allows the person
76+
// creating the game to choose a sensible number of lives for each participant.
77+
async handleNumberOfLivesSetting(settings, player) {
78+
const lives = await Question.ask(player, {
11079
question: 'Game objective',
111-
message: `Please enter the number of kills for the game.`,
80+
message: `Please enter the number of lives each participant has.`,
11281
constraints: {
113-
validation: isNumberInRange.bind(null, 2, 50),
114-
explanation: 'The number of kills must be between 2 and 50.',
115-
abort: 'You need to give a reasonable number of kills for the game.',
82+
validation: isNumberInRange.bind(null, 1, 50),
83+
explanation: 'The number of lives must be between 1 and 50.',
84+
abort: 'You need to give a reasonable number of lives for the game.',
11685
}
11786
});
11887

119-
if (!kills)
88+
if (!lives)
12089
return null;
12190

12291
settings.set('deathmatch/objective', {
123-
type: 'First to...',
124-
kills: parseInt(kills, 10),
92+
type: 'Number of lives...',
93+
lives: parseInt(lives, 10),
12594
});
12695
}
12796

@@ -132,7 +101,7 @@ export class ObjectiveSetting extends GameCustomSetting {
132101
message: `Please enter the game's time limit in seconds.`,
133102
constraints: {
134103
validation: isNumberInRange.bind(null, 30, 1800),
135-
explanation: 'The time limit must be between 30 seconds and 1800 seconds, which ' +
104+
explanation: 'The time limit must be between 30 seconds and 1,800 seconds, which ' +
136105
'is 30 minutes.',
137106
abort: 'You need to give a reasonable time limit for the game.',
138107
}
@@ -163,12 +132,9 @@ export class ObjectiveSetting extends GameCustomSetting {
163132
return '';
164133

165134
switch (option) {
166-
case 'Best of...':
167-
return format('{FFFF00}%d rounds', currentValue.kills);
135+
case 'Number of lives...':
136+
return format('{FFFF00}%d lives', currentValue.lives);
168137

169-
case 'First to...':
170-
return format('{FFFF00}%d kills', currentValue.kills);
171-
172138
case 'Time limit...':
173139
return format('{FFFF00}%s', timeDifferenceToString(currentValue.seconds));
174140
}

javascript/features/games_deathmatch/settings/objective_setting.test.js

+16-58
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('GameCustomSetting', (it, beforeEach) => {
3030
settingsFrozen: GameDescription.kDefaultSettings,
3131
settings: [
3232
new Setting(
33-
'deathmatch', 'objective', new ObjectiveSetting, { type: 'Last man standing' },
33+
'deathmatch', 'objective', new ObjectiveSetting, { type: 'Continuous' },
3434
'Objective')
3535
],
3636
});
@@ -57,7 +57,7 @@ describe('GameCustomSetting', (it, beforeEach) => {
5757
],
5858
[
5959
'Objective',
60-
'Last man standing',
60+
'Continuous',
6161
]
6262
]);
6363

@@ -69,72 +69,30 @@ describe('GameCustomSetting', (it, beforeEach) => {
6969

7070
assert.isTrue(settings.has('deathmatch/objective'));
7171
assert.typeOf(settings.get('deathmatch/objective'), 'object');
72-
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Last man standing' });
73-
74-
// (3) Gunther is able to start a Last man standing game.
75-
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
76-
() => gunther.respondToDialog({ listitem: 0 /* Last man standing */ })).then(
77-
() => gunther.respondToDialog({ listitem: 0 /* Start the game! */ }));
78-
79-
settings = await commands.determineSettings(description, gunther, params);
80-
assert.isNotNull(settings);
81-
82-
assert.isTrue(settings.has('deathmatch/objective'));
83-
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Last man standing' });
84-
85-
assert.equal(gunther.getLastDialogAsTable().rows.length, 3);
86-
assert.deepEqual(gunther.getLastDialogAsTable().rows[2], [
87-
'Objective',
88-
'Last man standing',
89-
]);
90-
91-
if (false) {
92-
// (4) Gunther is able to start a Best of... game.
93-
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
94-
() => gunther.respondToDialog({ listitem: 1 /* Best of... */ })).then(
95-
() => gunther.respondToDialog({ inputtext: 'banana' /* invalid */ })).then(
96-
() => gunther.respondToDialog({ response: 1 /* try again */ })).then(
97-
() => gunther.respondToDialog({ inputtext: '9999' /* invalid */ })).then(
98-
() => gunther.respondToDialog({ response: 1 /* try again */ })).then(
99-
() => gunther.respondToDialog({ inputtext: '42' /* valid */ })).then(
100-
() => gunther.respondToDialog({ listitem: 0 /* Start the game! */ }));
101-
102-
settings = await commands.determineSettings(description, gunther, params);
103-
assert.isNotNull(settings);
104-
105-
assert.isTrue(settings.has('deathmatch/objective'));
106-
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Best of...', kills: 42 });
107-
108-
assert.equal(gunther.getLastDialogAsTable().rows.length, 3);
109-
assert.deepEqual(gunther.getLastDialogAsTable().rows[2], [
110-
'Objective',
111-
'{FFFF00}Best of 42 rounds',
112-
]);
72+
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Continuous' });
11373

114-
// (5) Gunther is able to start a First to... game.
74+
// (3) Gunther is able to start a # of lives game.
11575
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
116-
() => gunther.respondToDialog({ listitem: 2 /* First to... */ })).then(
117-
() => gunther.respondToDialog({ inputtext: 'banana' /* invalid */ })).then(
118-
() => gunther.respondToDialog({ response: 1 /* try again */ })).then(
119-
() => gunther.respondToDialog({ inputtext: '9999' /* invalid */ })).then(
120-
() => gunther.respondToDialog({ response: 1 /* try again */ })).then(
121-
() => gunther.respondToDialog({ inputtext: '31' /* valid */ })).then(
76+
() => gunther.respondToDialog({ listitem: 0 /* Number of lives... */ })).then(
77+
() => gunther.respondToDialog({ inputtext: '5' /* five lives */ })).then(
12278
() => gunther.respondToDialog({ listitem: 0 /* Start the game! */ }));
12379

12480
settings = await commands.determineSettings(description, gunther, params);
12581
assert.isNotNull(settings);
12682

12783
assert.isTrue(settings.has('deathmatch/objective'));
128-
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'First to...', kills: 31 });
84+
assert.deepEqual(settings.get('deathmatch/objective'), {
85+
type: 'Number of lives...',
86+
lives: 5,
87+
});
12988

13089
assert.equal(gunther.getLastDialogAsTable().rows.length, 3);
13190
assert.deepEqual(gunther.getLastDialogAsTable().rows[2], [
13291
'Objective',
133-
'{FFFF00}First to 31 kills',
92+
'{FFFF00}Lives (5)',
13493
]);
135-
}
13694

137-
// (6) Gunther is able to start a Time limit... game.
95+
// (4) Gunther is able to start a Time limit... game.
13896
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
13997
() => gunther.respondToDialog({ listitem: 1 /* Time limit... */ })).then(
14098
() => gunther.respondToDialog({ inputtext: 'banana' /* invalid */ })).then(
@@ -157,7 +115,7 @@ if (false) {
157115
'{FFFF00}Time limit (12 minutes)',
158116
]);
159117

160-
// (7) Gunther is able to start a Continuous game.
118+
// (5) Gunther is able to start a Continuous game.
161119
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
162120
() => gunther.respondToDialog({ listitem: 2 /* Continuous */ })).then(
163121
() => gunther.respondToDialog({ listitem: 0 /* Start the game! */ }));
@@ -171,10 +129,10 @@ if (false) {
171129
assert.equal(gunther.getLastDialogAsTable().rows.length, 3);
172130
assert.deepEqual(gunther.getLastDialogAsTable().rows[2], [
173131
'Objective',
174-
'{FFFF00}Continuous',
132+
'Continuous', // default value
175133
]);
176134

177-
// (8) Gunther should be able to opt out of the dialog immediately.
135+
// (6) Gunther should be able to opt out of the dialog immediately.
178136
gunther.respondToDialog({ listitem: 2 /* Objective */ }).then(
179137
() => gunther.respondToDialog({ listitem: 1 /* Time limit... */ })).then(
180138
() => gunther.respondToDialog({ inputtext: 'banana' /* invalid */ })).then(
@@ -185,6 +143,6 @@ if (false) {
185143
assert.isNotNull(settings);
186144

187145
assert.isTrue(settings.has('deathmatch/objective'));
188-
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Last man standing' });
146+
assert.deepEqual(settings.get('deathmatch/objective'), { type: 'Continuous' });
189147
});
190148
});

0 commit comments

Comments
 (0)