redscript-mc 1.0.0 → 1.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.
Files changed (136) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
  4. package/CHANGELOG.md +112 -0
  5. package/CONTRIBUTING.md +140 -0
  6. package/README.md +28 -19
  7. package/README.zh.md +28 -19
  8. package/dist/__tests__/cli.test.js +148 -10
  9. package/dist/__tests__/codegen.test.js +26 -1
  10. package/dist/__tests__/diagnostics.test.js +5 -5
  11. package/dist/__tests__/e2e.test.js +336 -17
  12. package/dist/__tests__/formatter.test.d.ts +1 -0
  13. package/dist/__tests__/formatter.test.js +40 -0
  14. package/dist/__tests__/lexer.test.js +12 -2
  15. package/dist/__tests__/lowering.test.js +200 -12
  16. package/dist/__tests__/mc-integration.test.js +370 -31
  17. package/dist/__tests__/mc-syntax.test.js +3 -3
  18. package/dist/__tests__/nbt.test.js +2 -2
  19. package/dist/__tests__/optimizer-advanced.test.js +5 -5
  20. package/dist/__tests__/parser.test.js +80 -0
  21. package/dist/__tests__/runtime.test.js +9 -9
  22. package/dist/__tests__/typechecker.test.js +158 -0
  23. package/dist/ast/types.d.ts +40 -3
  24. package/dist/cli.js +25 -7
  25. package/dist/codegen/mcfunction/index.d.ts +1 -1
  26. package/dist/codegen/mcfunction/index.js +38 -3
  27. package/dist/codegen/structure/index.js +32 -1
  28. package/dist/compile.d.ts +10 -0
  29. package/dist/compile.js +36 -5
  30. package/dist/events/types.d.ts +35 -0
  31. package/dist/events/types.js +59 -0
  32. package/dist/formatter/index.d.ts +1 -0
  33. package/dist/formatter/index.js +26 -0
  34. package/dist/index.js +3 -2
  35. package/dist/ir/builder.d.ts +2 -1
  36. package/dist/ir/types.d.ts +11 -2
  37. package/dist/ir/types.js +1 -1
  38. package/dist/lexer/index.d.ts +1 -1
  39. package/dist/lexer/index.js +2 -0
  40. package/dist/lowering/index.d.ts +34 -1
  41. package/dist/lowering/index.js +622 -23
  42. package/dist/mc-test/runner.d.ts +2 -2
  43. package/dist/mc-test/runner.js +3 -3
  44. package/dist/mc-test/setup.js +2 -2
  45. package/dist/parser/index.d.ts +4 -0
  46. package/dist/parser/index.js +153 -16
  47. package/dist/typechecker/index.d.ts +17 -0
  48. package/dist/typechecker/index.js +343 -17
  49. package/docs/COMPILATION_STATS.md +24 -24
  50. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  51. package/docs/IMPLEMENTATION_GUIDE.md +1 -1
  52. package/docs/STRUCTURE_TARGET.md +1 -1
  53. package/editors/vscode/.vscodeignore +1 -0
  54. package/editors/vscode/CHANGELOG.md +9 -0
  55. package/editors/vscode/icons/mcrs.svg +7 -0
  56. package/editors/vscode/icons/redscript-icons.json +10 -0
  57. package/editors/vscode/out/extension.js +1295 -80
  58. package/editors/vscode/package-lock.json +2 -2
  59. package/editors/vscode/package.json +10 -3
  60. package/editors/vscode/src/hover.ts +55 -2
  61. package/editors/vscode/src/symbols.ts +42 -0
  62. package/package.json +1 -1
  63. package/src/__tests__/cli.test.ts +176 -10
  64. package/src/__tests__/codegen.test.ts +28 -1
  65. package/src/__tests__/diagnostics.test.ts +5 -5
  66. package/src/__tests__/e2e.test.ts +335 -17
  67. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  68. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  69. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  70. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  71. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  72. package/src/__tests__/lexer.test.ts +14 -2
  73. package/src/__tests__/lowering.test.ts +226 -12
  74. package/src/__tests__/mc-integration.test.ts +421 -31
  75. package/src/__tests__/mc-syntax.test.ts +3 -3
  76. package/src/__tests__/nbt.test.ts +2 -2
  77. package/src/__tests__/optimizer-advanced.test.ts +5 -5
  78. package/src/__tests__/parser.test.ts +91 -5
  79. package/src/__tests__/runtime.test.ts +9 -9
  80. package/src/__tests__/typechecker.test.ts +171 -0
  81. package/src/ast/types.ts +44 -3
  82. package/src/cli.ts +10 -10
  83. package/src/codegen/mcfunction/index.ts +40 -3
  84. package/src/codegen/structure/index.ts +35 -1
  85. package/src/compile.ts +54 -6
  86. package/src/events/types.ts +69 -0
  87. package/src/examples/capture_the_flag.mcrs +208 -0
  88. package/src/examples/{counter.rs → counter.mcrs} +1 -1
  89. package/src/examples/hunger_games.mcrs +301 -0
  90. package/src/examples/new_features_demo.mcrs +193 -0
  91. package/src/examples/parkour_race.mcrs +233 -0
  92. package/src/examples/rpg.mcrs +13 -0
  93. package/src/examples/{shop.rs → shop.mcrs} +1 -1
  94. package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
  95. package/src/examples/{turret.rs → turret.mcrs} +1 -1
  96. package/src/examples/zombie_survival.mcrs +314 -0
  97. package/src/index.ts +4 -3
  98. package/src/ir/builder.ts +3 -1
  99. package/src/ir/types.ts +12 -2
  100. package/src/lexer/index.ts +3 -1
  101. package/src/lowering/index.ts +684 -24
  102. package/src/mc-test/runner.ts +3 -3
  103. package/src/mc-test/setup.ts +2 -2
  104. package/src/parser/index.ts +170 -19
  105. package/src/stdlib/README.md +178 -140
  106. package/src/stdlib/bossbar.mcrs +68 -0
  107. package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
  108. package/src/stdlib/effects.mcrs +64 -0
  109. package/src/stdlib/interactions.mcrs +195 -0
  110. package/src/stdlib/inventory.mcrs +38 -0
  111. package/src/stdlib/mobs.mcrs +99 -0
  112. package/src/stdlib/particles.mcrs +52 -0
  113. package/src/stdlib/sets.mcrs +20 -0
  114. package/src/stdlib/spawn.mcrs +41 -0
  115. package/src/stdlib/tags.mcrs +951 -0
  116. package/src/stdlib/teams.mcrs +68 -0
  117. package/src/stdlib/timer.mcrs +72 -0
  118. package/src/stdlib/world.mcrs +92 -0
  119. package/src/typechecker/index.ts +404 -18
  120. package/src/examples/rpg.rs +0 -13
  121. package/src/stdlib/mobs.rs +0 -99
  122. package/src/stdlib/timer.rs +0 -51
  123. /package/src/examples/{arena.rs → arena.mcrs} +0 -0
  124. /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
  125. /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
  126. /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
  127. /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
  128. /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
  129. /package/src/stdlib/{math.rs → math.mcrs} +0 -0
  130. /package/src/stdlib/{player.rs → player.mcrs} +0 -0
  131. /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
  132. /package/src/templates/{combat.rs → combat.mcrs} +0 -0
  133. /package/src/templates/{economy.rs → economy.mcrs} +0 -0
  134. /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
  135. /package/src/templates/{quest.rs → quest.mcrs} +0 -0
  136. /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
