@starktma/minecraft-utils 1.3.25 → 1.4.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 (33) hide show
  1. package/dist/database/database.d.ts +6 -4
  2. package/dist/database/database.d.ts.map +1 -1
  3. package/dist/database/database.js +24 -11
  4. package/dist/database/database.js.map +1 -1
  5. package/dist/game-state-machine/database.d.ts +1 -1
  6. package/dist/game-state-machine/database.d.ts.map +1 -1
  7. package/dist/game-state-machine/database.js +1 -1
  8. package/dist/game-state-machine/database.js.map +1 -1
  9. package/dist/math/index.d.ts +63 -59
  10. package/dist/math/index.d.ts.map +1 -1
  11. package/dist/math/index.js +150 -76
  12. package/dist/math/index.js.map +1 -1
  13. package/dist/minecraft/effectsAPI.d.ts +44 -57
  14. package/dist/minecraft/effectsAPI.d.ts.map +1 -1
  15. package/dist/minecraft/effectsAPI.js +354 -351
  16. package/dist/minecraft/effectsAPI.js.map +1 -1
  17. package/dist/minecraft/index.d.ts +7 -7
  18. package/dist/minecraft/index.d.ts.map +1 -1
  19. package/dist/minecraft/index.js +45 -31
  20. package/dist/minecraft/index.js.map +1 -1
  21. package/dist/minecraft/projectileAPI.d.ts +21 -59
  22. package/dist/minecraft/projectileAPI.d.ts.map +1 -1
  23. package/dist/minecraft/projectileAPI.js +139 -137
  24. package/dist/minecraft/projectileAPI.js.map +1 -1
  25. package/dist/minecraft/structuresAPI.d.ts +10 -0
  26. package/dist/minecraft/structuresAPI.d.ts.map +1 -0
  27. package/dist/minecraft/structuresAPI.js +61 -0
  28. package/dist/minecraft/structuresAPI.js.map +1 -0
  29. package/package.json +5 -1
  30. package/dist/minecraft/potionAPI.d.ts +0 -74
  31. package/dist/minecraft/potionAPI.d.ts.map +0 -1
  32. package/dist/minecraft/potionAPI.js +0 -228
  33. package/dist/minecraft/potionAPI.js.map +0 -1
@@ -1,23 +1,22 @@
1
- import { MolangVariableMap, Player, system, world } from "@minecraft/server";
1
+ import { EntityItemComponent, MolangVariableMap, system, world, } from "@minecraft/server";
2
2
  import { MinecraftItemTypes } from "@minecraft/vanilla-data";
3
- import { SimpleDatabase } from "@starktma/minecraft-utils/database";
4
3
  import { getNamespace } from "../constants";
