@kradle/challenges 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/biome.json +39 -0
- package/dist/actions.d.ts +203 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +287 -0
- package/dist/actions.js.map +1 -0
- package/dist/api_utils.d.ts +5 -0
- package/dist/api_utils.d.ts.map +1 -0
- package/dist/api_utils.js +13 -0
- package/dist/api_utils.js.map +1 -0
- package/dist/challenge.d.ts +56 -0
- package/dist/challenge.d.ts.map +1 -0
- package/dist/challenge.js +462 -0
- package/dist/challenge.js.map +1 -0
- package/dist/commands.d.ts +38 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +135 -0
- package/dist/commands.js.map +1 -0
- package/dist/constants.d.ts +70 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +112 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/testmode.d.ts +3 -0
- package/dist/testmode.d.ts.map +1 -0
- package/dist/testmode.js +19 -0
- package/dist/testmode.js.map +1 -0
- package/dist/types.d.ts +103 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +22 -0
- package/dist/utils.js.map +1 -0
- package/package.json +27 -0
- package/src/actions.ts +388 -0
- package/src/api_utils.ts +10 -0
- package/src/challenge.ts +553 -0
- package/src/commands.ts +141 -0
- package/src/constants.ts +121 -0
- package/src/index.ts +4 -0
- package/src/testmode.ts +18 -0
- package/src/types.ts +136 -0
- package/src/utils.ts +22 -0
- package/tsconfig.json +38 -0
package/src/challenge.ts
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
_,
|
|
4
|
+
Advancement,
|
|
5
|
+
comment,
|
|
6
|
+
execute,
|
|
7
|
+
MCFunction,
|
|
8
|
+
type OBJECTIVE_CRITERION,
|
|
9
|
+
Objective,
|
|
10
|
+
type ObjectiveInstance,
|
|
11
|
+
type Score,
|
|
12
|
+
Selector,
|
|
13
|
+
savePack,
|
|
14
|
+
scoreboard,
|
|
15
|
+
tag,
|
|
16
|
+
} from "sandstone";
|
|
17
|
+
import type { ConditionType } from "sandstone/flow";
|
|
18
|
+
import { Actions, mapTarget } from "./actions";
|
|
19
|
+
import { Commands } from "./commands";
|
|
20
|
+
import {
|
|
21
|
+
ALL,
|
|
22
|
+
BUILTIN_SCORE_EVENTS,
|
|
23
|
+
BUILTIN_VARIABLES,
|
|
24
|
+
GAME_STATES,
|
|
25
|
+
HIDDEN_OBJECTIVE,
|
|
26
|
+
MAX_DURATION,
|
|
27
|
+
VISIBLE_OBJECTIVE,
|
|
28
|
+
WINNER_TAG,
|
|
29
|
+
} from "./constants";
|
|
30
|
+
import { set_up_test_mode, test_mode_enabled } from "./testmode";
|
|
31
|
+
import {
|
|
32
|
+
type _BaseConfig,
|
|
33
|
+
type _InputAdvancementCustomEventType,
|
|
34
|
+
type _InputCustomEventType,
|
|
35
|
+
type _InputScoreCustomEventType,
|
|
36
|
+
type _InputVariableType,
|
|
37
|
+
type _ScenarioEvents,
|
|
38
|
+
type CustomScoreEvent,
|
|
39
|
+
type FullVariable,
|
|
40
|
+
type FullVariables,
|
|
41
|
+
isSpecificObjectiveVariable,
|
|
42
|
+
type Roles,
|
|
43
|
+
type Variables,
|
|
44
|
+
} from "./types";
|
|
45
|
+
import { getChildFunctionName } from "./utils";
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_CHALLENGE_PATH = "kradle-studio/challenges";
|
|
48
|
+
|
|
49
|
+
const PREVIOUS_VALUE_PREFIX = "_prev_";
|
|
50
|
+
const PREVIOUS_VALUES_GLOBAL_OBJECTIVE_NAME = "prev_glob_values";
|
|
51
|
+
|
|
52
|
+
const EVENT_FIRES_COUNTER_PREFIX = "_count_";
|
|
53
|
+
const EVENT_FIRES_GLOBAL_COUNTER_OBJECTIVE_NAME = "fired_ev_counter";
|
|
54
|
+
|
|
55
|
+
let uniqueObjectiveCounter = 0;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Adds a suffix to the objective name while ensuring it fits within the 16 character limit.
|
|
59
|
+
*/
|
|
60
|
+
function addSuffixToObjectiveName(name: string, suffix: string): string {
|
|
61
|
+
const trimmedName = name.slice(0, 16 - suffix.length);
|
|
62
|
+
return `${trimmedName}${suffix}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Makes sure the objective name is unique by appending a counter to it.
|
|
67
|
+
* This is necessary because Minecraft requires objective names to be unique,
|
|
68
|
+
* as well as to fit within the 16 character limit.
|
|
69
|
+
*
|
|
70
|
+
* This is meant to be used for mass-generated objectives (previous values, counter of fired events...)
|
|
71
|
+
*/
|
|
72
|
+
function getUniqueObjectiveName(name: string): string {
|
|
73
|
+
uniqueObjectiveCounter++;
|
|
74
|
+
|
|
75
|
+
return addSuffixToObjectiveName(name, uniqueObjectiveCounter.toString());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns an Objective instance for the given name and type.
|
|
80
|
+
* If the objective already exists, it returns the existing one.
|
|
81
|
+
* This is useful to avoid creating multiple objectives with the same name.
|
|
82
|
+
*/
|
|
83
|
+
function getOrCreateObjective(name: string, type: OBJECTIVE_CRITERION): ObjectiveInstance {
|
|
84
|
+
try {
|
|
85
|
+
return Objective.create(name, type);
|
|
86
|
+
} catch (_) {
|
|
87
|
+
return Objective.get(name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isScoreCustomEvent(event: _InputCustomEventType): event is _InputScoreCustomEventType {
|
|
92
|
+
return "score" in event;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isAdvancementCustomEvent(event: _InputCustomEventType): event is _InputAdvancementCustomEventType {
|
|
96
|
+
return "criteria" in event;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class ChallengeBase<const ROLES_NAMES extends string, const VARIABLE_NAMES extends string> {
|
|
100
|
+
private game_duration: number;
|
|
101
|
+
private roles: Roles<ROLES_NAMES>;
|
|
102
|
+
private full_variables: FullVariables<VARIABLE_NAMES>;
|
|
103
|
+
private variablesPreviousValues: Record<string, Score> | undefined;
|
|
104
|
+
private _events: _ScenarioEvents = {};
|
|
105
|
+
|
|
106
|
+
/* The custom score events provided by the user, still in input type */
|
|
107
|
+
private _userInputCustomEvents: _InputScoreCustomEventType[] = [];
|
|
108
|
+
private _end_condition: ConditionType | undefined;
|
|
109
|
+
private _win_conditions: Record<ROLES_NAMES, ConditionType> | undefined;
|
|
110
|
+
|
|
111
|
+
private name: string;
|
|
112
|
+
private kradle_challenge_path: string;
|
|
113
|
+
|
|
114
|
+
constructor(config: _BaseConfig<ROLES_NAMES, VARIABLE_NAMES>) {
|
|
115
|
+
this.name = config.name;
|
|
116
|
+
this.kradle_challenge_path = config.kradle_challenge_path;
|
|
117
|
+
this.game_duration = config.GAME_DURATION ?? MAX_DURATION;
|
|
118
|
+
|
|
119
|
+
if (this.game_duration < 0) {
|
|
120
|
+
throw new Error("Game duration cannot be negative. Please set it to a positive value (lower than 5 minutes).");
|
|
121
|
+
}
|
|
122
|
+
if (this.game_duration > MAX_DURATION) {
|
|
123
|
+
throw new Error(`Game duration cannot exceed ${MAX_DURATION / 20} seconds.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.roles = Object.fromEntries(config.roles.map((role) => [role, role])) as any;
|
|
127
|
+
|
|
128
|
+
const all_variables = {
|
|
129
|
+
...BUILTIN_VARIABLES,
|
|
130
|
+
...config.custom_variables,
|
|
131
|
+
} as Record<string, _InputVariableType<string>>;
|
|
132
|
+
|
|
133
|
+
this.full_variables = {
|
|
134
|
+
...Object.fromEntries(
|
|
135
|
+
Object.entries(all_variables).map(([key, value]) => {
|
|
136
|
+
if (isSpecificObjectiveVariable(value)) {
|
|
137
|
+
return [
|
|
138
|
+
key,
|
|
139
|
+
{
|
|
140
|
+
type: value.type,
|
|
141
|
+
score: Objective.create(key, value.objective_type)("@s"),
|
|
142
|
+
default: (value as any).default,
|
|
143
|
+
updater: value.updater,
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
if (value.type === "global") {
|
|
148
|
+
const objective = value.hidden ? HIDDEN_OBJECTIVE : VISIBLE_OBJECTIVE;
|
|
149
|
+
return [
|
|
150
|
+
key,
|
|
151
|
+
{
|
|
152
|
+
type: value.type,
|
|
153
|
+
score: objective(key),
|
|
154
|
+
default: value.default,
|
|
155
|
+
updater: value.updater,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
return [
|
|
160
|
+
key,
|
|
161
|
+
{
|
|
162
|
+
type: value.type,
|
|
163
|
+
score: Objective.create(key, "dummy")("@s"),
|
|
164
|
+
default: value.default,
|
|
165
|
+
updater: value.updater,
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
}),
|
|
169
|
+
),
|
|
170
|
+
} as FullVariables<VARIABLE_NAMES>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private get variables(): Variables<VARIABLE_NAMES> {
|
|
174
|
+
return Object.fromEntries(
|
|
175
|
+
Object.entries(this.full_variables).map(([key, value]) => {
|
|
176
|
+
return [key, value.score];
|
|
177
|
+
}),
|
|
178
|
+
) as Variables<VARIABLE_NAMES>;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Build a CustomScoreEvent from an _InputScoreCustomEventType.
|
|
183
|
+
* This will setup the tracking objective.
|
|
184
|
+
*
|
|
185
|
+
* @param event The input event to create the custom event from.
|
|
186
|
+
* @param suffix A suffix that will be added to the variable's debug name, ensuring its uniqueness.
|
|
187
|
+
*/
|
|
188
|
+
private buildCustomScoreEvent(event: _InputScoreCustomEventType, suffix: string | number): CustomScoreEvent {
|
|
189
|
+
// How we store fire counters will depend whether the score is global or individual
|
|
190
|
+
const variableName = this.getVariableName(event.score);
|
|
191
|
+
const variableType = this.getVariableType(event.score);
|
|
192
|
+
const debugName = `${variableName}__${suffix}`;
|
|
193
|
+
|
|
194
|
+
if (variableType === "global") {
|
|
195
|
+
// For global scores, we create a single global objective.
|
|
196
|
+
const objective = getOrCreateObjective(EVENT_FIRES_GLOBAL_COUNTER_OBJECTIVE_NAME, "dummy");
|
|
197
|
+
return {
|
|
198
|
+
inputEvent: event,
|
|
199
|
+
counter: objective(debugName),
|
|
200
|
+
type: variableType,
|
|
201
|
+
debugName: debugName,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// For individual scores, we have to create a dedicated mirror objective
|
|
206
|
+
const objective = getOrCreateObjective(getUniqueObjectiveName(EVENT_FIRES_COUNTER_PREFIX + variableName), "dummy");
|
|
207
|
+
return {
|
|
208
|
+
inputEvent: event,
|
|
209
|
+
counter: objective(event.score.target),
|
|
210
|
+
type: variableType,
|
|
211
|
+
debugName: debugName,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private addVariable(name: string, variable: FullVariable) {
|
|
216
|
+
// @ts-expect-error
|
|
217
|
+
if (this.full_variables[name]) {
|
|
218
|
+
throw new Error(`Variable with name ${name} already exists.`);
|
|
219
|
+
}
|
|
220
|
+
// @ts-expect-error
|
|
221
|
+
this.full_variables[name] = variable;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private getPreviousValueScore(name: string): Score {
|
|
225
|
+
if (!this.variablesPreviousValues) {
|
|
226
|
+
throw new Error("Variables previous values not initialized.");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return this.variablesPreviousValues[name];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
events(
|
|
233
|
+
events: (variables: Variables<VARIABLE_NAMES>, roles: Roles<ROLES_NAMES>) => _ScenarioEvents,
|
|
234
|
+
): Pick<this, "custom_events" | "end_condition" | "win_conditions"> {
|
|
235
|
+
this._events = events(this.variables, this.roles);
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Returns the name of the variable based on its score.
|
|
241
|
+
*/
|
|
242
|
+
private getVariableName(variable: Score): string {
|
|
243
|
+
const variableName = Object.entries(this.full_variables).find(([_name, value]) => value.score === variable);
|
|
244
|
+
if (!variableName) {
|
|
245
|
+
throw new Error(`Variable with score ${variable} not found in variables.`);
|
|
246
|
+
}
|
|
247
|
+
return variableName[0];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Returns the type of the variable (individual/global) based on its score.
|
|
252
|
+
*/
|
|
253
|
+
private getVariableType(score: Score): "individual" | "global" {
|
|
254
|
+
const variableName = this.getVariableName(score);
|
|
255
|
+
if (!variableName) {
|
|
256
|
+
throw new Error(`Score ${score} not found in variables.`);
|
|
257
|
+
}
|
|
258
|
+
return this.full_variables[variableName as VARIABLE_NAMES].type;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
custom_events(
|
|
262
|
+
custom_events: (variables: Variables<VARIABLE_NAMES>, roles: Roles<ROLES_NAMES>) => _InputCustomEventType[],
|
|
263
|
+
): Pick<this, "end_condition" | "win_conditions"> {
|
|
264
|
+
const inputCustomEvents = custom_events(this.variables, this.roles);
|
|
265
|
+
|
|
266
|
+
// First, we translate advancement-based events directly into dedicated scores, and create the related Advancement
|
|
267
|
+
const advancementsAsScoreCustomEvents: _InputScoreCustomEventType[] = inputCustomEvents
|
|
268
|
+
.filter(isAdvancementCustomEvent)
|
|
269
|
+
.map((event, index) => {
|
|
270
|
+
const trigger = (event.criteria?.[0].trigger ?? "").split("minecraft:").at(-1)!.replace(/[:/]/g, "_");
|
|
271
|
+
const name = `adv_${trigger}__${index}`;
|
|
272
|
+
const score = getOrCreateObjective(getUniqueObjectiveName(name), "dummy")("@s");
|
|
273
|
+
|
|
274
|
+
this.addVariable(name, {
|
|
275
|
+
score: score,
|
|
276
|
+
default: 0,
|
|
277
|
+
type: "individual",
|
|
278
|
+
updater: undefined,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const setMCFunction = MCFunction(`custom_events_advancements/${name}`, () => {
|
|
282
|
+
// Called when the advancement is granted. This sets the score to 1 and revokes the advancement.
|
|
283
|
+
score.set(1);
|
|
284
|
+
advancement.revoke(Selector("@s"));
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const advancement = Advancement(name, {
|
|
288
|
+
criteria: Object.fromEntries(
|
|
289
|
+
event.criteria.map((criterion, criterionIndex) => [`criterion_${criterionIndex}`, criterion]),
|
|
290
|
+
),
|
|
291
|
+
rewards: {
|
|
292
|
+
function: setMCFunction,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
actions: [
|
|
298
|
+
// First action is to reset the score to 0
|
|
299
|
+
Actions.set({ variable: score, value: 0 }),
|
|
300
|
+
...event.actions,
|
|
301
|
+
],
|
|
302
|
+
mode: event.mode,
|
|
303
|
+
score: score,
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
this._userInputCustomEvents = [...inputCustomEvents.filter(isScoreCustomEvent), ...advancementsAsScoreCustomEvents];
|
|
308
|
+
return this;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
end_condition(
|
|
312
|
+
condition?: (variables: Variables<VARIABLE_NAMES>, roles: Roles<ROLES_NAMES>) => ConditionType,
|
|
313
|
+
): Pick<this, "win_conditions"> {
|
|
314
|
+
this._end_condition = condition?.(this.variables, this.roles);
|
|
315
|
+
|
|
316
|
+
return this;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
win_conditions(
|
|
320
|
+
condition: (variables: Variables<VARIABLE_NAMES>, roles: Roles<ROLES_NAMES>) => Record<ROLES_NAMES, ConditionType>,
|
|
321
|
+
) {
|
|
322
|
+
this._win_conditions = condition(this.variables, this.roles);
|
|
323
|
+
this.build();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Processes a custom score event by checking if the score has reached the target,
|
|
328
|
+
* and if so, executes the actions associated with the event.
|
|
329
|
+
*
|
|
330
|
+
* @param event - The custom event to process.
|
|
331
|
+
* @param mcFunctionReference - Optional reference to the parent MCFunction - used to give better naming to the children functions.
|
|
332
|
+
*/
|
|
333
|
+
private processCustomScoreEvent(event: CustomScoreEvent): void {
|
|
334
|
+
const { score, target, actions, mode } = event.inputEvent;
|
|
335
|
+
const { type, counter, debugName } = event;
|
|
336
|
+
|
|
337
|
+
const previousScore = this.getPreviousValueScore(this.getVariableName(score));
|
|
338
|
+
|
|
339
|
+
// If no target is specified, we assume the event fires when the score changes from its previous value
|
|
340
|
+
const scoreCondition = target
|
|
341
|
+
? _.and(previousScore.notEqualTo(target), score.equalTo(target))
|
|
342
|
+
: score.notEqualTo(previousScore);
|
|
343
|
+
|
|
344
|
+
// If the mode is "fire_once", we also check if the counter is 0.
|
|
345
|
+
const condition = mode === "repeatable" ? scoreCondition : _.and(scoreCondition, counter.equalTo(0));
|
|
346
|
+
|
|
347
|
+
if (type === "global") {
|
|
348
|
+
// For global scores, we can directly use the condition
|
|
349
|
+
comment(`Processing global custom score event ${debugName}`);
|
|
350
|
+
_.if(condition, () => {
|
|
351
|
+
actions.forEach((action) => action());
|
|
352
|
+
counter.add(1);
|
|
353
|
+
});
|
|
354
|
+
} else {
|
|
355
|
+
// For individual scores, we need to check each player's score
|
|
356
|
+
comment(`Processing individual custom score event ${debugName}`);
|
|
357
|
+
execute.as(ALL).run(getChildFunctionName(debugName), () => {
|
|
358
|
+
_.if(condition, () => {
|
|
359
|
+
actions.forEach((action) => action());
|
|
360
|
+
counter.add(1);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Builds the datapack based on the challenge configuration.
|
|
368
|
+
*/
|
|
369
|
+
private build() {
|
|
370
|
+
// Initialize previous values objectives
|
|
371
|
+
// Will store the previous values of all variables
|
|
372
|
+
this.variablesPreviousValues = Object.fromEntries(
|
|
373
|
+
Object.entries(this.full_variables).map(([name, { type, score }]) => {
|
|
374
|
+
if (type === "global") {
|
|
375
|
+
// For global variables, we create a single global objective to store previous values
|
|
376
|
+
const objective = getOrCreateObjective(PREVIOUS_VALUES_GLOBAL_OBJECTIVE_NAME, "dummy");
|
|
377
|
+
return [name, objective(`${name}__${score.target}`)];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// For individual variables, we create a dedicated mirror objective
|
|
381
|
+
const objective = Objective.create(getUniqueObjectiveName(PREVIOUS_VALUE_PREFIX + name), "dummy");
|
|
382
|
+
return [name, objective(score.target)];
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// "Build" the custom events (initializes the tracking objective)
|
|
387
|
+
const customScoreEvents = [...this._userInputCustomEvents, ...BUILTIN_SCORE_EVENTS(this.variables)].map(
|
|
388
|
+
(event, index) => this.buildCustomScoreEvent(event, index),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Initialize the challenge
|
|
392
|
+
MCFunction("start_challenge", () => {
|
|
393
|
+
comment("1. Setup all the global variables to their default values.");
|
|
394
|
+
comment(" This includes the game timer!");
|
|
395
|
+
this.variables.game_timer.set(this.game_duration);
|
|
396
|
+
for (const variable of Object.values(this.full_variables)) {
|
|
397
|
+
if (variable.type === "global" && variable.default !== undefined) {
|
|
398
|
+
variable.score.set(variable.default);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
tag(ALL).remove(WINNER_TAG);
|
|
402
|
+
|
|
403
|
+
comment("2. Set up test mode if it's enabled - needs to run here so the tester player is tagged");
|
|
404
|
+
if (test_mode_enabled) {
|
|
405
|
+
set_up_test_mode();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
comment("3. Process the start_challenge events");
|
|
409
|
+
this._events.start_challenge?.actions.forEach((action) => action());
|
|
410
|
+
|
|
411
|
+
comment("4. Schedule the init_participants function to run after 1s");
|
|
412
|
+
init_participants.schedule("1s");
|
|
413
|
+
|
|
414
|
+
comment("5. Display the game objective");
|
|
415
|
+
scoreboard.objectives.setDisplay("sidebar", VISIBLE_OBJECTIVE.name);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const init_participants = MCFunction("init_participants", () => {
|
|
419
|
+
comment("1. Setup all individual variables to their default values.");
|
|
420
|
+
for (const [name, variable] of Object.entries(this.full_variables)) {
|
|
421
|
+
const previousValue = this.getPreviousValueScore(name);
|
|
422
|
+
if (typeof previousValue === "undefined") {
|
|
423
|
+
throw new Error(`Previous value for variable ${name} not found.`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (variable.type === "individual" && variable.default !== undefined) {
|
|
427
|
+
execute.as(ALL).run(getChildFunctionName(name), () => {
|
|
428
|
+
variable.score.set(variable.default as number);
|
|
429
|
+
previousValue.set(variable.default as number);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (variable.type === "global" && variable.default !== undefined) {
|
|
434
|
+
variable.score.set(variable.default);
|
|
435
|
+
previousValue.set(variable.default);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
comment("2. Set the events fire counters to 0");
|
|
440
|
+
|
|
441
|
+
for (const event of customScoreEvents) {
|
|
442
|
+
// We could make the distinction between global and individual counters,
|
|
443
|
+
// but it just works to make all players set event fires to 0
|
|
444
|
+
execute.as(ALL).run(() => {
|
|
445
|
+
event.counter.set(0);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
comment("3. Process the init_participants events");
|
|
450
|
+
this._events.init_participants?.actions.forEach((action) => action());
|
|
451
|
+
|
|
452
|
+
comment("4. Set the game state to ON");
|
|
453
|
+
this.full_variables.game_state.score.set(GAME_STATES.ON);
|
|
454
|
+
|
|
455
|
+
//tellraw("@a", [`${DISPLAY_TAG}The challenge has started!`]);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
MCFunction(
|
|
459
|
+
"on_tick",
|
|
460
|
+
() => {
|
|
461
|
+
_.if(this.variables.game_state.equalTo(GAME_STATES.ON), () => {
|
|
462
|
+
comment("1. Run all updaters");
|
|
463
|
+
MCFunction("custom_variable_updaters", () => {
|
|
464
|
+
for (const [name, variable] of Object.entries(this.full_variables)) {
|
|
465
|
+
if (variable.updater) {
|
|
466
|
+
comment(`Updating variable ${name}`);
|
|
467
|
+
if (variable.type === "individual") {
|
|
468
|
+
execute
|
|
469
|
+
.as(ALL)
|
|
470
|
+
.at("@s")
|
|
471
|
+
.run(getChildFunctionName(`update_${name}`), () => {
|
|
472
|
+
variable.updater?.(variable.score, this.variables);
|
|
473
|
+
});
|
|
474
|
+
} else {
|
|
475
|
+
variable.updater(variable.score, this.variables);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
})();
|
|
480
|
+
|
|
481
|
+
comment("2. Process the on_tick event");
|
|
482
|
+
this._events.on_tick?.actions.forEach((action) => action());
|
|
483
|
+
|
|
484
|
+
comment("3. Process custom events");
|
|
485
|
+
MCFunction("process_custom_events", () => {
|
|
486
|
+
// We process all custom events - this checks if the condition is reached & fire them if necessary
|
|
487
|
+
customScoreEvents.forEach((event) => {
|
|
488
|
+
this.processCustomScoreEvent(event);
|
|
489
|
+
});
|
|
490
|
+
})();
|
|
491
|
+
|
|
492
|
+
comment("4. Set previous values for all variables");
|
|
493
|
+
execute.as(ALL).run(getChildFunctionName("set_previous_values"), () => {
|
|
494
|
+
for (const [name, variable] of Object.entries(this.full_variables)) {
|
|
495
|
+
// We could also make the distinction between global and individual variables,
|
|
496
|
+
// but again it just works to run it on all players
|
|
497
|
+
const previousValue = this.getPreviousValueScore(name);
|
|
498
|
+
previousValue.set(variable.score);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
comment("5. Check end conditions");
|
|
503
|
+
|
|
504
|
+
const timerCondition = this.variables.game_timer.equalTo(this.game_duration);
|
|
505
|
+
const endCondition = this._end_condition ? _.or(this._end_condition, timerCondition) : timerCondition;
|
|
506
|
+
|
|
507
|
+
_.if(endCondition, () => {
|
|
508
|
+
//tellraw("@a", [`${DISPLAY_TAG}End condition met! Ending challenge...`]);
|
|
509
|
+
end_challenge();
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
runEveryTick: true,
|
|
515
|
+
},
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const end_challenge = MCFunction("end_challenge", () => {
|
|
519
|
+
comment("1. Set the game state to OFF");
|
|
520
|
+
this.full_variables.game_state.score.set(GAME_STATES.OFF);
|
|
521
|
+
|
|
522
|
+
comment("2. Process the end_challenge events");
|
|
523
|
+
this._events.end_challenge?.actions.forEach((action) => action());
|
|
524
|
+
|
|
525
|
+
comment("3. Announce the winners");
|
|
526
|
+
for (const [role, condition] of Object.entries(this._win_conditions || {})) {
|
|
527
|
+
execute.as(mapTarget(role)).run(getChildFunctionName(`check_winner_team_${role}`), () => {
|
|
528
|
+
_.if(condition as ConditionType, () => {
|
|
529
|
+
Actions.announce({
|
|
530
|
+
message: [Selector("@s"), " has won the challenge!"],
|
|
531
|
+
})();
|
|
532
|
+
tag("@s").add(WINNER_TAG);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
Commands.declareWinners();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
savePack("datapack", {
|
|
541
|
+
customPath: path.join(this.kradle_challenge_path, this.name),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Creates a new challenge with the provided configuration.
|
|
548
|
+
*/
|
|
549
|
+
export function createChallenge<const ROLE_NAMES extends string, const VARIABLE_NAMES extends string>(
|
|
550
|
+
config: _BaseConfig<ROLE_NAMES, VARIABLE_NAMES>,
|
|
551
|
+
): Pick<ChallengeBase<ROLE_NAMES, VARIABLE_NAMES>, "events"> {
|
|
552
|
+
return new ChallengeBase(config);
|
|
553
|
+
}
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { _, type JSONTextComponent, type Score, Selector, type SelectorClass, tellraw } from "sandstone";
|
|
2
|
+
import { WATCHER, WINNER_TAG } from "./constants";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts a Sandstone Selector to a JSON Text Component that will be displayed like a JSON array in the chat.
|
|
6
|
+
* This is used to send selectors as part of JSON messages.
|
|
7
|
+
*
|
|
8
|
+
* @warning
|
|
9
|
+
* This does NOT work with empty selectors (they will be represented as `[""]`).
|
|
10
|
+
* Make sure to check if the selector matches any entities before using this function.
|
|
11
|
+
*
|
|
12
|
+
* @param selector The selector to convert.
|
|
13
|
+
* @returns An array of JSON Text Components that will be displayed as a JSON array in the chat.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const selector = Selector("@a", { tag: "kradle_participant" });
|
|
17
|
+
* const jsonArray = selectorToJSONArray(selector);
|
|
18
|
+
* tellraw("@a", selectorToJSONArray(selector));
|
|
19
|
+
*
|
|
20
|
+
* // Everyone will see this in the chat:
|
|
21
|
+
* // ["Player1","Player2","Player3"]
|
|
22
|
+
*/
|
|
23
|
+
function selectorToJSONArray(selector: SelectorClass<any, any>): JSONTextComponent[] {
|
|
24
|
+
return ['["', { selector: selector.toString(), separator: '","' } as any, '"]'];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Converts a Sandstone Selector to a JSON string that will be displayed like a JSON string in the chat.
|
|
29
|
+
* If the selector matches several entities, it will display a comma-separated list of their names.
|
|
30
|
+
*
|
|
31
|
+
* @param selector The selector to convert.
|
|
32
|
+
* @returns A JSON string that will be displayed like a JSON string in the chat.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const selector = Selector("@s");
|
|
36
|
+
* const jsonString = selectorToJSONString(selector);
|
|
37
|
+
* tellraw("@a", jsonString);
|
|
38
|
+
*
|
|
39
|
+
* // Everyone will see this in the chat:
|
|
40
|
+
* // "Player1"
|
|
41
|
+
*/
|
|
42
|
+
function selectorToJSONString(selector: SelectorClass<any, any>): JSONTextComponent[] {
|
|
43
|
+
return ['"', { selector: selector.toString(), separator: "," } as any, '"'];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Commands for interacting with Kradle's Watcher.
|
|
48
|
+
*/
|
|
49
|
+
export const Commands = {
|
|
50
|
+
/**
|
|
51
|
+
* Raw command - sends a command to Kradle's Watcher.
|
|
52
|
+
*
|
|
53
|
+
* Arguments are passed as a JSON object.
|
|
54
|
+
* @param command The command to send.
|
|
55
|
+
* @param args The JSON Text Component arguments to send with the command.
|
|
56
|
+
*
|
|
57
|
+
* @warning
|
|
58
|
+
* The arguments must resolve to a JSON-serializable object. This cannot be verified at compile time.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* Commands.raw("start_challenge", { name: "My Challenge" });
|
|
62
|
+
* // The watcher will receive the following payload::
|
|
63
|
+
* // {"type":"kradle_command","command":"start_challenge","arguments":{"name":"My Challenge"}}
|
|
64
|
+
*
|
|
65
|
+
* Commands.raw("fetch", {url: "https://goldprice.org", "target_score": "gold_price", callback: "kradle:on_gold_price_fetched"})
|
|
66
|
+
* // The watcher will receive the following payload:
|
|
67
|
+
* // {"type":"kradle_command","command":"fetch","arguments":{"url":"https://goldprice.org","target_score":"gold_price","callback":"kradle:on_gold_price_fetched"}}
|
|
68
|
+
*/
|
|
69
|
+
raw: (command: string, args: Record<string, JSONTextComponent>) => {
|
|
70
|
+
const basePayload: string = JSON.stringify({
|
|
71
|
+
command,
|
|
72
|
+
type: "kradle_command",
|
|
73
|
+
arguments: {},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// We need to insert our arguments into the "arguments" field of the base payload.
|
|
77
|
+
const splitIndex = basePayload.indexOf("}");
|
|
78
|
+
const leftText = basePayload.slice(0, splitIndex);
|
|
79
|
+
const rightText = basePayload.slice(splitIndex);
|
|
80
|
+
|
|
81
|
+
function parseValue(value: JSONTextComponent): JSONTextComponent {
|
|
82
|
+
if (typeof value === "boolean") {
|
|
83
|
+
return value.toString();
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === "string") {
|
|
86
|
+
return JSON.stringify(value);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const argumentsPayload = Object.entries(args).flatMap(([key, value], index) => {
|
|
92
|
+
const result = [`"${key}":`, parseValue(value)];
|
|
93
|
+
if (index > 0) {
|
|
94
|
+
return [",", ...result];
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
});
|
|
98
|
+
tellraw(WATCHER, [leftText, argumentsPayload, rightText]);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Declare winners of the challenge to the Watcher. This will end the challenge.
|
|
103
|
+
*
|
|
104
|
+
* The `winners` argument of the payload will be a JSON list of player names.
|
|
105
|
+
* Ex: { "winners": ["player1", "player2"] }
|
|
106
|
+
*
|
|
107
|
+
* If no winners are selected, an empty array will be sent.
|
|
108
|
+
* Ex: { "winners": [] }
|
|
109
|
+
*/
|
|
110
|
+
declareWinners: () => {
|
|
111
|
+
const selector = Selector("@a", { tag: WINNER_TAG });
|
|
112
|
+
|
|
113
|
+
// If no winners are selected, we send them as a JSON array.
|
|
114
|
+
_.if(selector, () => {
|
|
115
|
+
Commands.raw("declare_winners", {
|
|
116
|
+
winners: selectorToJSONArray(selector),
|
|
117
|
+
});
|
|
118
|
+
}).else(() => {
|
|
119
|
+
// Else, we send an empty array.
|
|
120
|
+
Commands.raw("declare_winners", {
|
|
121
|
+
winners: "[]",
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
logVariable: (message: string, variable: Score, store: boolean) => {
|
|
127
|
+
const objectiveName = variable.objective.name;
|
|
128
|
+
const variableName = variable.target.toString();
|
|
129
|
+
|
|
130
|
+
return Commands.raw("log_variable", {
|
|
131
|
+
message,
|
|
132
|
+
store,
|
|
133
|
+
variable: JSON.stringify({
|
|
134
|
+
objective: objectiveName,
|
|
135
|
+
selector: variableName,
|
|
136
|
+
}),
|
|
137
|
+
value: variable,
|
|
138
|
+
targets: selectorToJSONString(Selector(variableName as any)),
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
};
|