@kradle/challenges 0.1.0 → 0.2.0
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/AGENTS.md +273 -0
- package/CLAUDE.md +1 -0
- package/LLM_README.md +1380 -0
- package/README.md +498 -1
- package/dist/actions.d.ts +11 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +12 -0
- package/dist/actions.js.map +1 -1
- package/dist/challenge.js +1 -1
- package/dist/challenge.js.map +1 -1
- package/package.json +1 -1
- package/src/actions.ts +15 -1
- package/src/challenge.ts +1 -1
package/LLM_README.md
ADDED
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
# LLM_README.md - @kradle/challenges API Reference
|
|
2
|
+
|
|
3
|
+
This document provides exhaustive API documentation for AI agents using the `@kradle/challenges` package. This is the complete reference for creating Minecraft datapack-based challenges.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Overview](#overview)
|
|
8
|
+
2. [Installation](#installation)
|
|
9
|
+
3. [Core Concepts](#core-concepts)
|
|
10
|
+
4. [createChallenge API](#createchallenge-api)
|
|
11
|
+
5. [Variables](#variables)
|
|
12
|
+
6. [Events](#events)
|
|
13
|
+
7. [Actions](#actions)
|
|
14
|
+
8. [Utilities](#utilities)
|
|
15
|
+
9. [Sandstone Integration](#sandstone-integration)
|
|
16
|
+
10. [Complete Examples](#complete-examples)
|
|
17
|
+
11. [Common Patterns](#common-patterns)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
22
|
+
|
|
23
|
+
`@kradle/challenges` is a TypeScript framework for creating Minecraft challenges that compile to datapacks. It provides:
|
|
24
|
+
|
|
25
|
+
- **Variable System**: Track per-player and global game state with automatic tick updates
|
|
26
|
+
- **Event System**: Lifecycle hooks and custom event triggers based on scores/advancements
|
|
27
|
+
- **Actions Library**: Pre-built game operations (give items, teleport, announce, etc.)
|
|
28
|
+
- **Role Management**: Assign players to teams with role-specific win conditions
|
|
29
|
+
- **Sandstone Integration**: Full access to Sandstone's Minecraft command generation
|
|
30
|
+
|
|
31
|
+
**Key Principle**: Challenges are defined declaratively. You specify variables, events, and conditions - the framework generates the datapack.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @kradle/challenges sandstone@0.14.0-alpha.13
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Requirements:**
|
|
42
|
+
- Node.js >= 22.18.0
|
|
43
|
+
- Sandstone 0.14.0-alpha.13 (peer dependency)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Core Concepts
|
|
48
|
+
|
|
49
|
+
### Ticks
|
|
50
|
+
|
|
51
|
+
Minecraft runs at 20 ticks per second. Time values in this API are in ticks:
|
|
52
|
+
- 1 second = 20 ticks
|
|
53
|
+
- 1 minute = 1200 ticks (60 * 20)
|
|
54
|
+
- 5 minutes = 6000 ticks
|
|
55
|
+
|
|
56
|
+
### Namespace
|
|
57
|
+
|
|
58
|
+
Items and entities are namespaced with `minecraft:` - for example, you should use `minecraft:diamond` instead of `diamond`. Same for entities.
|
|
59
|
+
|
|
60
|
+
### Scores
|
|
61
|
+
|
|
62
|
+
All variables are backed by Minecraft scoreboards. The `Score` type from Sandstone represents a scoreboard value. Scores support comparison methods:
|
|
63
|
+
- `score.equalTo(value)` / `score.equalTo(otherScore)`
|
|
64
|
+
- `score.greaterThan(value)`
|
|
65
|
+
- `score.greaterOrEqualThan(value)`
|
|
66
|
+
- `score.lowerThan(value)`
|
|
67
|
+
- `score.lowerOrEqualThan(value)`
|
|
68
|
+
|
|
69
|
+
### Roles
|
|
70
|
+
|
|
71
|
+
Roles define player groups (e.g., teams). Each role can have different win conditions. All players assigned to a role share that role's win condition.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## createChallenge API
|
|
76
|
+
|
|
77
|
+
### Signature
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
function createChallenge<
|
|
81
|
+
ROLES extends readonly string[],
|
|
82
|
+
VARIABLES extends Record<string, _InputVariableType>
|
|
83
|
+
>(config: _BaseConfig<ROLES, VARIABLES>): ChallengeBuilder
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Configuration Object
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
interface _BaseConfig<ROLES, VARIABLES> {
|
|
90
|
+
// Required: Challenge name (used for datapack namespace)
|
|
91
|
+
name: string;
|
|
92
|
+
|
|
93
|
+
// Required: Output path for generated datapack
|
|
94
|
+
kradle_challenge_path: string;
|
|
95
|
+
|
|
96
|
+
// Required: Player roles as readonly tuple
|
|
97
|
+
// Example: ["attacker", "defender"] as const
|
|
98
|
+
roles: ROLES;
|
|
99
|
+
|
|
100
|
+
// Required: Custom variable definitions
|
|
101
|
+
custom_variables: VARIABLES;
|
|
102
|
+
|
|
103
|
+
// Optional: Game duration in ticks (default: 6000 = 5 minutes)
|
|
104
|
+
GAME_DURATION?: number;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Builder Methods
|
|
109
|
+
|
|
110
|
+
The builder uses a fluent API. Methods must be called in order:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
createChallenge(config)
|
|
114
|
+
.events(eventCallback) // Define lifecycle events
|
|
115
|
+
.custom_events(customEventCallback) // Define triggered events
|
|
116
|
+
.end_condition(endConditionCallback) // Define game end condition
|
|
117
|
+
.win_conditions(winConditionsCallback) // Define win conditions (triggers build)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### `.events(callback)`
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
.events((variables: Variables, roles: Roles) => {
|
|
124
|
+
return {
|
|
125
|
+
start_challenge?: () => void; // Runs once when game starts
|
|
126
|
+
init_participants?: () => void; // Runs once per player after start
|
|
127
|
+
on_tick?: () => void; // Runs every tick for each player
|
|
128
|
+
end_challenge?: () => void; // Runs once when game ends
|
|
129
|
+
};
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `.custom_events(callback)`
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
.custom_events((variables: Variables, roles: Roles) => {
|
|
137
|
+
return Array<ScoreEvent | AdvancementEvent>;
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Score Event:**
|
|
142
|
+
```typescript
|
|
143
|
+
{
|
|
144
|
+
score: Score; // Variable to watch
|
|
145
|
+
target: number; // Threshold value
|
|
146
|
+
mode: "fire_once" | "repeatable";
|
|
147
|
+
actions: () => void; // Actions to execute
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Advancement Event:**
|
|
152
|
+
```typescript
|
|
153
|
+
{
|
|
154
|
+
criteria: Array<{ trigger: string; conditions?: object }>;
|
|
155
|
+
mode: "fire_once" | "repeatable";
|
|
156
|
+
actions: () => void;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### `.end_condition(callback)`
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
.end_condition((variables: Variables, roles: Roles) => {
|
|
164
|
+
return Condition; // Returns a Sandstone condition
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### `.win_conditions(callback)`
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
.win_conditions((variables: Variables, roles: Roles) => {
|
|
172
|
+
return {
|
|
173
|
+
[roleName: string]: Condition; // One condition per role
|
|
174
|
+
};
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Variables
|
|
181
|
+
|
|
182
|
+
### Built-in Variables
|
|
183
|
+
|
|
184
|
+
These are always available in every challenge:
|
|
185
|
+
|
|
186
|
+
| Variable | Type | Description |
|
|
187
|
+
|----------|------|-------------|
|
|
188
|
+
| `death_count` | individual | Player's death count (Minecraft `deathCount` objective) |
|
|
189
|
+
| `has_never_died` | individual | 1 if player has never died, 0 otherwise |
|
|
190
|
+
| `alive_players` | global | Count of living participants |
|
|
191
|
+
| `main_score` | individual | Primary score shown on sidebar |
|
|
192
|
+
| `game_timer` | global | Ticks since game start (increments each tick) |
|
|
193
|
+
| `game_state` | global | 0=CREATED, 1=OFF, 2=ON |
|
|
194
|
+
| `player_count` | global | Total participant count |
|
|
195
|
+
| `player_number` | individual | Unique player ID (1 to N) |
|
|
196
|
+
|
|
197
|
+
### Custom Variable Definition
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
custom_variables: {
|
|
201
|
+
variable_name: {
|
|
202
|
+
type: "individual" | "global";
|
|
203
|
+
objective_type?: string; // Minecraft objective criterion
|
|
204
|
+
hidden?: boolean; // Hide from scoreboard (global only)
|
|
205
|
+
default?: number; // Initial value
|
|
206
|
+
updater?: UpdaterFunction;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Variable Types
|
|
212
|
+
|
|
213
|
+
#### Individual Objective-Based
|
|
214
|
+
|
|
215
|
+
Automatically tracks Minecraft statistics:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
pigs_killed: {
|
|
219
|
+
type: "individual",
|
|
220
|
+
objective_type: "minecraft.killed:minecraft.pig",
|
|
221
|
+
default: 0,
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Common objective types:
|
|
226
|
+
- `"minecraft.killed:minecraft.<entity>"` - Entities killed
|
|
227
|
+
- `"minecraft.killed_by:minecraft.<entity>"` - Killed by entity
|
|
228
|
+
- `"minecraft.picked_up:minecraft.<item>"` - Items picked up
|
|
229
|
+
- `"minecraft.mined:minecraft.<block>"` - Blocks mined
|
|
230
|
+
- `"minecraft.used:minecraft.<item>"` - Items used
|
|
231
|
+
- `"minecraft.crafted:minecraft.<item>"` - Items crafted
|
|
232
|
+
- `"playerKillCount"` - PvP kills
|
|
233
|
+
- `"deathCount"` - Deaths
|
|
234
|
+
- `"dummy"` - Manual/computed value
|
|
235
|
+
|
|
236
|
+
You can find objectives definition on the [Scoreboard](https://minecraft.fandom.com/wiki/Scoreboard) wiki pagem as well as the [Statistics](https://minecraft.fandom.com/wiki/Statistics) page for more in-depth explanation.
|
|
237
|
+
|
|
238
|
+
#### Individual Dummy
|
|
239
|
+
|
|
240
|
+
Computed per-player values:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
current_y_position: {
|
|
244
|
+
type: "individual",
|
|
245
|
+
objective_type: "dummy",
|
|
246
|
+
updater: (value) => {
|
|
247
|
+
execute.as("@s").store.result.score(value).run.data.get.entity("@s", "Pos[1]");
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Global Variables
|
|
253
|
+
|
|
254
|
+
Shared across all players:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
max_score_global: {
|
|
258
|
+
type: "global",
|
|
259
|
+
hidden: false,
|
|
260
|
+
default: 0,
|
|
261
|
+
updater: (value, { main_score }) => {
|
|
262
|
+
value.set(0);
|
|
263
|
+
forEveryPlayer(() => {
|
|
264
|
+
_.if(main_score.greaterThan(value), () => {
|
|
265
|
+
value.set(main_score);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Updater Function Signature
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
type UpdaterFunction = (
|
|
276
|
+
value: Score, // Current variable's score
|
|
277
|
+
variables: Record<string, Score> // All variables including built-ins
|
|
278
|
+
) => void;
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Updaters run every tick. They should be idempotent. They are not necessary if the variable is individual and tracks a Minecraft objective.
|
|
282
|
+
|
|
283
|
+
### Score Methods
|
|
284
|
+
|
|
285
|
+
All variables are `Score` objects with these methods:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Set value
|
|
289
|
+
score.set(number | Score);
|
|
290
|
+
|
|
291
|
+
// Arithmetic
|
|
292
|
+
score.add(number | Score);
|
|
293
|
+
score.remove(number | Score);
|
|
294
|
+
|
|
295
|
+
// Comparison (return Condition)
|
|
296
|
+
score.equalTo(number | Score);
|
|
297
|
+
score.greaterThan(number | Score);
|
|
298
|
+
score.greaterOrEqualThan(number | Score);
|
|
299
|
+
score.lowerThan(number | Score);
|
|
300
|
+
score.lowerOrEqualThan(number | Score);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Events
|
|
306
|
+
|
|
307
|
+
### Lifecycle Events
|
|
308
|
+
|
|
309
|
+
#### `start_challenge`
|
|
310
|
+
|
|
311
|
+
Runs once when the challenge starts. Use for global setup.
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
start_challenge: () => {
|
|
315
|
+
Actions.setTime({ time: "day" });
|
|
316
|
+
Actions.gamerule({ rule: "doDaylightCycle", value: false });
|
|
317
|
+
Actions.announce({ message: "Game starting!" });
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### `init_participants`
|
|
322
|
+
|
|
323
|
+
Runs once per player after start (delayed 1 second). Use for player setup.
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
init_participants: () => {
|
|
327
|
+
Actions.give({ item: "minecraft:diamond_sword", count: 1 });
|
|
328
|
+
Actions.setAttribute({ attribute: "generic.max_health", value: 40 });
|
|
329
|
+
Actions.teleport({ target: "@s", pos: [0, 100, 0] });
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### `on_tick`
|
|
334
|
+
|
|
335
|
+
Runs every tick for each player. Use sparingly for performance.
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
on_tick: () => {
|
|
339
|
+
// Check something every tick
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
#### `end_challenge`
|
|
344
|
+
|
|
345
|
+
Runs once when the game ends. Use for cleanup and announcements.
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
end_challenge: () => {
|
|
349
|
+
Actions.announce({ message: "Game over!" });
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Custom Events
|
|
354
|
+
|
|
355
|
+
Custom events allow you to trigger actions based on score thresholds or Minecraft advancement criteria.
|
|
356
|
+
|
|
357
|
+
#### Score-Based Events
|
|
358
|
+
|
|
359
|
+
Score events watch a variable and trigger when it reaches a specified target value. They are evaluated every tick.
|
|
360
|
+
|
|
361
|
+
**Parameters:**
|
|
362
|
+
- `score` (required): The `Score` variable to watch
|
|
363
|
+
- `target` (optional): The threshold value. If omitted, triggers on any score change
|
|
364
|
+
- `mode` (required): `"fire_once"` or `"repeatable"`
|
|
365
|
+
- `actions` (required): Function containing actions to execute
|
|
366
|
+
|
|
367
|
+
**How it works:**
|
|
368
|
+
1. Every tick, the system compares the current score to the target
|
|
369
|
+
2. For `"fire_once"`: triggers once when `score == target` (tracks previous value to detect the 1st time threshold is met)
|
|
370
|
+
3. For `"repeatable"`: triggers every tick while `score == target`
|
|
371
|
+
4. For individual variables, events fire per-player; for global variables, events fire once globally
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
{
|
|
375
|
+
score: variables.diamonds,
|
|
376
|
+
target: 10,
|
|
377
|
+
mode: "fire_once",
|
|
378
|
+
actions: () => {
|
|
379
|
+
Actions.announce({ message: "10 diamonds collected!" });
|
|
380
|
+
},
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Without target (triggers on any change):**
|
|
385
|
+
```typescript
|
|
386
|
+
{
|
|
387
|
+
score: variables.death_count,
|
|
388
|
+
mode: "fire_once", // Triggers once when death_count changes from 0
|
|
389
|
+
actions: () => {
|
|
390
|
+
Actions.announce({ message: "First death!" });
|
|
391
|
+
},
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Modes:**
|
|
396
|
+
- `"fire_once"`: Triggers once when threshold is reached (per player for individual variables). Uses previous tick comparison to detect when the score crosses the target.
|
|
397
|
+
- `"repeatable"`: Triggers every tick while `score >= target`. Useful for continuous effects.
|
|
398
|
+
|
|
399
|
+
#### Advancement-Based Events
|
|
400
|
+
|
|
401
|
+
Advancement events trigger when a Minecraft advancement criterion is met. Internally, an advancement is created that grants when the criterion triggers, which then fires the event. The `criteria` array follows the [Minecraft Advancement JSON format](https://minecraft.fandom.com/wiki/Advancement/JSON_format).
|
|
402
|
+
|
|
403
|
+
**Parameters:**
|
|
404
|
+
- `criteria` (required): Array of advancement trigger objects with optional conditions
|
|
405
|
+
- `mode` (required): `"fire_once"` or `"repeatable"`
|
|
406
|
+
- `actions` (required): Function containing actions to execute
|
|
407
|
+
|
|
408
|
+
**How it works:**
|
|
409
|
+
1. An advancement is generated with your specified criteria
|
|
410
|
+
2. When Minecraft grants the advancement (criterion met), the event fires
|
|
411
|
+
3. For `"repeatable"`: the advancement is automatically revoked so it can trigger again
|
|
412
|
+
4. For `"fire_once"`: the advancement stays granted, preventing re-triggering
|
|
413
|
+
|
|
414
|
+
**Simple example - Track when a player hits another player:**
|
|
415
|
+
```typescript
|
|
416
|
+
{
|
|
417
|
+
criteria: [
|
|
418
|
+
{
|
|
419
|
+
trigger: "minecraft:player_hurt_entity",
|
|
420
|
+
conditions: {
|
|
421
|
+
entity: { type: "minecraft:player" }
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
],
|
|
425
|
+
mode: "repeatable",
|
|
426
|
+
actions: () => {
|
|
427
|
+
Actions.increment({ score: variables.pvp_hits });
|
|
428
|
+
Actions.announce({ message: "PvP hit!" });
|
|
429
|
+
},
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Multiple triggers example:**
|
|
434
|
+
```typescript
|
|
435
|
+
{
|
|
436
|
+
criteria: [
|
|
437
|
+
{ trigger: "minecraft:player_hurt_entity" },
|
|
438
|
+
{ trigger: "minecraft:entity_hurt_player" },
|
|
439
|
+
],
|
|
440
|
+
mode: "repeatable",
|
|
441
|
+
actions: () => {
|
|
442
|
+
Actions.increment({ score: variables.combat_actions });
|
|
443
|
+
},
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Common triggers:
|
|
448
|
+
- `"minecraft:player_hurt_entity"` - Player attacks entity
|
|
449
|
+
- `"minecraft:entity_hurt_player"` - Entity attacks player
|
|
450
|
+
- `"minecraft:player_killed_entity"` - Player kills entity
|
|
451
|
+
- `"minecraft:consume_item"` - Item consumed
|
|
452
|
+
- `"minecraft:inventory_changed"` - Inventory changes
|
|
453
|
+
- `"minecraft:location"` - Player at location
|
|
454
|
+
- `"minecraft:enter_block"` - Player enters block
|
|
455
|
+
|
|
456
|
+
See the [Minecraft Wiki](https://minecraft.fandom.com/wiki/Advancement/JSON_format#List_of_triggers) for the full list of triggers and their conditions.
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Actions
|
|
461
|
+
|
|
462
|
+
All actions are called via the `Actions` object:
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
import { Actions } from "@kradle/challenges";
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Communication
|
|
469
|
+
|
|
470
|
+
#### `Actions.announce(params)`
|
|
471
|
+
|
|
472
|
+
Broadcast message to all players with KRADLE tag.
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
Actions.announce({
|
|
476
|
+
message: string; // Message text
|
|
477
|
+
});
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
#### `Actions.tellraw(params)`
|
|
481
|
+
|
|
482
|
+
Send formatted message to specific target.
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
Actions.tellraw({
|
|
486
|
+
target: string; // Selector (e.g., "@a", "@s", "@p")
|
|
487
|
+
message: JSONTextComponent; // Minecraft JSON text
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Example:
|
|
491
|
+
Actions.tellraw({
|
|
492
|
+
target: "@a",
|
|
493
|
+
message: { text: "Hello!", color: "gold", bold: true }
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Items & Inventory
|
|
498
|
+
|
|
499
|
+
#### `Actions.give(params)`
|
|
500
|
+
|
|
501
|
+
Give items to current player.
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
Actions.give({
|
|
505
|
+
item: string; // Item ID without "minecraft:" prefix
|
|
506
|
+
count: number; // Amount
|
|
507
|
+
nbt?: NBTObject; // Optional NBT data
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Examples:
|
|
511
|
+
Actions.give({ item: "minecraft:diamond_sword", count: 1 });
|
|
512
|
+
Actions.give({
|
|
513
|
+
item: "minecraft:diamond_sword",
|
|
514
|
+
count: 1,
|
|
515
|
+
nbt: {
|
|
516
|
+
Enchantments: [{ id: "minecraft:sharpness", lvl: 5 }]
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
#### `Actions.giveLoot(params)`
|
|
522
|
+
|
|
523
|
+
Give random items from loot table.
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
Actions.giveLoot({
|
|
527
|
+
loot_table: string; // Loot table path
|
|
528
|
+
count: number; // Number of rolls
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Example:
|
|
532
|
+
Actions.giveLoot({
|
|
533
|
+
loot_table: "minecraft:chests/simple_dungeon",
|
|
534
|
+
count: 3
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
#### `Actions.clear(params)`
|
|
539
|
+
|
|
540
|
+
Clear items from player inventory.
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
Actions.clear({
|
|
544
|
+
item?: string; // Item to clear (omit for all items)
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Examples:
|
|
548
|
+
Actions.clear({ item: "diamond" });
|
|
549
|
+
Actions.clear({}); // Clear everything
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
#### `Actions.countItems(params)`
|
|
553
|
+
|
|
554
|
+
Count the number of a specific item in a target's inventory. Creates and returns a temporary variable with the count.
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
Actions.countItems({
|
|
558
|
+
target: TargetNames; // The target to count items for
|
|
559
|
+
item: ITEMS; // The item to count
|
|
560
|
+
}): Score; // Returns a new variable containing the count
|
|
561
|
+
|
|
562
|
+
// Example - Use directly in conditions:
|
|
563
|
+
_.if(Actions.countItems({ target: "self", item: "minecraft:diamond" }).greaterThan(5), () => {
|
|
564
|
+
Actions.announce({ message: "You have more than 5 diamonds!" });
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Example - Store in a variable for later use:
|
|
568
|
+
const diamondCount = Actions.countItems({ target: "self", item: "minecraft:diamond" });
|
|
569
|
+
_.if(diamondCount.greaterThan(10), () => {
|
|
570
|
+
Actions.announce({ message: "You have more than 10 diamonds!" });
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Example - Set a custom variable from the count:
|
|
574
|
+
const count = Actions.countItems({ target: "self", item: "minecraft:diamond" });
|
|
575
|
+
variables.my_diamond_count.set(count);
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
**Note:** This action creates a temporary variable internally using `Variable()` and uses `execute.store.result.score` with `clear` command (count 0) to count items without removing them from the inventory.
|
|
579
|
+
|
|
580
|
+
### Entities
|
|
581
|
+
|
|
582
|
+
#### `Actions.summonMultiple(params)`
|
|
583
|
+
|
|
584
|
+
Summon multiple entities at location.
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
Actions.summonMultiple({
|
|
588
|
+
entity: string; // Entity ID
|
|
589
|
+
count: number; // How many
|
|
590
|
+
pos: [number, number, number]; // Coordinates
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// Example:
|
|
594
|
+
Actions.summonMultiple({
|
|
595
|
+
entity: "zombie",
|
|
596
|
+
count: 5,
|
|
597
|
+
pos: [0, 64, 0]
|
|
598
|
+
});
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
#### `Actions.summonItem(params)`
|
|
602
|
+
|
|
603
|
+
Summon item entity at location.
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
Actions.summonItem({
|
|
607
|
+
item: string;
|
|
608
|
+
pos: [number, number, number];
|
|
609
|
+
count?: number;
|
|
610
|
+
nbt?: NBTObject;
|
|
611
|
+
});
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
#### `Actions.kill(params)`
|
|
615
|
+
|
|
616
|
+
Kill entities matching selector.
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
Actions.kill({
|
|
620
|
+
target: string; // Entity selector
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Examples:
|
|
624
|
+
Actions.kill({ target: "@e[type=zombie]" });
|
|
625
|
+
Actions.kill({ target: "@e[type=!player]" });
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
#### `Actions.teleport(params)`
|
|
629
|
+
|
|
630
|
+
Teleport entity to location.
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
Actions.teleport({
|
|
634
|
+
target: string; // Entity selector
|
|
635
|
+
pos: [number, number, number]; // Destination
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Example:
|
|
639
|
+
Actions.teleport({
|
|
640
|
+
target: "@s",
|
|
641
|
+
pos: [0, 100, 0]
|
|
642
|
+
});
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### World
|
|
646
|
+
|
|
647
|
+
#### `Actions.setBlock(params)`
|
|
648
|
+
|
|
649
|
+
Set a single block.
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
Actions.setBlock({
|
|
653
|
+
pos: [number, number, number];
|
|
654
|
+
block: string; // Block ID
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Example:
|
|
658
|
+
Actions.setBlock({
|
|
659
|
+
pos: [0, 64, 0],
|
|
660
|
+
block: "diamond_block"
|
|
661
|
+
});
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
#### `Actions.fill(params)`
|
|
665
|
+
|
|
666
|
+
Fill region with blocks.
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
Actions.fill({
|
|
670
|
+
start: [number, number, number];
|
|
671
|
+
end: [number, number, number];
|
|
672
|
+
block: string;
|
|
673
|
+
mode: "fill" | "line" | "pyramid";
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// Example:
|
|
677
|
+
Actions.fill({
|
|
678
|
+
start: [0, 64, 0],
|
|
679
|
+
end: [10, 64, 10],
|
|
680
|
+
block: "stone",
|
|
681
|
+
mode: "fill"
|
|
682
|
+
});
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
#### `Actions.setTime(params)`
|
|
686
|
+
|
|
687
|
+
Set world time.
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
Actions.setTime({
|
|
691
|
+
time: "day" | "night" | number; // Named or tick value
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// Examples:
|
|
695
|
+
Actions.setTime({ time: "day" });
|
|
696
|
+
Actions.setTime({ time: 6000 }); // Noon
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
#### `Actions.gamerule(params)`
|
|
700
|
+
|
|
701
|
+
Set a gamerule.
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
Actions.gamerule({
|
|
705
|
+
rule: string;
|
|
706
|
+
value: boolean | number;
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
// Examples:
|
|
710
|
+
Actions.gamerule({ rule: "doDaylightCycle", value: false });
|
|
711
|
+
Actions.gamerule({ rule: "mobGriefing", value: false });
|
|
712
|
+
Actions.gamerule({ rule: "randomTickSpeed", value: 0 });
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### Scores
|
|
716
|
+
|
|
717
|
+
#### `Actions.set(params)`
|
|
718
|
+
|
|
719
|
+
Set score to value or copy from another score.
|
|
720
|
+
|
|
721
|
+
```typescript
|
|
722
|
+
// Set to number
|
|
723
|
+
Actions.set({
|
|
724
|
+
score: Score;
|
|
725
|
+
value: number;
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// Copy from another score
|
|
729
|
+
Actions.set({
|
|
730
|
+
score: Score;
|
|
731
|
+
source: Score;
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// Examples:
|
|
735
|
+
Actions.set({ score: variables.main_score, value: 0 });
|
|
736
|
+
Actions.set({ score: variables.main_score, source: variables.diamonds });
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
#### `Actions.increment(params)`
|
|
740
|
+
|
|
741
|
+
Add 1 to score.
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
Actions.increment({
|
|
745
|
+
score: Score;
|
|
746
|
+
});
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
#### `Actions.decrement(params)`
|
|
750
|
+
|
|
751
|
+
Subtract 1 from score.
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
Actions.decrement({
|
|
755
|
+
score: Score;
|
|
756
|
+
});
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Player Attributes
|
|
760
|
+
|
|
761
|
+
#### `Actions.setAttribute(params)`
|
|
762
|
+
|
|
763
|
+
Set entity attribute.
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
Actions.setAttribute({
|
|
767
|
+
attribute: string; // Attribute name
|
|
768
|
+
value: number;
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// Examples:
|
|
772
|
+
Actions.setAttribute({ attribute: "max_health", value: 40 });
|
|
773
|
+
Actions.setAttribute({ attribute: "movement_speed", value: 0.2 });
|
|
774
|
+
Actions.setAttribute({ attribute: "attack_damage", value: 10 });
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
Common attributes:
|
|
778
|
+
- `"max_health"` - Maximum HP (default 20)
|
|
779
|
+
- `"movement_speed"` - Walk speed (default 0.1)
|
|
780
|
+
- `"attack_damage"` - Base attack damage
|
|
781
|
+
- `"armor"` - Armor points
|
|
782
|
+
- `"knockback_resistance"` - Knockback resistance (0-1)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
### Logging
|
|
786
|
+
|
|
787
|
+
#### `Actions.log_variable(params)`
|
|
788
|
+
|
|
789
|
+
Log variable to watcher system (debugging).
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
Actions.log_variable({
|
|
793
|
+
message: string;
|
|
794
|
+
variable: Score;
|
|
795
|
+
store: string;
|
|
796
|
+
});
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
---
|
|
800
|
+
|
|
801
|
+
## Utilities
|
|
802
|
+
|
|
803
|
+
### `forEveryPlayer(callback)`
|
|
804
|
+
|
|
805
|
+
Execute code for each participant at their location.
|
|
806
|
+
|
|
807
|
+
```typescript
|
|
808
|
+
import { forEveryPlayer } from "@kradle/challenges";
|
|
809
|
+
|
|
810
|
+
forEveryPlayer(() => {
|
|
811
|
+
// Runs as(@s) at(@s) for each participant
|
|
812
|
+
// @s is the current player
|
|
813
|
+
});
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**Example - Find maximum score:**
|
|
817
|
+
```typescript
|
|
818
|
+
max_score: {
|
|
819
|
+
type: "global",
|
|
820
|
+
updater: (value, { main_score }) => {
|
|
821
|
+
value.set(0);
|
|
822
|
+
forEveryPlayer(() => {
|
|
823
|
+
_.if(main_score.greaterThan(value), () => {
|
|
824
|
+
value.set(main_score);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
},
|
|
828
|
+
}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Constants
|
|
832
|
+
|
|
833
|
+
```typescript
|
|
834
|
+
import { ALL, KRADLE_PARTICIPANT_TAG, WINNER_TAG } from "@kradle/challenges";
|
|
835
|
+
|
|
836
|
+
// ALL - Selector for all participants: @a[tag=kradle_participant]
|
|
837
|
+
// KRADLE_PARTICIPANT_TAG - Tag name: "kradle_participant"
|
|
838
|
+
// WINNER_TAG - Tag name: "kradle_winner"
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
## Sandstone Integration
|
|
844
|
+
|
|
845
|
+
This package is built on Sandstone. You can use Sandstone APIs directly:
|
|
846
|
+
|
|
847
|
+
```typescript
|
|
848
|
+
import { _, execute, Selector, rel, abs, MCFunction } from "sandstone";
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
### Conditions with `_`
|
|
852
|
+
|
|
853
|
+
```typescript
|
|
854
|
+
// Single condition
|
|
855
|
+
_.if(score.greaterThan(10), () => {
|
|
856
|
+
// actions
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
// Combined conditions
|
|
860
|
+
_.if(_.and(
|
|
861
|
+
score1.greaterThan(5),
|
|
862
|
+
score2.equalTo(1)
|
|
863
|
+
), () => {
|
|
864
|
+
// actions
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
_.if(_.or(
|
|
868
|
+
condition1,
|
|
869
|
+
condition2
|
|
870
|
+
), () => {
|
|
871
|
+
// actions
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// Block check
|
|
875
|
+
_.if(_.block(rel(0, -1, 0), "minecraft:diamond_block"), () => {
|
|
876
|
+
// Player standing on diamond block
|
|
877
|
+
});
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### Execute Commands
|
|
881
|
+
|
|
882
|
+
```typescript
|
|
883
|
+
// Store result in score
|
|
884
|
+
execute.as("@s").store.result.score(myScore).run.data.get.entity("@s", "Pos[1]");
|
|
885
|
+
|
|
886
|
+
// Run at location
|
|
887
|
+
execute.at("@s").run.particle("minecraft:flame", rel(0, 1, 0));
|
|
888
|
+
|
|
889
|
+
// Conditional execution
|
|
890
|
+
execute.if.score(myScore, ">=", 10).run.say("High score!");
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
### Selectors
|
|
894
|
+
|
|
895
|
+
```typescript
|
|
896
|
+
import { Selector } from "sandstone";
|
|
897
|
+
|
|
898
|
+
// With arguments
|
|
899
|
+
Selector("@a", { tag: "my_tag" });
|
|
900
|
+
Selector("@e", { type: "zombie", limit: 1, sort: "nearest" });
|
|
901
|
+
|
|
902
|
+
// NBT check
|
|
903
|
+
Selector("@s", {
|
|
904
|
+
nbt: { Inventory: [{ id: "minecraft:diamond" }] }
|
|
905
|
+
});
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
### Relative/Absolute Coordinates
|
|
909
|
+
|
|
910
|
+
```typescript
|
|
911
|
+
import { rel, abs } from "sandstone";
|
|
912
|
+
|
|
913
|
+
rel(0, 1, 0); // ~ ~1 ~
|
|
914
|
+
abs(0, 64, 0); // 0 64 0
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
---
|
|
918
|
+
|
|
919
|
+
## Complete Examples
|
|
920
|
+
|
|
921
|
+
### Example 1: Speed Challenge - First to Kill 2 Pigs
|
|
922
|
+
|
|
923
|
+
```typescript
|
|
924
|
+
import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges";
|
|
925
|
+
import { _ } from "sandstone";
|
|
926
|
+
|
|
927
|
+
createChallenge({
|
|
928
|
+
name: "pig-farming",
|
|
929
|
+
kradle_challenge_path: "./output",
|
|
930
|
+
roles: ["farmer"] as const,
|
|
931
|
+
GAME_DURATION: 2 * 60 * 20, // 2 minutes
|
|
932
|
+
custom_variables: {
|
|
933
|
+
pigs_farmed: {
|
|
934
|
+
type: "individual",
|
|
935
|
+
objective_type: "minecraft.killed:minecraft.pig",
|
|
936
|
+
default: 0,
|
|
937
|
+
updater: (value, { main_score }) => {
|
|
938
|
+
main_score.set(value);
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
game_over: {
|
|
942
|
+
type: "global",
|
|
943
|
+
updater: (value, { pigs_farmed }) => {
|
|
944
|
+
value.set(0);
|
|
945
|
+
forEveryPlayer(() => {
|
|
946
|
+
_.if(pigs_farmed.greaterOrEqualThan(2), () => {
|
|
947
|
+
value.set(1);
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
},
|
|
953
|
+
})
|
|
954
|
+
.events(() => ({
|
|
955
|
+
start_challenge: () => {
|
|
956
|
+
Actions.setTime({ time: "day" });
|
|
957
|
+
Actions.announce({ message: "First to kill 2 pigs wins!" });
|
|
958
|
+
},
|
|
959
|
+
init_participants: () => {
|
|
960
|
+
Actions.give({ item: "minecraft:iron_sword", count: 1 });
|
|
961
|
+
},
|
|
962
|
+
}))
|
|
963
|
+
.custom_events(({ pigs_farmed }) => [
|
|
964
|
+
{
|
|
965
|
+
score: pigs_farmed,
|
|
966
|
+
target: 1,
|
|
967
|
+
mode: "fire_once",
|
|
968
|
+
actions: () => {
|
|
969
|
+
Actions.announce({ message: "First pig down!" });
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
])
|
|
973
|
+
.end_condition(({ game_over }) => game_over.equalTo(1))
|
|
974
|
+
.win_conditions(({ pigs_farmed }, { farmer }) => ({
|
|
975
|
+
[farmer]: pigs_farmed.greaterOrEqualThan(2),
|
|
976
|
+
}));
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
### Example 2: Climb Challenge - Reach Highest Point
|
|
980
|
+
|
|
981
|
+
```typescript
|
|
982
|
+
import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges";
|
|
983
|
+
import { _, execute } from "sandstone";
|
|
984
|
+
|
|
985
|
+
createChallenge({
|
|
986
|
+
name: "climb",
|
|
987
|
+
kradle_challenge_path: "./output",
|
|
988
|
+
roles: ["climber"] as const,
|
|
989
|
+
GAME_DURATION: 3 * 60 * 20,
|
|
990
|
+
custom_variables: {
|
|
991
|
+
current_height: {
|
|
992
|
+
type: "individual",
|
|
993
|
+
objective_type: "dummy",
|
|
994
|
+
updater: (value) => {
|
|
995
|
+
execute.as("@s").store.result.score(value).run.data.get.entity("@s", "Pos[1]");
|
|
996
|
+
},
|
|
997
|
+
},
|
|
998
|
+
max_height: {
|
|
999
|
+
type: "individual",
|
|
1000
|
+
updater: (value, { current_height, main_score }) => {
|
|
1001
|
+
_.if(current_height.greaterThan(value), () => {
|
|
1002
|
+
value.set(current_height);
|
|
1003
|
+
});
|
|
1004
|
+
main_score.set(value);
|
|
1005
|
+
},
|
|
1006
|
+
},
|
|
1007
|
+
max_height_global: {
|
|
1008
|
+
type: "global",
|
|
1009
|
+
updater: (value, { max_height }) => {
|
|
1010
|
+
value.set(0);
|
|
1011
|
+
forEveryPlayer(() => {
|
|
1012
|
+
_.if(max_height.greaterThan(value), () => {
|
|
1013
|
+
value.set(max_height);
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
is_winner: {
|
|
1019
|
+
type: "individual",
|
|
1020
|
+
updater: (value, { max_height, max_height_global, has_never_died }) => {
|
|
1021
|
+
value.set(0);
|
|
1022
|
+
_.if(_.and(
|
|
1023
|
+
max_height.equalTo(max_height_global),
|
|
1024
|
+
max_height.greaterThan(0),
|
|
1025
|
+
has_never_died.equalTo(1)
|
|
1026
|
+
), () => {
|
|
1027
|
+
value.set(1);
|
|
1028
|
+
});
|
|
1029
|
+
},
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
})
|
|
1033
|
+
.events(() => ({
|
|
1034
|
+
start_challenge: () => {
|
|
1035
|
+
Actions.setTime({ time: "day" });
|
|
1036
|
+
Actions.announce({ message: "Climb as high as you can!" });
|
|
1037
|
+
},
|
|
1038
|
+
init_participants: () => {
|
|
1039
|
+
Actions.give({ item: "minecraft:cobblestone", count: 64 });
|
|
1040
|
+
Actions.give({ item: "minecraft:cobblestone", count: 64 });
|
|
1041
|
+
},
|
|
1042
|
+
}))
|
|
1043
|
+
.custom_events(() => [])
|
|
1044
|
+
.end_condition(({ game_timer }) => game_timer.greaterThan(3 * 60 * 20))
|
|
1045
|
+
.win_conditions(({ is_winner }, { climber }) => ({
|
|
1046
|
+
[climber]: is_winner.equalTo(1),
|
|
1047
|
+
}));
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
### Example 3: Battle Royale - Last Player Standing
|
|
1051
|
+
|
|
1052
|
+
```typescript
|
|
1053
|
+
import { createChallenge, Actions } from "@kradle/challenges";
|
|
1054
|
+
import { _ } from "sandstone";
|
|
1055
|
+
|
|
1056
|
+
createChallenge({
|
|
1057
|
+
name: "battle-royale",
|
|
1058
|
+
kradle_challenge_path: "./output",
|
|
1059
|
+
roles: ["fighter"] as const,
|
|
1060
|
+
GAME_DURATION: 5 * 60 * 20,
|
|
1061
|
+
custom_variables: {
|
|
1062
|
+
kills: {
|
|
1063
|
+
type: "individual",
|
|
1064
|
+
objective_type: "playerKillCount",
|
|
1065
|
+
default: 0,
|
|
1066
|
+
updater: (value, { main_score }) => {
|
|
1067
|
+
main_score.set(value);
|
|
1068
|
+
},
|
|
1069
|
+
},
|
|
1070
|
+
sole_survivor: {
|
|
1071
|
+
type: "individual",
|
|
1072
|
+
updater: (value, { alive_players, has_never_died }) => {
|
|
1073
|
+
value.set(0);
|
|
1074
|
+
_.if(_.and(
|
|
1075
|
+
alive_players.equalTo(1),
|
|
1076
|
+
has_never_died.equalTo(1)
|
|
1077
|
+
), () => {
|
|
1078
|
+
value.set(1);
|
|
1079
|
+
});
|
|
1080
|
+
},
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
})
|
|
1084
|
+
.events(() => ({
|
|
1085
|
+
start_challenge: () => {
|
|
1086
|
+
Actions.setTime({ time: "day" });
|
|
1087
|
+
Actions.gamerule({ rule: "naturalRegeneration", value: false });
|
|
1088
|
+
Actions.announce({ message: "Last player standing wins!" });
|
|
1089
|
+
},
|
|
1090
|
+
init_participants: () => {
|
|
1091
|
+
Actions.give({ item: "minecraft:stone_sword", count: 1 });
|
|
1092
|
+
Actions.give({ item: "minecraft:leather_chestplate", count: 1 });
|
|
1093
|
+
Actions.give({ item: "minecraft:cooked_beef", count: 10 });
|
|
1094
|
+
},
|
|
1095
|
+
}))
|
|
1096
|
+
.custom_events(({ kills }) => [
|
|
1097
|
+
{
|
|
1098
|
+
score: kills,
|
|
1099
|
+
target: 1,
|
|
1100
|
+
mode: "fire_once",
|
|
1101
|
+
actions: () => {
|
|
1102
|
+
Actions.announce({ message: "First blood!" });
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
])
|
|
1106
|
+
.end_condition(({ alive_players }) => alive_players.equalTo(1))
|
|
1107
|
+
.win_conditions(({ sole_survivor }, { fighter }) => ({
|
|
1108
|
+
[fighter]: sole_survivor.equalTo(1),
|
|
1109
|
+
}));
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
### Example 4: Team-Based - Hunters vs Protectors
|
|
1113
|
+
|
|
1114
|
+
```typescript
|
|
1115
|
+
import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges";
|
|
1116
|
+
import { _ } from "sandstone";
|
|
1117
|
+
|
|
1118
|
+
createChallenge({
|
|
1119
|
+
name: "pig-farming-v2",
|
|
1120
|
+
kradle_challenge_path: "./output",
|
|
1121
|
+
roles: ["pighunter", "pigsaver"] as const,
|
|
1122
|
+
GAME_DURATION: 2 * 60 * 20,
|
|
1123
|
+
custom_variables: {
|
|
1124
|
+
pigs_killed: {
|
|
1125
|
+
type: "individual",
|
|
1126
|
+
objective_type: "minecraft.killed:minecraft.pig",
|
|
1127
|
+
default: 0,
|
|
1128
|
+
updater: (value, { main_score }) => {
|
|
1129
|
+
main_score.set(value);
|
|
1130
|
+
},
|
|
1131
|
+
},
|
|
1132
|
+
pig_killed_max: {
|
|
1133
|
+
type: "global",
|
|
1134
|
+
updater: (value, { pigs_killed }) => {
|
|
1135
|
+
value.set(0);
|
|
1136
|
+
forEveryPlayer(() => {
|
|
1137
|
+
_.if(pigs_killed.greaterThan(value), () => {
|
|
1138
|
+
value.set(pigs_killed);
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
})
|
|
1145
|
+
.events((vars, { pighunter, pigsaver }) => ({
|
|
1146
|
+
start_challenge: () => {
|
|
1147
|
+
Actions.setTime({ time: "day" });
|
|
1148
|
+
Actions.announce({ message: "Hunters: Kill 2 pigs! Protectors: Stop them!" });
|
|
1149
|
+
},
|
|
1150
|
+
init_participants: () => {
|
|
1151
|
+
// Different items based on role would be set via role-specific logic
|
|
1152
|
+
Actions.give({ item: "minecraft:wooden_sword", count: 1 });
|
|
1153
|
+
},
|
|
1154
|
+
}))
|
|
1155
|
+
.custom_events(() => [])
|
|
1156
|
+
.end_condition(({ pig_killed_max }) => pig_killed_max.greaterOrEqualThan(2))
|
|
1157
|
+
.win_conditions(({ pig_killed_max }, { pighunter, pigsaver }) => ({
|
|
1158
|
+
[pighunter]: pig_killed_max.greaterOrEqualThan(2),
|
|
1159
|
+
[pigsaver]: pig_killed_max.lowerThan(2),
|
|
1160
|
+
}));
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
### Example 5: Capture the Flag
|
|
1164
|
+
|
|
1165
|
+
```typescript
|
|
1166
|
+
import { createChallenge, Actions } from "@kradle/challenges";
|
|
1167
|
+
import { _, Selector, rel } from "sandstone";
|
|
1168
|
+
import type { Score } from "sandstone";
|
|
1169
|
+
|
|
1170
|
+
createChallenge({
|
|
1171
|
+
name: "capture-the-flag",
|
|
1172
|
+
kradle_challenge_path: "./output",
|
|
1173
|
+
roles: ["red_team", "blue_team"] as const,
|
|
1174
|
+
GAME_DURATION: 5 * 60 * 20,
|
|
1175
|
+
custom_variables: {
|
|
1176
|
+
holds_red_banner: {
|
|
1177
|
+
type: "individual",
|
|
1178
|
+
updater: (value: Score) => {
|
|
1179
|
+
value.set(0);
|
|
1180
|
+
_.if(Selector("@s", {
|
|
1181
|
+
nbt: { Inventory: [{ id: "minecraft:red_banner" }] }
|
|
1182
|
+
}), () => {
|
|
1183
|
+
value.set(1);
|
|
1184
|
+
});
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
stands_blue_wool: {
|
|
1188
|
+
type: "individual",
|
|
1189
|
+
updater: (value: Score) => {
|
|
1190
|
+
value.set(0);
|
|
1191
|
+
_.if(_.block(rel(0, -1, 0), "minecraft:blue_wool"), () => {
|
|
1192
|
+
value.set(1);
|
|
1193
|
+
});
|
|
1194
|
+
},
|
|
1195
|
+
},
|
|
1196
|
+
captured_flag: {
|
|
1197
|
+
type: "individual",
|
|
1198
|
+
updater: (value, { holds_red_banner, stands_blue_wool, main_score }) => {
|
|
1199
|
+
value.set(0);
|
|
1200
|
+
_.if(_.and(
|
|
1201
|
+
holds_red_banner.equalTo(1),
|
|
1202
|
+
stands_blue_wool.equalTo(1)
|
|
1203
|
+
), () => {
|
|
1204
|
+
value.set(1);
|
|
1205
|
+
main_score.set(1);
|
|
1206
|
+
});
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1209
|
+
},
|
|
1210
|
+
})
|
|
1211
|
+
.events(() => ({
|
|
1212
|
+
start_challenge: () => {
|
|
1213
|
+
Actions.announce({ message: "Capture the enemy flag and return to base!" });
|
|
1214
|
+
},
|
|
1215
|
+
init_participants: () => {
|
|
1216
|
+
Actions.give({ item: "minecraft:iron_sword", count: 1 });
|
|
1217
|
+
},
|
|
1218
|
+
}))
|
|
1219
|
+
.custom_events(({ captured_flag }) => [
|
|
1220
|
+
{
|
|
1221
|
+
score: captured_flag,
|
|
1222
|
+
target: 1,
|
|
1223
|
+
mode: "fire_once",
|
|
1224
|
+
actions: () => {
|
|
1225
|
+
Actions.announce({ message: "Flag captured! Game over!" });
|
|
1226
|
+
},
|
|
1227
|
+
},
|
|
1228
|
+
])
|
|
1229
|
+
.end_condition(({ captured_flag }) => captured_flag.equalTo(1))
|
|
1230
|
+
.win_conditions(({ captured_flag }, { red_team, blue_team }) => ({
|
|
1231
|
+
[red_team]: captured_flag.equalTo(0),
|
|
1232
|
+
[blue_team]: captured_flag.equalTo(1),
|
|
1233
|
+
}));
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
---
|
|
1237
|
+
|
|
1238
|
+
## Common Patterns
|
|
1239
|
+
|
|
1240
|
+
### Pattern 1: Sync Variable to Main Score
|
|
1241
|
+
|
|
1242
|
+
```typescript
|
|
1243
|
+
my_variable: {
|
|
1244
|
+
type: "individual",
|
|
1245
|
+
objective_type: "some_criterion",
|
|
1246
|
+
updater: (value, { main_score }) => {
|
|
1247
|
+
main_score.set(value);
|
|
1248
|
+
},
|
|
1249
|
+
}
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
### Pattern 2: Find Global Maximum
|
|
1253
|
+
|
|
1254
|
+
```typescript
|
|
1255
|
+
max_global: {
|
|
1256
|
+
type: "global",
|
|
1257
|
+
updater: (value, { individual_score }) => {
|
|
1258
|
+
value.set(0);
|
|
1259
|
+
forEveryPlayer(() => {
|
|
1260
|
+
_.if(individual_score.greaterThan(value), () => {
|
|
1261
|
+
value.set(individual_score);
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
},
|
|
1265
|
+
}
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
### Pattern 3: Winner Detection (Has Max Score + Alive)
|
|
1269
|
+
|
|
1270
|
+
```typescript
|
|
1271
|
+
is_winner: {
|
|
1272
|
+
type: "individual",
|
|
1273
|
+
updater: (value, { main_score, max_global, has_never_died }) => {
|
|
1274
|
+
value.set(0);
|
|
1275
|
+
_.if(_.and(
|
|
1276
|
+
main_score.equalTo(max_global),
|
|
1277
|
+
main_score.greaterThan(0),
|
|
1278
|
+
has_never_died.equalTo(1)
|
|
1279
|
+
), () => {
|
|
1280
|
+
value.set(1);
|
|
1281
|
+
});
|
|
1282
|
+
},
|
|
1283
|
+
}
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
### Pattern 4: Track Player Position
|
|
1287
|
+
|
|
1288
|
+
```typescript
|
|
1289
|
+
current_y: {
|
|
1290
|
+
type: "individual",
|
|
1291
|
+
objective_type: "dummy",
|
|
1292
|
+
updater: (value) => {
|
|
1293
|
+
execute.as("@s").store.result.score(value).run.data.get.entity("@s", "Pos[1]");
|
|
1294
|
+
},
|
|
1295
|
+
}
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
### Pattern 5: Check Inventory for Item
|
|
1299
|
+
|
|
1300
|
+
```typescript
|
|
1301
|
+
has_diamond: {
|
|
1302
|
+
type: "individual",
|
|
1303
|
+
updater: (value: Score) => {
|
|
1304
|
+
value.set(0);
|
|
1305
|
+
_.if(Selector("@s", {
|
|
1306
|
+
nbt: { Inventory: [{ id: "minecraft:diamond" }] }
|
|
1307
|
+
}), () => {
|
|
1308
|
+
value.set(1);
|
|
1309
|
+
});
|
|
1310
|
+
},
|
|
1311
|
+
}
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
### Pattern 6: Check Block Below Player
|
|
1315
|
+
|
|
1316
|
+
```typescript
|
|
1317
|
+
on_gold_block: {
|
|
1318
|
+
type: "individual",
|
|
1319
|
+
updater: (value: Score) => {
|
|
1320
|
+
value.set(0);
|
|
1321
|
+
_.if(_.block(rel(0, -1, 0), "minecraft:gold_block"), () => {
|
|
1322
|
+
value.set(1);
|
|
1323
|
+
});
|
|
1324
|
+
},
|
|
1325
|
+
}
|
|
1326
|
+
```
|
|
1327
|
+
|
|
1328
|
+
### Pattern 7: Count Entities
|
|
1329
|
+
|
|
1330
|
+
```typescript
|
|
1331
|
+
zombie_count: {
|
|
1332
|
+
type: "global",
|
|
1333
|
+
updater: (value) => {
|
|
1334
|
+
value.set(0);
|
|
1335
|
+
execute.as(Selector("@e", { type: "zombie" })).run(() => {
|
|
1336
|
+
value.add(1);
|
|
1337
|
+
});
|
|
1338
|
+
},
|
|
1339
|
+
}
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
### Pattern 8: Simple End Condition
|
|
1343
|
+
|
|
1344
|
+
```typescript
|
|
1345
|
+
.end_condition(({ objective_complete }) => objective_complete.equalTo(1))
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
### Pattern 9: Multi-Condition End
|
|
1349
|
+
|
|
1350
|
+
```typescript
|
|
1351
|
+
.end_condition(({ alive_players, objective_complete }) =>
|
|
1352
|
+
_.or(
|
|
1353
|
+
alive_players.equalTo(1),
|
|
1354
|
+
objective_complete.equalTo(1)
|
|
1355
|
+
)
|
|
1356
|
+
)
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
### Pattern 10: Opposing Team Win Conditions
|
|
1360
|
+
|
|
1361
|
+
```typescript
|
|
1362
|
+
.win_conditions(({ team_a_score, team_b_score }, { team_a, team_b }) => ({
|
|
1363
|
+
[team_a]: team_a_score.greaterThan(team_b_score),
|
|
1364
|
+
[team_b]: team_b_score.greaterThan(team_a_score),
|
|
1365
|
+
}))
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
---
|
|
1369
|
+
|
|
1370
|
+
## Tips for LLMs
|
|
1371
|
+
|
|
1372
|
+
1. **Always use `as const`** for roles array to get proper type inference
|
|
1373
|
+
2. **Updaters should be idempotent** - they run every tick
|
|
1374
|
+
3. **Use `forEveryPlayer` for global aggregations** (max, count, any/all checks)
|
|
1375
|
+
4. **Import `_` from sandstone** for conditions (`_.if`, `_.and`, `_.or`)
|
|
1376
|
+
5. **Variables are Scores** - use `.set()`, `.add()`, comparison methods
|
|
1377
|
+
6. **Main score is displayed** - sync your primary metric to `main_score`
|
|
1378
|
+
7. **Time is in ticks** - multiply seconds by 20
|
|
1379
|
+
8. **Win conditions are per-role** - each role needs its own condition
|
|
1380
|
+
9. **Custom events fire per-player** for individual variables
|