5
- class EffectEntityDatabase extends SimpleDatabase {
6
- static instance;
7
- constructor() {
8
- super("effect_entities");
9
- }
10
- static getInstance() {
11
- if (!EffectEntityDatabase.instance) {
12
- EffectEntityDatabase.instance = new EffectEntityDatabase();
13
- }
14
- return EffectEntityDatabase.instance;
4
+ import { SimpleDatabase } from "../database";
5
+ class EffectDatabase extends SimpleDatabase {
6
+ constructor(entity) {
7
+ super("effects", entity);
15
8
  }
16
9
  }
10
+ // ============================= EffectManager =================================
17
11
  class EffectManager {
18
- configs = new Map();
19
- entityDB = null;
20
12
  static instance;
13
+ effectConfigs = new Map();
14
+ potionConfigs = new Map();
15
+ areaEffectClouds = new Map();
16
+ cloudIdCounter = 0;
17
+ trackedEntities = new Map();
18
+ // Entity databases: entityId -> EffectDatabase
19
+ entityDatabases = new Map();
21
20
  static getInstance() {
22
21
  if (!EffectManager.instance) {
23
22
  EffectManager.instance = new EffectManager();
@@ -25,119 +24,69 @@ class EffectManager {
25
24
  return EffectManager.instance;
26
25
  }
27
26
  constructor() { }
28
- getEntityDatabase() {
29
- if (!this.entityDB) {
30
- this.entityDB = EffectEntityDatabase.getInstance();
31
- }
32
- return this.entityDB;
33
- }
34
- /**
35
- * Track an entity as having effects
36
- * @param entity - The entity to track
37
- */
38
- trackEntity(entity) {
39
- const db = this.getEntityDatabase();
40
- const tracker = {
41
- id: entity.id,
42
- };
43
- if (!db.hasObject(entity.id)) {
44
- db.addObject(tracker);
27
+ // ============================= Database Helpers ==========================
28
+ // Get or create database for an entity
29
+ getEntityDatabase(entity) {
30
+ let db = this.entityDatabases.get(entity.id);
31
+ if (!db) {
32
+ db = new EffectDatabase(entity);
33
+ this.entityDatabases.set(entity.id, db);
45
34
  }
35
+ return db;
46
36
  }
47
- /**
48
- * Stop tracking an entity (when it has no more effects)
49
- * @param entity - The entity to stop tracking
50
- */
51
- untrackEntity(entity) {
52
- const db = this.getEntityDatabase();
53
- if (db.hasObject(entity.id)) {
54
- db.removeObject(entity.id);
55
- }
37
+ getEffect(entity, effectType) {
38
+ const db = this.getEntityDatabase(entity);
39
+ return db.getObject(effectType) || null;
56
40
  }
57
- /**
58
- * Check if an entity has any effect tags
59
- * @param entity - The entity to check
60
- * @returns True if entity has any effect tags
61
- */
62
- entityHasEffectTags(entity) {
63
- return entity.getTags().some((tag) => tag.includes("starkteffects"));
41
+ getAllEffects(entity) {
42
+ const db = this.getEntityDatabase(entity);
43
+ return db.getAllObjects();
64
44
  }
65
- /**
66
- * Parse effect data from entity tags regardless of namespace
67
- * @param entity - The entity to get effects from
68
- * @returns Array of effect objects parsed from all effect tags
69
- */
70
- getEffectsFromTags(entity) {
71
- return entity
72
- .getTags()
73
- .filter((tag) => tag.includes("starkteffects"))
74
- .map((tag) => {
75
- const parts = tag.split("_");
76
- if (parts.length < 4)
77
- return null;
78
- // Find the namespace and effects parts
79
- const effectsIndex = parts.findIndex((part) => part === "starkteffects");
80
- if (effectsIndex === -1 || effectsIndex === 0 || effectsIndex >= parts.length - 2)
81
- return null;
82
- const namespace = parts.slice(0, effectsIndex).join("_");
83
- const effectType = parts.slice(effectsIndex + 1, -2).join("_");
84
- const amplifier = parseInt(parts[parts.length - 2]);
85
- const duration = parseInt(parts[parts.length - 1]);
86
- const config = this.configs.get(effectType);
87
- return {
88
- id: `${entity.id}_${effectType}`,
89
- entityId: entity.id,
90
- effectType,
91
- amplifier,
92
- duration,
93
- color: config?.color || { red: 1, green: 1, blue: 1, alpha: 1 },
94
- };
95
- })
96
- .filter((effect) => effect !== null);
45
+ // Check if entity has any active effects and should be tracked
46
+ hasActiveEffects(entity) {
47
+ const db = this.getEntityDatabase(entity);
48
+ return db.getAllObjects().length > 0;
97
49
  }
98
- /**
99
- * Parse ALL effect data from entity tags regardless of namespace
100
- * @param entity - The entity to get effects from
101
- * @returns Array of effect objects parsed from all effect tags
102
- */
103
- getAllEffectsFromTags(entity) {
104
- return entity
105
- .getTags()
106
- .filter((tag) => tag.includes("starkteffects"))
107
- .map((tag) => {
108
- const parts = tag.split("_");
109
- if (parts.length < 4)
110
- return null;
111
- // Find the namespace and effects parts
112
- const effectsIndex = parts.findIndex((part) => part === "starkteffects");
113
- if (effectsIndex === -1 || effectsIndex === 0 || effectsIndex >= parts.length - 2)
114
- return null;
115
- const namespace = parts.slice(0, effectsIndex).join("_");
116
- const effectType = parts.slice(effectsIndex + 1, -2).join("_");
117
- const amplifier = parseInt(parts[parts.length - 2]);
118
- const duration = parseInt(parts[parts.length - 1]);
119
- const config = this.configs.get(effectType);
120
- return {
121
- id: `${entity.id}_${effectType}`,
122
- entityId: entity.id,
123
- effectType,
124
- amplifier,
125
- duration,
126
- color: config?.color || { red: 1, green: 1, blue: 1, alpha: 1 },
127
- };
128
- })
129
- .filter((effect) => effect !== null);
50
+ debug() {
51
+ for (const player of world.getAllPlayers()) {
52
+ const effects = this.getAllEffects(player);
53
+ if (effects.length === 0) {
54
+ player.onScreenDisplay.setActionBar("§8[§7Effects§8] §7No active effects");
55
+ continue;
56
+ }
57
+ const effectMap = new Map();
58
+ for (const effect of effects) {
59
+ const existing = effectMap.get(effect.effectType);
60
+ if (!existing || effect.duration > existing.duration) {
61
+ effectMap.set(effect.effectType, effect);
62
+ }
63
+ }
64
+ const sorted = Array.from(effectMap.values()).sort((a, b) => b.duration - a.duration);
65
+ const display = sorted.map((effect) => {
66
+ const seconds = Math.ceil(effect.duration / 20);
67
+ const minutes = Math.floor(seconds / 60);
68
+ const remainingSeconds = seconds % 60;
69
+ const timeString = seconds > 60
70
+ ? `§a${minutes}:${remainingSeconds.toString().padStart(2, "0")}`
71
+ : seconds > 10
72
+ ? `§e${seconds}s`
73
+ : `§c${seconds}s`;
74
+ const name = effect.effectType
75
+ .split("_")
76
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
77
+ .join(" ");
78
+ const amp = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"][effect.amplifier] || effect.amplifier + 1;
79
+ return `§6${name}§r §7${amp}§r §8(${timeString}§8)§r`;
80
+ });
81
+ player.onScreenDisplay.setActionBar(`§8[§bActive Effects§8]§r\n${display.join("\n")}`);
82
+ }
130
83
  }
131
- /**
132
- * Register a effect effect with full configuration
133
- * @param config - Complete effect configuration including effect type, color, sounds, particle type, and handler
134
- * @returns Helper functions for adding, removing, and applying the effect
135
- */
84
+ // ============================= Effect API ================================
136
85
  registerEffect(config) {
137
- this.configs.set(config.effectType, config);
86
+ this.effectConfigs.set(config.effectType, config);
138
87
  return {
139
- addEffect: (entity, amplifier, duration) => {
140
- this.addEffect(entity, config.effectType, amplifier, duration);
88
+ addEffect: (entity, amplifier, durationTicks) => {
89
+ this.addEffect(entity, config.effectType, amplifier, durationTicks);
141
90
  },
142
91
  removeEffect: (entity) => {
143
92
  this.removeEffect(entity, config.effectType);
@@ -146,293 +95,347 @@ class EffectManager {
146
95
  };
147
96
  }
148
97
  addEffect(entity, effectType, amplifier, duration) {
149
- // Get config if registered
150
- const config = this.configs.get(effectType);
151
- const currentNamespace = getNamespace();
152
- // Find existing effect from tags
153
- const existingTag = entity
154
- .getTags()
155
- .find((tag) => tag.startsWith(`${currentNamespace}_starkteffects_${effectType}_`));
156
- if (existingTag) {
157
- // Parse existing effect
158
- const parts = existingTag.split("_");
159
- if (parts.length >= 4) {
160
- const existingAmplifier = parseInt(parts[parts.length - 2]);
161
- const existingDuration = parseInt(parts[parts.length - 1]);
162
- // Update if stronger or longer
163
- if (amplifier > existingAmplifier || (amplifier === existingAmplifier && duration * 20 > existingDuration)) {
164
- // Remove old tag and add new one
165
- entity.removeTag(existingTag);
166
- entity.addTag(`${currentNamespace}_starkteffects_${effectType}_${amplifier}_${duration * 20}`);
167
- }
168
- }
169
- }
170
- else {
171
- // New effect - add tag
172
- entity.addTag(`${currentNamespace}_starkteffects_${effectType}_${amplifier}_${duration * 20}`);
173
- // Play start sound if configured
174
- if (config?.sounds?.start) {
98
+ const config = this.effectConfigs.get(effectType);
99
+ const db = this.getEntityDatabase(entity);
100
+ const existing = db.getObject(effectType);
101
+ // Determine which effect to keep (stronger one)
102
+ if (!existing ||
103
+ amplifier > existing.amplifier ||
104
+ (amplifier === existing.amplifier && duration > existing.duration)) {
105
+ const effect = {
106
+ id: effectType,
107
+ namespace: getNamespace(),
108
+ effectType,
109
+ amplifier,
110
+ duration,
111
+ color: config?.color || { red: 1, green: 1, blue: 1, alpha: 1 },
112
+ };
113
+ db.updateObject(effect);
114
+ // Play start sound only for new effects
115
+ if (!existing && config?.sounds?.start) {
175
116
  try {
176
117
  entity.dimension.playSound(config.sounds.start, entity.location);
177
118
  }
178
- catch (error) { }
119
+ catch { }
179
120
  }
180
121
  }
181
- // Track this entity as having effects
182
- this.trackEntity(entity);
122
+ // Track entity for effect updates
123
+ this.trackedEntities.set(entity.id, entity);
183
124
  }
184
125
  removeEffect(entity, effectType) {
185
- const currentNamespace = getNamespace();
186
- // Find and remove effect tags
187
- const effectTags = entity
188
- .getTags()
189
- .filter((tag) => tag.startsWith(`${currentNamespace}_starkteffects_${effectType}_`));
190
- if (effectTags.length > 0) {
191
- // Play end sound if configured
192
- const config = this.configs.get(effectType);
193
- if (config?.sounds?.end) {
194
- try {
195
- entity.dimension.playSound(config.sounds.end, entity.location);
196
- }
197
- catch (error) { }
198
- }
199
- // Remove effect tags
200
- effectTags.forEach((tag) => {
201
- entity.removeTag(tag);
202
- });
203
- // Check if entity still has any effects, untrack if not
204
- if (!this.entityHasEffectTags(entity)) {
205
- this.untrackEntity(entity);
126
+ const db = this.getEntityDatabase(entity);
127
+ const hasEffect = db.hasObject(effectType);
128
+ const config = this.effectConfigs.get(effectType);
129
+ // Remove from database
130
+ db.removeObject(effectType);
131
+ // Play end sound only if effect existed
132
+ if (hasEffect && config?.sounds?.end) {
133
+ try {
134
+ entity.dimension.playSound(config.sounds.end, entity.location);
206
135
  }
136
+ catch { }
137
+ }
138
+ // Untrack entity if no effects remain
139
+ if (!this.hasActiveEffects(entity)) {
140
+ this.entityDatabases.delete(entity.id);
141
+ this.trackedEntities.delete(entity.id);
207
142
  }
208
143
  }
209
144
  removeAllEffects(entity) {
210
- const currentNamespace = getNamespace();
211
- // Get all effect tags
212
- const effectTags = entity.getTags().filter((tag) => tag.startsWith(`${currentNamespace}_starkteffects_`));
213
- // Play end sounds for each effect type
214
- const effectTypes = new Set();
215
- effectTags.forEach((tag) => {
216
- const parts = tag.split("_");
217
- if (parts.length >= 4) {
218
- const effectType = parts.slice(2, -2).join("_");
219
- effectTypes.add(effectType);
220
- }
221
- });
222
- effectTypes.forEach((effectType) => {
223
- const config = this.configs.get(effectType);
145
+ const db = this.getEntityDatabase(entity);
146
+ const effects = db.getAllObjects();
147
+ entity
148
+ .getTags()
149
+ .filter((tag) => tag.startsWith("starkeffects|pause_effect"))
150
+ .forEach((tag) => entity.removeTag(tag));
151
+ // Play end sounds for each effect
152
+ for (const effect of effects) {
153
+ const config = this.effectConfigs.get(effect.effectType);
224
154
  if (config?.sounds?.end) {
225
155
  try {
226
156
  entity.dimension.playSound(config.sounds.end, entity.location);
227
157
  }
228
- catch (error) { }
158
+ catch { }
229
159
  }
230
- });
231
- // Remove all effect tags
232
- effectTags.forEach((tag) => {
233
- entity.removeTag(tag);
234
- });
235
- // Untrack this entity since it has no more effects
236
- this.untrackEntity(entity);
237
- }
238
- extendEffect(entity, effectType, additionalDuration) {
239
- // Debug: Show all tags on the entity
240
- const allTags = entity.getTags();
241
- // Find effect tags of any namespace
242
- const effectTags = entity.getTags().filter((tag) => {
243
- // Tag format: {namespace}_starkteffects_{effectType}_{amplifier}_{duration}
244
- // We need to find the "_starkteffects_" marker and work from there
245
- const effectsIndex = tag.indexOf("_starkteffects_");
246
- if (effectsIndex === -1)
247
- return false;
248
- // Extract parts after "_starkteffects_"
249
- const afterEffects = tag.substring(effectsIndex + 15); // 15 = "_starkteffects_".length
250
- const parts = afterEffects.split("_");
251
- // Need at least 3 parts: effectType, amplifier, duration
252
- // But effectType might contain underscores, so amplifier and duration are the last 2 parts
253
- if (parts.length < 3)
254
- return false;
255
- // Extract effect type (everything except the last 2 parts which are amplifier and duration)
256
- const tagEffectType = parts.slice(0, -2).join("_");
257
- return tagEffectType === effectType;
258
- });
259
- if (effectTags.length === 0)
260
- return false;
261
- // For each matching effect tag, extend its duration
262
- effectTags.forEach((tag) => {
263
- // Tag format: {namespace}_starkteffects_{effectType}_{amplifier}_{duration}
264
- const effectsIndex = tag.indexOf("_starkteffects_");
265
- const namespace = tag.substring(0, effectsIndex);
266
- const afterEffects = tag.substring(effectsIndex + 15); // 15 = "_starkteffects_".length
267
- const parts = afterEffects.split("_");
268
- // Last two parts are always amplifier and duration
269
- const amplifier = parseInt(parts[parts.length - 2]);
270
- const currentDuration = parseInt(parts[parts.length - 1]);
271
- const newDuration = currentDuration + additionalDuration;
272
- // Remove old tag and add new one with extended duration
273
- entity.removeTag(tag);
274
- const newTag = `${namespace}_starkteffects_${effectType}_${amplifier}_${newDuration}`;
275
- entity.addTag(newTag);
276
- });
277
- return true;
160
+ }
161
+ // Clear database and untrack
162
+ db.eraseAllObjects();
163
+ this.entityDatabases.delete(entity.id);
164
+ this.trackedEntities.delete(entity.id);
278
165
  }
279
166
  hasEffect(entity, effectType) {
280
- const currentNamespace = getNamespace();
281
- return entity.getTags().some((tag) => tag.startsWith(`${currentNamespace}_starkteffects_${effectType}_`));
167
+ const db = this.getEntityDatabase(entity);
168
+ return db.hasObject(effectType);
282
169
  }
283
170
  getEffects(entity) {
284
- return this.getAllEffectsFromTags(entity);
171
+ const db = this.getEntityDatabase(entity);
172
+ return db.getAllObjects();
285
173
  }
286
- /**
287
- * Debug function to display all active effects for an entity in the action bar
288
- * @param entity - The entity to display effects for (must be a Player)
289
- */
290
- debugShowEffects(entity) {
291
- // Only works for players
292
- if (!(entity instanceof Player)) {
293
- return;
294
- }
295
- const currentNamespace = getNamespace();
296
- // Get effects from tags instead of database
297
- const effectTags = entity.getTags().filter((tag) => tag.includes(`starkteffects`));
298
- if (effectTags.length === 0) {
299
- entity.onScreenDisplay.setActionBar("§8[§7Effects§8] §7No active effects");
300
- return;
301
- }
302
- // Parse effect data from tags
303
- const effects = effectTags
304
- .map((tag) => {
305
- // Format: namespace_starkteffects_${effectType}_${amplifier}_${duration}
306
- const parts = tag.split("_");
307
- if (parts.length < 4)
308
- return null;
309
- const effectType = parts.slice(2, -2).join("_"); // Handle effect types with underscores
310
- const amplifier = parseInt(parts[parts.length - 2]);
311
- const duration = parseInt(parts[parts.length - 1]);
312
- return { effectType, amplifier, duration };
313
- })
314
- .filter((effect) => effect !== null);
315
- if (effects.length === 0) {
316
- entity.onScreenDisplay.setActionBar("§8[§7Effects§8] §7No active effects");
317
- return;
318
- }
319
- // Sort effects by remaining duration (longest first)
320
- const sortedEffects = effects.sort((a, b) => b.duration - a.duration);
321
- const effectStrings = sortedEffects.map((effect) => {
322
- const seconds = Math.ceil(effect.duration / 20);
323
- const minutes = Math.floor(seconds / 60);
324
- const remainingSeconds = seconds % 60;
325
- // Format time with different colors based on urgency
326
- let timeString;
327
- let timeColor;
328
- if (seconds > 60) {
329
- timeString = `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
330
- timeColor = "§a"; // Green for long duration
331
- }
332
- else if (seconds > 10) {
333
- timeString = `${seconds}s`;
334
- timeColor = "§e"; // Yellow for medium duration
335
- }
336
- else {
337
- timeString = `${seconds}s`;
338
- timeColor = "§c"; // Red for short duration
339
- }
340
- // Format effect name with proper capitalization
341
- const effectName = effect.effectType.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1))[1];
342
- // Format amplifier with Roman numerals for style
343
- const romanNumerals = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"];
344
- const amplifierDisplay = effect.amplifier < romanNumerals.length ? romanNumerals[effect.amplifier] : (effect.amplifier + 1).toString();
345
- return `§6${effectName}§r §7${amplifierDisplay}§r §8(${timeColor}${timeString}§8)§r`;
346
- });
347
- // Create a formatted display with header and effects
348
- const header = "§8[§bActive Effects§8]§r";
349
- const effectsDisplay = effectStrings.join("\n");
350
- const displayText = `${header}\n${effectsDisplay}`;
351
- entity.onScreenDisplay.setActionBar(displayText);
174
+ // ============================= Potion API ================================
175
+ registerPotion(config) {
176
+ const fullConfig = {
177
+ splashRange: 4,
178
+ lingeringMaxRadius: 3,
179
+ lingeringLifetime: 600,
180
+ lingeringDurationMultiplier: 0.25,
181
+ ...config,
182
+ };
183
+ this.potionConfigs.set(`${config.namespace}:${config.effectKey}`, fullConfig);
184
+ return { handler: config.handler };
352
185
  }
353
- start() {
354
- if (!EffectManager.instance)
355
- return;
186
+ // ============================= System ====================================
187
+ start(startup, debug) {
188
+ if (startup) {
189
+ for (const [, config] of this.potionConfigs.entries()) {
190
+ const componentId = `${config.namespace}:${config.effectKey}_potion_effect`;
191
+ startup.itemComponentRegistry.registerCustomComponent(componentId, {
192
+ onConsume: (event, params) => {
193
+ const p = params.params;
194
+ const color = {
195
+ red: p.potion_color[0],
196
+ green: p.potion_color[1],
197
+ blue: p.potion_color[2],
198
+ alpha: p.potion_color[3],
199
+ };
200
+ config.handler(event.source, p.amplifier, p.duration * 20, color);
201
+ },
202
+ });
203
+ }
204
+ world.afterEvents.projectileHitBlock.subscribe((event) => this.handleProjectileHit(event.projectile));
205
+ world.afterEvents.projectileHitEntity.subscribe((event) => this.handleProjectileHit(event.projectile));
206
+ }
356
207
  system.runInterval(() => {
357
- const entityDB = this.getEntityDatabase();
358
- const trackedEntityIds = entityDB.getAllObjects();
359
- for (const tracker of trackedEntityIds) {
360
- const entity = world.getEntity(tracker.id);
361
- // If entity doesn't exist, clean up tracking
362
- if (!entity)
208
+ //if (debug) {
209
+ // this.debug();
210
+ //}
211
+ // Process effects on tracked entities only
212
+ for (const [entityId, entity] of this.trackedEntities.entries()) {
213
+ if (!entity.isValid) {
214
+ this.entityDatabases.delete(entityId);
215
+ this.trackedEntities.delete(entityId);
363
216
  continue;
364
- // Check if entity still has effect tags (cleanup orphaned tracking)
365
- if (!this.entityHasEffectTags(entity)) {
366
- entityDB.removeObject(tracker.id);
217
+ }
218
+ const allTags = entity.getTags();
219
+ const pauseEffects = allTags.filter((tag) => tag.startsWith(`starkeffects|pause_effect`));
220
+ const db = this.getEntityDatabase(entity);
221
+ const effects = db.getAllObjects();
222
+ if (effects.length === 0) {
223
+ this.entityDatabases.delete(entityId);
224
+ this.trackedEntities.delete(entityId);
367
225
  continue;
368
226
  }
369
- this.debugShowEffects(entity);
370
- const effects = this.getEffectsFromTags(entity).filter((effect) => {
371
- // Only process effects from current namespace
372
- const currentNamespace = getNamespace();
373
- return entity
374
- .getTags()
375
- .some((tag) => tag.startsWith(`${currentNamespace}_starkteffects_${effect.effectType}_`));
376
- });
227
+ // Store original durations to detect handler modifications
228
+ const originalDurations = new Map();
229
+ for (const effect of effects) {
230
+ originalDurations.set(effect.effectType, effect.duration);
231
+ }
232
+ // Apply handlers and update effects
377
233
  for (const effect of effects) {
378
- // Get config for this effect
379
- const config = this.configs.get(effect.effectType);
380
- // Apply effect handler
234
+ const config = this.effectConfigs.get(effect.effectType);
235
+ // Run effect handler
381
236
  if (config?.handler) {
382
237
  config.handler(entity, effect);
383
238
  }
384
- // Play ambient sound periodically (every 2 seconds)
239
+ // Get current effect from database (handler may have modified it)
240
+ const currentEffect = db.getObject(effect.effectType);
241
+ if (!currentEffect)
242
+ continue;
243
+ // Check if handler modified the duration
244
+ const originalDuration = originalDurations.get(effect.effectType);
245
+ if (currentEffect.duration !== originalDuration) {
246
+ // Handler modified it, skip countdown
247
+ continue;
248
+ }
249
+ // Play ambient sounds
385
250
  if (config?.sounds?.ambient && system.currentTick % 40 === 0) {
386
251
  try {
387
252
  entity.dimension.playSound(config.sounds.ambient, entity.location, { volume: 0.5 });
388
253
  }
389
- catch (error) { }
254
+ catch { }
390
255
  }
391
256
  // Spawn particles
392
- if (system.currentTick % 2 === 0) {
393
- const particleLocation = entity.location;
394
- particleLocation.y++;
257
+ if (system.currentTick % 20 === 0) {
258
+ const loc = { ...entity.location, y: entity.location.y + 1 };
395
259
  const variables = new MolangVariableMap();
396
- variables.setColorRGBA("color", effect.color);
397
- const particleType = config?.particleType || "minecraft:mobspell_emitter";
260
+ variables.setColorRGBA("color", currentEffect.color);
398
261
  try {
399
- entity.dimension.spawnParticle(particleType, particleLocation, variables);
262
+ entity.dimension.spawnParticle(config?.particleType || "minecraft:mobspell_emitter", loc, variables);
400
263
  }
401
- catch (error) { }
264
+ catch { }
402
265
  }
403
- // Update duration
404
- const newDuration = effect.duration - 1;
405
- if (newDuration <= 0) {
406
- // Play end sound when effect expires naturally
266
+ if (pauseEffects.length > 0) {
267
+ const excludeEffects = pauseEffects.map((tag) => tag.split("|")[2]);
268
+ if (!excludeEffects.includes(effect.effectType))
269
+ continue;
270
+ }
271
+ // Countdown duration
272
+ const newDuration = currentEffect.duration - 1;
273
+ if (newDuration > 0) {
274
+ db.updateObject({ ...currentEffect, duration: newDuration });
275
+ }
276
+ else {
277
+ db.removeObject(effect.effectType);
407
278
  if (config?.sounds?.end) {
408
279
  try {
409
280
  entity.dimension.playSound(config.sounds.end, entity.location);
410
281
  }
411
- catch (error) { }
282
+ catch { }
412
283
  }
413
- // Remove expired effect tag
414
- const currentNamespace = getNamespace();
415
- entity.removeTag(`${currentNamespace}_starkteffects_${effect.effectType}_${effect.amplifier}_${effect.duration}`);
416
- }
417
- else {
418
- // Update tag with new duration
419
- const currentNamespace = getNamespace();
420
- entity.removeTag(`${currentNamespace}_starkteffects_${effect.effectType}_${effect.amplifier}_${effect.duration}`);
421
- entity.addTag(`${currentNamespace}_starkteffects_${effect.effectType}_${effect.amplifier}_${newDuration}`);
422
284
  }
423
285
  }
424
- // Clean up tracking if entity has no more effects
425
- if (!this.entityHasEffectTags(entity)) {
426
- entityDB.removeObject(tracker.id);
286
+ // Untrack if no effects remain
287
+ if (!this.hasActiveEffects(entity)) {
288
+ this.entityDatabases.delete(entityId);
289
+ this.trackedEntities.delete(entityId);
427
290
  }
428
291
  }
292
+ for (const cloud of this.areaEffectClouds.values()) {
293
+ this.updateCloud(cloud);
294
+ }
295
+ }, 1);
296
+ // Track entities that spawn with active effects
297
+ world.afterEvents.entitySpawn.subscribe((event) => {
298
+ if (!event.entity || !event.entity.isValid)
299
+ return;
300
+ if (event.entity.hasComponent(EntityItemComponent.componentId))
301
+ return;
302
+ if (this.hasActiveEffects(event.entity)) {
303
+ this.trackedEntities.set(event.entity.id, event.entity);
304
+ }
305
+ });
306
+ world.afterEvents.playerJoin.subscribe((event) => {
307
+ const player = world.getEntity(event.playerId);
308
+ if (this.hasActiveEffects(player)) {
309
+ this.trackedEntities.set(player.id, player);
310
+ }
311
+ });
312
+ // Clean up when entities die
313
+ world.afterEvents.entityDie.subscribe((event) => {
314
+ this.entityDatabases.delete(event.deadEntity.id);
315
+ this.trackedEntities.delete(event.deadEntity.id);
429
316
  });
430
317
  world.afterEvents.itemCompleteUse.subscribe((event) => {
431
318
  if (event.itemStack.typeId === MinecraftItemTypes.MilkBucket) {
432
319
  this.removeAllEffects(event.source);
433
320
  }
434
321
  });
322
+ world.afterEvents.worldLoad.subscribe(() => {
323
+ world.getAllPlayers().forEach((player) => {
324
+ if (this.hasActiveEffects(player)) {
325
+ this.trackedEntities.set(player.id, player);
326
+ }
327
+ });
328
+ });
329
+ }
330
+ // ============================= Potion Projectiles ========================
331
+ handleProjectileHit(projectile) {
332
+ const configs = Array.from(this.potionConfigs.values());
333
+ for (const config of configs) {
334
+ if (projectile.typeId !== `${config.namespace}:potion_projectile`)
335
+ continue;
336
+ const effectId = projectile.getProperty(`${config.namespace}:effect_id`);
337
+ if (effectId !== config.effectId)
338
+ continue;
339
+ const potionType = projectile.getProperty(`${config.namespace}:type`);
340
+ if (potionType === 1) {
341
+ this.handleSplash(projectile, config);
342
+ }
343
+ else if (potionType === 2) {
344
+ this.handleLingering(projectile, config);
345
+ }
346
+ else {
347
+ projectile.remove();
348
+ }
349
+ return;
350
+ }
351
+ }
352
+ handleSplash(projectile, config) {
353
+ const location = { ...projectile.location, y: projectile.location.y + 0.1 };
354
+ const color = this.getColorFromEntity(projectile, config.namespace);
355
+ const amplifier = projectile.getProperty(`${config.namespace}:amplifier`);
356
+ const duration = projectile.getProperty(`${config.namespace}:duration`) * 20;
357
+ const variables = new MolangVariableMap();
358
+ variables.setFloat("splash_range", config.splashRange);
359
+ variables.setFloat("splash_power", 1);
360
+ variables.setColorRGBA("color", color);
361
+ projectile.dimension.spawnParticle("minecraft:splash_spell_emitter", location, variables);
362
+ const nearbyEntities = projectile.dimension.getEntities({
363
+ excludeTypes: ["minecraft:item", "minecraft:arrow"],
364
+ location: projectile.location,
365
+ maxDistance: config.splashRange,
366
+ });
367
+ for (const entity of nearbyEntities) {
368
+ if (entity.id !== projectile.id) {
369
+ config.handler(entity, amplifier, duration, color);
370
+ }
371
+ }
372
+ projectile.remove();
373
+ }
374
+ handleLingering(projectile, config) {
375
+ const location = { ...projectile.location, y: projectile.location.y + 0.1 };
376
+ const color = this.getColorFromEntity(projectile, config.namespace);
377
+ const amplifier = projectile.getProperty(`${config.namespace}:amplifier`);
378
+ const duration = projectile.getProperty(`${config.namespace}:duration`) * 20;
379
+ const cloud = {
380
+ id: `aoe_cloud_${this.cloudIdCounter++}`,
381
+ dimension: projectile.dimension.id,
382
+ location,
383
+ color,
384
+ amplifier,
385
+ duration,
386
+ currentRadius: config.lingeringMaxRadius,
387
+ maxRadius: config.lingeringMaxRadius,
388
+ createdTick: system.currentTick,
389
+ maxLifetime: config.lingeringLifetime,
390
+ affectedEntities: new Set(),
391
+ handler: config.handler,
392
+ };
393
+ this.areaEffectClouds.set(cloud.id, cloud);
394
+ projectile.remove();
395
+ }
396
+ updateCloud(cloud) {
397
+ const dimension = world.getDimension(cloud.dimension);
398
+ const age = system.currentTick - cloud.createdTick;
399
+ const ageProgress = Math.min(age / cloud.maxLifetime, 1);
400
+ cloud.currentRadius = Math.max(0, cloud.maxRadius * (1 - ageProgress));
401
+ if (age >= cloud.maxLifetime || cloud.currentRadius <= 0) {
402
+ this.areaEffectClouds.delete(cloud.id);
403
+ return;
404
+ }
405
+ const variables = new MolangVariableMap();
406
+ variables.setFloat("cloud_lifetime", cloud.maxLifetime / 20);
407
+ variables.setFloat("cloud_radius", cloud.currentRadius);
408
+ variables.setFloat("particle_multiplier", cloud.currentRadius / cloud.maxRadius);
409
+ variables.setColorRGBA("color", cloud.color);
410
+ dimension.spawnParticle("minecraft:mobspell_lingering", cloud.location, variables);
411
+ if (age < 20)
412
+ return;
413
+ const nearbyEntities = dimension.getEntities({
414
+ location: cloud.location,
415
+ maxDistance: cloud.currentRadius + 1,
416
+ excludeTypes: ["minecraft:item", "minecraft:arrow", "minecraft:xp_orb"],
417
+ });
418
+ for (const entity of nearbyEntities) {
419
+ if (cloud.affectedEntities.has(entity.id))
420
+ continue;
421
+ cloud.handler(entity, cloud.amplifier, Math.floor(cloud.duration), cloud.color);
422
+ cloud.currentRadius = Math.max(0, cloud.currentRadius - 0.5);
423
+ cloud.maxLifetime = Math.max(0, cloud.maxLifetime - 100);
424
+ cloud.affectedEntities.add(entity.id);
425
+ system.runTimeout(() => {
426
+ cloud.affectedEntities.delete(entity.id);
427
+ }, 60);
428
+ }
429
+ }
430
+ getColorFromEntity(entity, namespace) {
431
+ return {
432
+ red: entity.getProperty(`${namespace}:color_r`),
433
+ green: entity.getProperty(`${namespace}:color_g`),
434
+ blue: entity.getProperty(`${namespace}:color_b`),
435
+ alpha: entity.getProperty(`${namespace}:color_a`),
436
+ };
435
437
  }
436
438
  }
437
- export { EffectManager };
439
+ const effectManager = EffectManager.getInstance();
440
+ export { EffectManager, effectManager };
438
441
  //# sourceMappingURL=effectsAPI.js.map