@@ -8,6 +8,7 @@ import { optimizeForStructure, optimizeForStructureWithStats } from '../../optim
8
8
  import { preprocessSource } from '../../compile'
9
9
  import type { IRCommand, IRFunction, IRModule } from '../../ir/types'
10
10
  import type { DatapackFile } from '../mcfunction'
11
+ import { EVENT_TYPES, isEventTypeName, type EventTypeName } from '../../events/types'
11
12
 
12
13
  const DATA_VERSION = 3953
13
14
  const MAX_WIDTH = 16
@@ -87,9 +88,13 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
87
88
  const entries: CommandEntry[] = []
88
89
  const triggerHandlers = module.functions.filter(fn => fn.isTriggerHandler && fn.triggerName)
89
90
  const triggerNames = new Set(triggerHandlers.map(fn => fn.triggerName!))
91
+ const eventHandlers = module.functions.filter((fn): fn is IRFunction & { eventHandler: { eventType: EventTypeName; tag: string } } =>
92
+ !!fn.eventHandler && isEventTypeName(fn.eventHandler.eventType)
93
+ )
94
+ const eventTypes = new Set<EventTypeName>(eventHandlers.map(fn => fn.eventHandler.eventType))
90
95
  const loadCommands = [
91
96
  `scoreboard objectives add ${OBJ} dummy`,
92
- ...module.globals.map(globalName => `scoreboard players set ${varRef(globalName)} ${OBJ} 0`),
97
+ ...module.globals.map(g => `scoreboard players set ${varRef(g.name)} ${OBJ} ${g.init}`),
93
98
  ...Array.from(triggerNames).flatMap(triggerName => [
94
99
  `scoreboard objectives add ${triggerName} trigger`,
95
100
  `scoreboard players enable @a ${triggerName}`,
@@ -99,6 +104,21 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
99
104
  ).map(constSetup),
100
105
  ]
101
106
 
107
+ for (const eventType of eventTypes) {
108
+ if (eventType === 'PlayerDeath') {
109
+ loadCommands.push('scoreboard objectives add rs.deaths deathCount')
110
+ } else if (eventType === 'EntityKill') {
111
+ loadCommands.push('scoreboard objectives add rs.kills totalKillCount')
112
+ }
113
+ }
114
+
115
+ // Call @load functions from __load
116
+ for (const fn of module.functions) {
117
+ if (fn.isLoadInit) {
118
+ loadCommands.push(`function ${module.namespace}:${fn.name}`)
119
+ }
120
+ }
121
+
102
122
  const sections: Array<{ name: string; commands: IRCommand[]; repeat?: boolean }> = []
103
123
 
104
124
  if (loadCommands.length > 0) {
@@ -139,6 +159,20 @@ function collectCommandEntriesFromModule(module: IRModule): CommandEntry[] {
139
159
  })
140
160
  }
