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.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
- package/CHANGELOG.md +112 -0
- package/CONTRIBUTING.md +140 -0
- package/README.md +28 -19
- package/README.zh.md +28 -19
- package/dist/__tests__/cli.test.js +148 -10
- package/dist/__tests__/codegen.test.js +26 -1
- package/dist/__tests__/diagnostics.test.js +5 -5
- package/dist/__tests__/e2e.test.js +336 -17
- package/dist/__tests__/formatter.test.d.ts +1 -0
- package/dist/__tests__/formatter.test.js +40 -0
- package/dist/__tests__/lexer.test.js +12 -2
- package/dist/__tests__/lowering.test.js +200 -12
- package/dist/__tests__/mc-integration.test.js +370 -31
- package/dist/__tests__/mc-syntax.test.js +3 -3
- package/dist/__tests__/nbt.test.js +2 -2
- package/dist/__tests__/optimizer-advanced.test.js +5 -5
- package/dist/__tests__/parser.test.js +80 -0
- package/dist/__tests__/runtime.test.js +9 -9
- package/dist/__tests__/typechecker.test.js +158 -0
- package/dist/ast/types.d.ts +40 -3
- package/dist/cli.js +25 -7
- package/dist/codegen/mcfunction/index.d.ts +1 -1
- package/dist/codegen/mcfunction/index.js +38 -3
- package/dist/codegen/structure/index.js +32 -1
- package/dist/compile.d.ts +10 -0
- package/dist/compile.js +36 -5
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/formatter/index.d.ts +1 -0
- package/dist/formatter/index.js +26 -0
- package/dist/index.js +3 -2
- package/dist/ir/builder.d.ts +2 -1
- package/dist/ir/types.d.ts +11 -2
- package/dist/ir/types.js +1 -1
- package/dist/lexer/index.d.ts +1 -1
- package/dist/lexer/index.js +2 -0
- package/dist/lowering/index.d.ts +34 -1
- package/dist/lowering/index.js +622 -23
- package/dist/mc-test/runner.d.ts +2 -2
- package/dist/mc-test/runner.js +3 -3
- package/dist/mc-test/setup.js +2 -2
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +153 -16
- package/dist/typechecker/index.d.ts +17 -0
- package/dist/typechecker/index.js +343 -17
- package/docs/COMPILATION_STATS.md +24 -24
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/docs/IMPLEMENTATION_GUIDE.md +1 -1
- package/docs/STRUCTURE_TARGET.md +1 -1
- package/editors/vscode/.vscodeignore +1 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icons/mcrs.svg +7 -0
- package/editors/vscode/icons/redscript-icons.json +10 -0
- package/editors/vscode/out/extension.js +1295 -80
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +10 -3
- package/editors/vscode/src/hover.ts +55 -2
- package/editors/vscode/src/symbols.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +176 -10
- package/src/__tests__/codegen.test.ts +28 -1
- package/src/__tests__/diagnostics.test.ts +5 -5
- package/src/__tests__/e2e.test.ts +335 -17
- package/src/__tests__/fixtures/event-test.mcrs +13 -0
- package/src/__tests__/fixtures/impl-test.mcrs +46 -0
- package/src/__tests__/fixtures/interval-test.mcrs +11 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
- package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
- package/src/__tests__/lexer.test.ts +14 -2
- package/src/__tests__/lowering.test.ts +226 -12
- package/src/__tests__/mc-integration.test.ts +421 -31
- package/src/__tests__/mc-syntax.test.ts +3 -3
- package/src/__tests__/nbt.test.ts +2 -2
- package/src/__tests__/optimizer-advanced.test.ts +5 -5
- package/src/__tests__/parser.test.ts +91 -5
- package/src/__tests__/runtime.test.ts +9 -9
- package/src/__tests__/typechecker.test.ts +171 -0
- package/src/ast/types.ts +44 -3
- package/src/cli.ts +10 -10
- package/src/codegen/mcfunction/index.ts +40 -3
- package/src/codegen/structure/index.ts +35 -1
- package/src/compile.ts +54 -6
- package/src/events/types.ts +69 -0
- package/src/examples/capture_the_flag.mcrs +208 -0
- package/src/examples/{counter.rs → counter.mcrs} +1 -1
- package/src/examples/hunger_games.mcrs +301 -0
- package/src/examples/new_features_demo.mcrs +193 -0
- package/src/examples/parkour_race.mcrs +233 -0
- package/src/examples/rpg.mcrs +13 -0
- package/src/examples/{shop.rs → shop.mcrs} +1 -1
- package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
- package/src/examples/{turret.rs → turret.mcrs} +1 -1
- package/src/examples/zombie_survival.mcrs +314 -0
- package/src/index.ts +4 -3
- package/src/ir/builder.ts +3 -1
- package/src/ir/types.ts +12 -2
- package/src/lexer/index.ts +3 -1
- package/src/lowering/index.ts +684 -24
- package/src/mc-test/runner.ts +3 -3
- package/src/mc-test/setup.ts +2 -2
- package/src/parser/index.ts +170 -19
- package/src/stdlib/README.md +178 -140
- package/src/stdlib/bossbar.mcrs +68 -0
- package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
- package/src/stdlib/effects.mcrs +64 -0
- package/src/stdlib/interactions.mcrs +195 -0
- package/src/stdlib/inventory.mcrs +38 -0
- package/src/stdlib/mobs.mcrs +99 -0
- package/src/stdlib/particles.mcrs +52 -0
- package/src/stdlib/sets.mcrs +20 -0
- package/src/stdlib/spawn.mcrs +41 -0
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/teams.mcrs +68 -0
- package/src/stdlib/timer.mcrs +72 -0
- package/src/stdlib/world.mcrs +92 -0
- package/src/typechecker/index.ts +404 -18
- package/src/examples/rpg.rs +0 -13
- package/src/stdlib/mobs.rs +0 -99
- package/src/stdlib/timer.rs +0 -51
- /package/src/examples/{arena.rs → arena.mcrs} +0 -0
- /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
- /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
- /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
- /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
- /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
- /package/src/stdlib/{math.rs → math.mcrs} +0 -0
- /package/src/stdlib/{player.rs → player.mcrs} +0 -0
- /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
- /package/src/templates/{combat.rs → combat.mcrs} +0 -0
- /package/src/templates/{economy.rs → economy.mcrs} +0 -0
- /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
- /package/src/templates/{quest.rs → quest.mcrs} +0 -0
- /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(
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|