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