141
161
  }
162
+ if (eventHandlers.length > 0) {
163
+ for (const eventType of eventTypes) {
164
+ const tag = EVENT_TYPES[eventType].tag
165
+ const handlers = eventHandlers.filter(fn => fn.eventHandler?.eventType === eventType)
166
+ for (const handler of handlers) {
167
+ tickCommands.push({
168
+ cmd: `execute as @a[tag=${tag}] run function ${module.namespace}:${handler.name}`,
169
+ })
170
+ }
171
+ tickCommands.push({
172
+ cmd: `tag @a[tag=${tag}] remove ${tag}`,
173
+ })
174
+ }
175
+ }
142
176
  if (tickCommands.length > 0) {
143
177
  sections.push({
144
178
  name: '__tick',
package/src/compile.ts CHANGED
@@ -39,6 +39,17 @@ export interface CompileResult {
39
39
  error?: DiagnosticError
40
40
  }
41
41
 
42
+ export interface SourceRange {
43
+ startLine: number
44
+ endLine: number
45
+ filePath: string
46
+ }
47
+
48
+ export interface PreprocessedSource {
49
+ source: string
50
+ ranges: SourceRange[]
51
+ }
52
+
42
53
  const IMPORT_RE = /^\s*import\s+"([^"]+)"\s*;?\s*$/
43
54
 
44
55
  interface PreprocessOptions {
@@ -46,7 +57,19 @@ interface PreprocessOptions {
46
57
  seen?: Set<string>
47
58
  }
48
59
 
49
- export function preprocessSource(source: string, options: PreprocessOptions = {}): string {
60
+ function countLines(source: string): number {
61
+ return source === '' ? 0 : source.split('\n').length
62
+ }
63
+
64
+ function offsetRanges(ranges: SourceRange[], lineOffset: number): SourceRange[] {
65
+ return ranges.map(range => ({
66
+ startLine: range.startLine + lineOffset,
67
+ endLine: range.endLine + lineOffset,
68
+ filePath: range.filePath,
69
+ }))
70
+ }
71
+
72
+ export function preprocessSourceWithMetadata(source: string, options: PreprocessOptions = {}): PreprocessedSource {
50
73
  const { filePath } = options
51
74
  const seen = options.seen ?? new Set<string>()
52
75
 
@@ -55,7 +78,7 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
55
78
  }
56
79
 
57
80
  const lines = source.split('\n')
58
- const imports: string[] = []
81
+ const imports: PreprocessedSource[] = []
59
82
  const bodyLines: string[] = []
60
83
  let parsingHeader = true
61
84
 
@@ -90,7 +113,7 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
90
113
  )
91
114
  }
92
115
 
93
- imports.push(preprocessSource(importedSource, { filePath: importPath, seen }))
116
+ imports.push(preprocessSourceWithMetadata(importedSource, { filePath: importPath, seen }))
94
117
  }
95
118
  continue
96
119
  }
@@ -104,7 +127,31 @@ export function preprocessSource(source: string, options: PreprocessOptions = {}
104
127
  bodyLines.push(line)
105
128
  }
106
129
 
107
- return [...imports, bodyLines.join('\n')].filter(Boolean).join('\n')
130
+ const body = bodyLines.join('\n')
131
+ const parts = [...imports.map(entry => entry.source), body].filter(Boolean)
132
+ const combined = parts.join('\n')
133
+
134
+ const ranges: SourceRange[] = []
135
+ let lineOffset = 0
136
+
137
+ for (const entry of imports) {
138
+ ranges.push(...offsetRanges(entry.ranges, lineOffset))
139
+ lineOffset += countLines(entry.source)
140
+ }
141
+
142
+ if (filePath && body) {
143
+ ranges.push({
144
+ startLine: lineOffset + 1,
145
+ endLine: lineOffset + countLines(body),
146
+ filePath: path.resolve(filePath),
147
+ })
148
+ }
149
+
150
+ return { source: combined, ranges }
151
+ }
152
+
153
+ export function preprocessSource(source: string, options: PreprocessOptions = {}): string {
154
+ return preprocessSourceWithMetadata(source, options).source
108
155
  }
109
156
 
110
157
  // ---------------------------------------------------------------------------
@@ -116,7 +163,8 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
116
163
  let sourceLines = source.split('\n')
117
164
 
118
165
  try {
119
- const preprocessedSource = preprocessSource(source, { filePath })
166
+ const preprocessed = preprocessSourceWithMetadata(source, { filePath })
167
+ const preprocessedSource = preprocessed.source
120
168
  sourceLines = preprocessedSource.split('\n')
121
169
 
122
170
  // Lexing
@@ -126,7 +174,7 @@ export function compile(source: string, options: CompileOptions = {}): CompileRe
126
174
  const ast = new Parser(tokens, preprocessedSource, filePath).parse(namespace)
127
175
 
128
176
  // Lowering
129
- const ir = new Lowering(namespace).lower(ast)
177
+ const ir = new Lowering(namespace, preprocessed.ranges).lower(ast)
130
178
 
131
179
  // Optimization
132
180
  const optimized: IRModule = shouldOptimize
@@ -0,0 +1,69 @@
1
+ import type { TypeNode } from '../ast/types'
2
+
3
+ export const EVENT_TYPES = {
4
+ PlayerDeath: {
5
+ tag: 'rs.just_died',
6
+ params: ['player: Player'],
7
+ detection: 'scoreboard',
8
+ },
9
+ PlayerJoin: {
10
+ tag: 'rs.just_joined',
11
+ params: ['player: Player'],
12
+ detection: 'tag',
13
+ },
14
+ BlockBreak: {
15
+ tag: 'rs.just_broke_block',
16
+ params: ['player: Player', 'block: string'],
17
+ detection: 'advancement',
18
+ },
19
+ EntityKill: {
20
+ tag: 'rs.just_killed',
21
+ params: ['player: Player'],
22
+ detection: 'scoreboard',
23
+ },
24
+ ItemUse: {
25
+ tag: 'rs.just_used_item',
26
+ params: ['player: Player'],
27
+ detection: 'scoreboard',
28
+ },
29
+ } as const
30
+
31
+ export type EventTypeName = keyof typeof EVENT_TYPES
32
+
33
+ export interface EventParamSpec {
34
+ name: string
35
+ type: TypeNode
36
+ }
37
+
38
+ export function isEventTypeName(value: string): value is EventTypeName {
39
+ return value in EVENT_TYPES
40
+ }
41
+
42
+ export function getEventParamSpecs(eventType: EventTypeName): EventParamSpec[] {
43
+ return EVENT_TYPES[eventType].params.map(parseEventParam)
44
+ }
45
+
46
+ function parseEventParam(spec: string): EventParamSpec {
47
+ const match = spec.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([A-Za-z_][A-Za-z0-9_]*)$/)
48
+ if (!match) {
49
+ throw new Error(`Invalid event parameter spec: ${spec}`)
50
+ }
51
+
52
+ const [, name, typeName] = match
53
+ return {
54
+ name,
55
+ type: toTypeNode(typeName),
56
+ }
57
+ }
58
+
59
+ function toTypeNode(typeName: string): TypeNode {
60
+ if (typeName === 'Player') {
61
+ return { kind: 'entity', entityType: 'Player' }
62
+ }
63
+
64
+ if (typeName === 'string' || typeName === 'int' || typeName === 'bool' || typeName === 'float' || typeName === 'void' || typeName === 'BlockPos' || typeName === 'byte' || typeName === 'short' || typeName === 'long' || typeName === 'double') {
65
+ return { kind: 'named', name: typeName }
66
+ }
67
+
68
+ return { kind: 'struct', name: typeName }
69
+ }
@@ -0,0 +1,208 @@
1
+ // ============================================
2
+ // Capture the Flag Mini-Game
3
+ // ============================================
4
+ // 场景:两队对抗,抢夺对方旗帜带回己方基地得分
5
+ // Scenario: Two teams compete to capture enemy flag
6
+ // ============================================
7
+
8
+ import "../stdlib/teams.mcrs"
9
+ import "../stdlib/effects.mcrs"
10
+ import "../stdlib/world.mcrs"
11
+ import "../stdlib/inventory.mcrs"
12
+ import "../stdlib/particles.mcrs"
13
+
14
+ // ===== 配置 =====
15
+ const RED_BASE_X: int = -50;
16
+ const RED_BASE_Z: int = 0;
17
+ const BLUE_BASE_X: int = 50;
18
+ const BLUE_BASE_Z: int = 0;
19
+ const BASE_Y: int = 64;
20
+ const WIN_SCORE: int = 3;
21
+
22
+ // ===== 游戏状态 =====
23
+ struct GameState {
24
+ running: int,
25
+ red_score: int,
26
+ blue_score: int,
27
+ red_flag_taken: int,
28
+ blue_flag_taken: int
29
+ }
30
+
31
+ let game: GameState = GameState {
32
+ running: 0,
33
+ red_score: 0,
34
+ blue_score: 0,
35
+ red_flag_taken: 0,
36
+ blue_flag_taken: 0
37
+ };
38
+
39
+ // ===== 初始化 =====
40
+ @load
41
+ fn init() {
42
+ // 创建记分板
43
+ scoreboard_add_objective("ctf_team", "dummy");
44
+ scoreboard_add_objective("ctf_flag", "dummy");
45
+
46
+ // 创建队伍
47
+ setup_two_teams();
48
+
49
+ // 世界设置
50
+ set_day();
51
+ weather_clear();
52
+ enable_keep_inventory();
53
+
54
+ announce("§e[CTF] §f夺旗战已加载!使用 /trigger start 开始游戏");
55
+ }
56
+
57
+ // ===== 开始游戏 =====
58
+ fn start_game() {
59
+ game.running = 1;
60
+ game.red_score = 0;
61
+ game.blue_score = 0;
62
+
63
+ // 随机分配队伍
64
+ assign_teams();
65
+
66
+ // 传送到各自基地
67
+ tp(@a[team=red], RED_BASE_X, BASE_Y, RED_BASE_Z);
68
+ tp(@a[team=blue], BLUE_BASE_X, BASE_Y, BLUE_BASE_Z);
69
+
70
+ // 给装备
71
+ foreach (p in @a) {
72
+ clear_inventory(p);
73
+ give_kit_warrior(p);
74
+ }
75
+
76
+ // 放置旗帜(盔甲架)
77
+ place_flags();
78
+
79
+ announce("§e[CTF] §a游戏开始!抢夺敌方旗帜!");
80
+ title(@a, "§6夺旗战开始!");
81
+ }
82
+
83
+ fn place_flags() {
84
+ // 红方旗帜(红色旗帜方块)
85
+ setblock(RED_BASE_X, BASE_Y + 1, RED_BASE_Z, "minecraft:red_banner");
86
+ // 蓝方旗帜
87
+ setblock(BLUE_BASE_X, BASE_Y + 1, BLUE_BASE_Z, "minecraft:blue_banner");
88
+
89
+ game.red_flag_taken = 0;
90
+ game.blue_flag_taken = 0;
91
+ }
92
+
93
+ fn assign_teams() {
94
+ let count: int = 0;
95
+ foreach (p in @a) {
96
+ if (count % 2 == 0) {
97
+ team_join(p, "red");
98
+ scoreboard_set(p, "ctf_team", 1);
99
+ } else {
100
+ team_join(p, "blue");
101
+ scoreboard_set(p, "ctf_team", 2);
102
+ }
103
+ count = count + 1;
104
+ }
105
+ }
106
+
107
+ // ===== 每 tick 检查 =====
108
+ @tick
109
+ fn game_tick() {
110
+ if (game.running == 1) {
111
+ check_flag_pickup();
112
+ check_flag_capture();
113
+ check_win();
114
+ }
115
+ }
116
+
117
+ fn check_flag_pickup() {
118
+ // 检查蓝队玩家是否靠近红旗
119
+ foreach (p in @a[team=blue]) {
120
+ // 如果在红方基地附近且旗帜未被拿走
121
+ let dist: int = distance_to(p, RED_BASE_X, BASE_Y, RED_BASE_Z);
122
+ if (dist < 3) {
123
+ if (game.red_flag_taken == 0) {
124
+ game.red_flag_taken = 1;
125
+ tag_add(p, "has_flag");
126
+ setblock(RED_BASE_X, BASE_Y + 1, RED_BASE_Z, "minecraft:air");
127
+ announce("§e[CTF] §9蓝队 §f拿到了 §c红方旗帜§f!");
128
+ glow(p, 9999);
129
+ }
130
+ }
131
+ }
132
+
133
+ // 检查红队玩家是否靠近蓝旗
134
+ foreach (p in @a[team=red]) {
135
+ let dist: int = distance_to(p, BLUE_BASE_X, BASE_Y, BLUE_BASE_Z);
136
+ if (dist < 3) {
137
+ if (game.blue_flag_taken == 0) {
138
+ game.blue_flag_taken = 1;
139
+ tag_add(p, "has_flag");
140
+ setblock(BLUE_BASE_X, BASE_Y + 1, BLUE_BASE_Z, "minecraft:air");
141
+ announce("§e[CTF] §c红队 §f拿到了 §9蓝方旗帜§f!");
142
+ glow(p, 9999);
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ fn check_flag_capture() {
149
+ // 蓝队带红旗回蓝方基地
150
+ foreach (p in @a[team=blue, tag=has_flag]) {
151
+ let dist: int = distance_to(p, BLUE_BASE_X, BASE_Y, BLUE_BASE_Z);
152
+ if (dist < 5) {
153
+ game.blue_score = game.blue_score + 1;
154
+ announce("§e[CTF] §9蓝队 §a得分!§f (" + game.blue_score + "/" + WIN_SCORE + ")");
155
+ tag_remove(p, "has_flag");
156
+ effect_clear(p, "minecraft:glowing");
157
+ place_flags();
158
+ happy(p);
159
+ }
160
+ }
161
+
162
+ // 红队带蓝旗回红方基地
163
+ foreach (p in @a[team=red, tag=has_flag]) {
164
+ let dist: int = distance_to(p, RED_BASE_X, BASE_Y, RED_BASE_Z);
165
+ if (dist < 5) {
166
+ game.red_score = game.red_score + 1;
167
+ announce("§e[CTF] §c红队 §a得分!§f (" + game.red_score + "/" + WIN_SCORE + ")");
168
+ tag_remove(p, "has_flag");
169
+ effect_clear(p, "minecraft:glowing");
170
+ place_flags();
171
+ happy(p);
172
+ }
173
+ }
174
+ }
175
+
176
+ fn check_win() {
177
+ if (game.red_score >= WIN_SCORE) {
178
+ end_game("red");
179
+ }
180
+ if (game.blue_score >= WIN_SCORE) {
181
+ end_game("blue");
182
+ }
183
+ }
184
+
185
+ fn end_game(winner: string) {
186
+ game.running = 0;
187
+
188
+ if (winner == "red") {
189
+ title(@a, "§c红队获胜!");
190
+ announce("§e[CTF] §c红队 §6赢得了比赛!");
191
+ } else {
192
+ title(@a, "§9蓝队获胜!");
193
+ announce("§e[CTF] §9蓝队 §6赢得了比赛!");
194
+ }
195
+
196
+ // 清理
197
+ foreach (p in @a[tag=has_flag]) {
198
+ tag_remove(p, "has_flag");
199
+ effect_clear(p);
200
+ }
201
+ }
202
+
203
+ // ===== 辅助函数 =====
204
+ fn distance_to(target: selector, x: int, y: int, z: int) -> int {
205
+ // 简化的距离计算(使用记分板)
206
+ let dist: int = scoreboard_get(target, "ctf_dist");
207
+ return dist;
208
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  @tick
4
4
  fn counter_tick() {
5
- let ticks: int = scoreboard_get("counter", #ticks);
5
+ let ticks = scoreboard_get("counter", #ticks);
6
6
  ticks = ticks + 1;
7
7
  scoreboard_set("counter", #ticks, ticks);
8
8