minecraft-inventory 0.1.28 → 0.1.29

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-inventory",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "release": {
Binary file
Binary file
Binary file
Binary file
@@ -326,7 +326,7 @@ export function InventoryOverlay({
326
326
  lineHeight: 1,
327
327
  }}
328
328
  >
329
- INV 0.1.28
329
+ INV 0.1.29
330
330
  </a>
331
331
  )}
332
332
 
@@ -0,0 +1,350 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import { useInventoryContext } from '../../context/InventoryContext'
3
+ import { useTextures } from '../../context/TextureContext'
4
+ import { useScale } from '../../context/ScaleContext'
5
+
6
+ // Bundled payment item textures (not available via texture provider in the full client)
7
+ import netherite_ingot_png from '../../assets/beacon/netherite_ingot.png'
8
+ import emerald_png from '../../assets/beacon/emerald.png'
9
+ import diamond_png from '../../assets/beacon/diamond.png'
10
+ import gold_ingot_png from '../../assets/beacon/gold_ingot.png'
11
+ import iron_ingot_png from '../../assets/beacon/iron_ingot.png'
12
+
13
+ const SPRITE_BASE = '1.21.11/textures/gui/sprites/container/beacon'
14
+ const MOB_EFFECT_BASE = '1.21.11/textures/mob_effect'
15
+
16
+ // Effect IDs matching vanilla BeaconMenu.encodeEffect: registry_id + 1
17
+ // ContainerData: ≤0 = none, 1+ = effect (1-based)
18
+ // Protocol set_beacon_effect option(varint) uses the same 1-based encoding
19
+ const EFFECTS = {
20
+ NONE: -1,
21
+ SPEED: 1, // registry 0 + 1
22
+ HASTE: 3, // registry 2 + 1
23
+ STRENGTH: 5, // registry 4 + 1
24
+ JUMP_BOOST: 8, // registry 7 + 1
25
+ REGENERATION: 10, // registry 9 + 1
26
+ RESISTANCE: 11, // registry 10 + 1
27
+ } as const
28
+
29
+ const EFFECT_INFO: Record<number, { name: string; sprite: string }> = {
30
+ [EFFECTS.SPEED]: { name: 'Speed', sprite: 'speed' },
31
+ [EFFECTS.HASTE]: { name: 'Haste', sprite: 'haste' },
32
+ [EFFECTS.RESISTANCE]: { name: 'Resistance', sprite: 'resistance' },
33
+ [EFFECTS.JUMP_BOOST]: { name: 'Jump Boost', sprite: 'jump_boost' },
34
+ [EFFECTS.STRENGTH]: { name: 'Strength', sprite: 'strength' },
35
+ [EFFECTS.REGENERATION]: { name: 'Regeneration', sprite: 'regeneration' },
36
+ }
37
+
38
+ // Primary buttons: 3 tiers of effects
39
+ const PRIMARY_TIERS: { tier: number; y: number; effects: number[] }[] = [
40
+ { tier: 0, y: 22, effects: [EFFECTS.SPEED, EFFECTS.HASTE] },
41
+ { tier: 1, y: 47, effects: [EFFECTS.RESISTANCE, EFFECTS.JUMP_BOOST] },
42
+ { tier: 2, y: 72, effects: [EFFECTS.STRENGTH] },
43
+ ]
44
+
45
+ const PRIMARY_CENTER_X = 76
46
+
47
+ // Payment indicator items shown next to the payment slot (vanilla renderBg)
48
+ const PAYMENT_ITEMS: { src: string; x: number }[] = [
49
+ { src: netherite_ingot_png, x: 20 },
50
+ { src: emerald_png, x: 41 },
51
+ { src: diamond_png, x: 63 },
52
+ { src: gold_ingot_png, x: 86 },
53
+ { src: iron_ingot_png, x: 108 },
54
+ ]
55
+
56
+ interface BeaconEffectsProps {
57
+ properties: Record<string, number>
58
+ }
59
+
60
+ export function BeaconEffects({ properties }: BeaconEffectsProps) {
61
+ const { sendAction, getSlot } = useInventoryContext()
62
+ const textures = useTextures()
63
+ const { scale } = useScale()
64
+
65
+ const levels = properties.levels ?? 0
66
+ // ContainerData: ≤0 means "no effect" (server sends -1 for uninitialized, vanilla sends 0)
67
+ const serverPrimary = (properties.primaryEffect ?? -1) > 0 ? properties.primaryEffect! : EFFECTS.NONE
68
+ const serverSecondary = (properties.secondaryEffect ?? -1) > 0 ? properties.secondaryEffect! : EFFECTS.NONE
69
+
70
+ const [selectedPrimary, setSelectedPrimary] = useState(serverPrimary)
71
+ const [selectedSecondary, setSelectedSecondary] = useState(serverSecondary)
72
+ const [hoveredButton, setHoveredButton] = useState<string | null>(null)
73
+
74
+ // Sync local state when server properties change
75
+ useEffect(() => {
76
+ setSelectedPrimary(serverPrimary)
77
+ setSelectedSecondary(serverSecondary)
78
+ }, [serverPrimary, serverSecondary])
79
+
80
+ // Payment slot is index 0 in the beacon container
81
+ const paymentSlot = getSlot(0)
82
+ const hasPayment = paymentSlot?.item != null
83
+
84
+ // Confirm requires payment AND a primary selection (valid effects are > 0)
85
+ const canConfirm = hasPayment && selectedPrimary > 0
86
+
87
+ // Upgrade button mirrors the LOCAL selected primary (vanilla: BeaconScreen.this.primary)
88
+ const showUpgrade = selectedPrimary > 0
89
+
90
+ const getButtonSprite = (disabled: boolean, selected: boolean, hovered: boolean) => {
91
+ if (disabled) return textures.getGuiTextureUrl(`${SPRITE_BASE}/button_disabled.png`)
92
+ if (selected) return textures.getGuiTextureUrl(`${SPRITE_BASE}/button_selected.png`)
93
+ if (hovered) return textures.getGuiTextureUrl(`${SPRITE_BASE}/button_highlighted.png`)
94
+ return textures.getGuiTextureUrl(`${SPRITE_BASE}/button.png`)
95
+ }
96
+
97
+ const getEffectSprite = (effect: number) => {
98
+ const info = EFFECT_INFO[effect]
99
+ if (!info) return undefined
100
+ return textures.getGuiTextureUrl(`${MOB_EFFECT_BASE}/${info.sprite}.png`)
101
+ }
102
+
103
+ const handleConfirm = () => {
104
+ if (!canConfirm) return
105
+ sendAction({
106
+ type: 'beacon',
107
+ primaryEffect: selectedPrimary,
108
+ secondaryEffect: selectedSecondary,
109
+ })
110
+ sendAction({ type: 'close' })
111
+ }
112
+
113
+ const handleCancel = () => {
114
+ sendAction({ type: 'close' })
115
+ }
116
+
117
+ // Center buttons around a given X coordinate
118
+ const getButtonX = (centerX: number, index: number, count: number) => {
119
+ const totalWidth = count * 22 + (count - 1) * 2
120
+ return centerX + index * 24 - totalWidth / 2
121
+ }
122
+
123
+ const renderEffectButton = (
124
+ effect: number,
125
+ x: number,
126
+ y: number,
127
+ disabled: boolean,
128
+ isSelected: boolean,
129
+ isPrimary: boolean,
130
+ key: string,
131
+ tooltip?: string,
132
+ ) => {
133
+ const hovered = hoveredButton === key && !disabled
134
+ const info = EFFECT_INFO[effect]
135
+ const title = tooltip ?? info?.name
136
+
137
+ return (
138
+ <div
139
+ key={key}
140
+ onClick={() => {
141
+ if (disabled) return
142
+ if (isPrimary) {
143
+ setSelectedPrimary(effect)
144
+ } else {
145
+ setSelectedSecondary(effect)
146
+ }
147
+ }}
148
+ onMouseEnter={() => setHoveredButton(key)}
149
+ onMouseLeave={() => setHoveredButton(null)}
150
+ title={title}
151
+ style={{
152
+ position: 'absolute',
153
+ left: x * scale,
154
+ top: y * scale,
155
+ width: 22 * scale,
156
+ height: 22 * scale,
157
+ cursor: disabled ? 'default' : 'pointer',
158
+ }}
159
+ >
160
+ <img
161
+ src={getButtonSprite(disabled, isSelected, hovered)}
162
+ alt=""
163
+ draggable={false}
164
+ style={{
165
+ position: 'absolute',
166
+ left: 0,
167
+ top: 0,
168
+ width: 22 * scale,
169
+ height: 22 * scale,
170
+ imageRendering: 'pixelated',
171
+ }}
172
+ />
173
+ {getEffectSprite(effect) && (
174
+ <img
175
+ src={getEffectSprite(effect)}
176
+ alt=""
177
+ draggable={false}
178
+ style={{
179
+ position: 'absolute',
180
+ left: 2 * scale,
181
+ top: 2 * scale,
182
+ width: 18 * scale,
183
+ height: 18 * scale,
184
+ imageRendering: 'pixelated',
185
+ }}
186
+ />
187
+ )}
188
+ </div>
189
+ )
190
+ }
191
+
192
+ const labelStyle = (x: number): React.CSSProperties => ({
193
+ position: 'absolute',
194
+ left: x * scale,
195
+ top: 10 * scale,
196
+ fontSize: 8 * scale,
197
+ fontFamily: "'Minecraft', monospace",
198
+ color: '#E0E0E0',
199
+ textShadow: 'none',
200
+ transform: 'translateX(-50%)',
201
+ whiteSpace: 'nowrap',
202
+ pointerEvents: 'none',
203
+ })
204
+
205
+ return (
206
+ <>
207
+ {/* Labels */}
208
+ <span style={labelStyle(62)}>Primary Power</span>
209
+ <span style={labelStyle(169)}>Secondary Power</span>
210
+
211
+ {/* Payment item indicators (decorative icons showing valid payment items) */}
212
+ {PAYMENT_ITEMS.map(({ src, x }) => (
213
+ <img
214
+ key={`pay-${x}`}
215
+ src={src}
216
+ alt=""
217
+ draggable={false}
218
+ style={{
219
+ position: 'absolute',
220
+ left: x * scale,
221
+ top: 109 * scale,
222
+ width: 16 * scale,
223
+ height: 16 * scale,
224
+ imageRendering: 'pixelated',
225
+ pointerEvents: 'none',
226
+ }}
227
+ />
228
+ ))}
229
+
230
+ {/* Primary effect buttons (3 tiers) */}
231
+ {PRIMARY_TIERS.map(({ tier, y, effects }) =>
232
+ effects.map((effect, idx) => {
233
+ const x = getButtonX(PRIMARY_CENTER_X, idx, effects.length)
234
+ const disabled = tier >= levels
235
+ const isSelected = selectedPrimary === effect
236
+ return renderEffectButton(effect, x, y, disabled, isSelected, true, `primary-${effect}`)
237
+ })
238
+ )}
239
+
240
+ {/* Secondary: Regeneration button (fixed position x=144) */}
241
+ {renderEffectButton(
242
+ EFFECTS.REGENERATION,
243
+ 144,
244
+ 47,
245
+ 3 >= levels,
246
+ selectedSecondary === EFFECTS.REGENERATION,
247
+ false,
248
+ 'secondary-regen',
249
+ )}
250
+
251
+ {/* Secondary: Upgrade button (fixed position x=168, mirrors local primary selection) */}
252
+ {showUpgrade && renderEffectButton(
253
+ selectedPrimary,
254
+ 168,
255
+ 47,
256
+ 3 >= levels,
257
+ selectedSecondary === selectedPrimary,
258
+ false,
259
+ 'secondary-upgrade',
260
+ `${EFFECT_INFO[selectedPrimary]?.name ?? 'Effect'} II`,
261
+ )}
262
+
263
+ {/* Confirm button */}
264
+ <div
265
+ onClick={handleConfirm}
266
+ onMouseEnter={() => setHoveredButton('confirm')}
267
+ onMouseLeave={() => setHoveredButton(null)}
268
+ title="Done"
269
+ style={{
270
+ position: 'absolute',
271
+ left: 164 * scale,
272
+ top: 107 * scale,
273
+ width: 22 * scale,
274
+ height: 22 * scale,
275
+ cursor: canConfirm ? 'pointer' : 'default',
276
+ }}
277
+ >
278
+ <img
279
+ src={getButtonSprite(!canConfirm, false, hoveredButton === 'confirm' && canConfirm)}
280
+ alt=""
281
+ draggable={false}
282
+ style={{
283
+ position: 'absolute',
284
+ left: 0,
285
+ top: 0,
286
+ width: 22 * scale,
287
+ height: 22 * scale,
288
+ imageRendering: 'pixelated',
289
+ }}
290
+ />
291
+ <img
292
+ src={textures.getGuiTextureUrl(`${SPRITE_BASE}/confirm.png`)}
293
+ alt=""
294
+ draggable={false}
295
+ style={{
296
+ position: 'absolute',
297
+ left: 2 * scale,
298
+ top: 2 * scale,
299
+ width: 18 * scale,
300
+ height: 18 * scale,
301
+ imageRendering: 'pixelated',
302
+ }}
303
+ />
304
+ </div>
305
+
306
+ {/* Cancel button */}
307
+ <div
308
+ onClick={handleCancel}
309
+ onMouseEnter={() => setHoveredButton('cancel')}
310
+ onMouseLeave={() => setHoveredButton(null)}
311
+ title="Cancel"
312
+ style={{
313
+ position: 'absolute',
314
+ left: 190 * scale,
315
+ top: 107 * scale,
316
+ width: 22 * scale,
317
+ height: 22 * scale,
318
+ cursor: 'pointer',
319
+ }}
320
+ >
321
+ <img
322
+ src={getButtonSprite(false, false, hoveredButton === 'cancel')}
323
+ alt=""
324
+ draggable={false}
325
+ style={{
326
+ position: 'absolute',
327
+ left: 0,
328
+ top: 0,
329
+ width: 22 * scale,
330
+ height: 22 * scale,
331
+ imageRendering: 'pixelated',
332
+ }}
333
+ />
334
+ <img
335
+ src={textures.getGuiTextureUrl(`${SPRITE_BASE}/cancel.png`)}
336
+ alt=""
337
+ draggable={false}
338
+ style={{
339
+ position: 'absolute',
340
+ left: 2 * scale,
341
+ top: 2 * scale,
342
+ width: 18 * scale,
343
+ height: 18 * scale,
344
+ imageRendering: 'pixelated',
345
+ }}
346
+ />
347
+ </div>
348
+ </>
349
+ )
350
+ }
@@ -13,6 +13,7 @@ import { HotbarExtras } from './HotbarExtras'
13
13
  import { AnvilInput } from './AnvilInput'
14
14
  import { AnvilCost } from './AnvilCost'
15
15
  import { EntityDisplay } from './EntityDisplay'
16
+ import { BeaconEffects } from './BeaconEffects'
16
17
 
17
18
  interface InventoryWindowProps {
18
19
  type: string
@@ -67,7 +68,9 @@ export function InventoryWindow({
67
68
  }
68
69
 
69
70
  // Player inventory: don't show title (it's just the player's own inventory)
70
- const effectiveTitle = type === 'player' ? undefined : (title ?? windowState?.title ?? def.title)
71
+ // Beacon: vanilla overrides renderLabels to show "Primary/Secondary Power" instead of title
72
+ const isBeacon = def?.name === 'beacon'
73
+ const effectiveTitle = (type === 'player' || isBeacon) ? undefined : (title ?? windowState?.title ?? def.title)
71
74
  const isVillager = type === 'villager'
72
75
  const isEnchanting = def?.name === 'enchanting_table'
73
76
  const isHotbar = type === 'hotbar'
@@ -144,6 +147,9 @@ export function InventoryWindow({
144
147
  {isAnvil && <AnvilInput x={59} y={20} width={110} height={16} />}
145
148
  {isAnvil && <AnvilCost properties={effectiveProperties} backgroundWidth={def.backgroundWidth} />}
146
149
 
150
+ {/* Beacon effect selection panel */}
151
+ {isBeacon && <BeaconEffects properties={effectiveProperties} />}
152
+
147
153
  {/* Hotbar extras: active slot indicator, offhand slot, open-inventory button */}
148
154
  {isHotbar && (
149
155
  <HotbarExtras
@@ -654,21 +654,27 @@ export function createMineflayerConnector(bot: MineflayerBot, options?: Mineflay
654
654
  }
655
655
 
656
656
  if (action.type === 'beacon' && win && isBeaconWindow(win)) {
657
+ // EFFECTS values are 1-based (matching vanilla encodeEffect: registry_id + 1)
658
+ // Protocol option(varint): undefined=absent, varint=1-based effect ID
659
+ // Server ContainerData uses same encoding: ≤0 = none, 1+ = effect
660
+ const protoPrimary = action.primaryEffect > 0 ? action.primaryEffect : undefined
661
+ const protoSecondary = action.secondaryEffect > 0 ? action.secondaryEffect : undefined
662
+
657
663
  let handled = false
658
664
  if (typeof win.setBeaconEffects === 'function') {
659
665
  logHelperIntent(action, 'window.setBeaconEffects', {
660
- primaryEffect: action.primaryEffect,
661
- secondaryEffect: action.secondaryEffect,
666
+ primaryEffect: protoPrimary,
667
+ secondaryEffect: protoSecondary,
662
668
  })
663
- await win.setBeaconEffects(action.primaryEffect, action.secondaryEffect)
669
+ await win.setBeaconEffects(protoPrimary as any, protoSecondary as any)
664
670
  handled = true
665
671
  } else if (ext._client) {
666
672
  const packet = {
667
- primaryEffect: action.primaryEffect,
668
- secondaryEffect: action.secondaryEffect,
673
+ primary_effect: protoPrimary,
674
+ secondary_effect: protoSecondary,
669
675
  }
670
- logPacketWrite('beacon_effect', packet)
671
- ext._client.write('beacon_effect', packet)
676
+ logPacketWrite('set_beacon_effect', packet)
677
+ ext._client.write('set_beacon_effect', packet)
672
678
  handled = true
673
679
  }
674
680
  if (handled) {
@@ -44,6 +44,18 @@ import _gui_sprites_container_enchanting_table_level_3 from 'mc-assets/dist/othe
44
44
  import _gui_sprites_container_enchanting_table_level_1_disabled from 'mc-assets/dist/other-textures/latest/gui/sprites/container/enchanting_table/level_1_disabled.png'
45
45
  import _gui_sprites_container_enchanting_table_level_2_disabled from 'mc-assets/dist/other-textures/latest/gui/sprites/container/enchanting_table/level_2_disabled.png'
46
46
  import _gui_sprites_container_enchanting_table_level_3_disabled from 'mc-assets/dist/other-textures/latest/gui/sprites/container/enchanting_table/level_3_disabled.png'
47
+ import _gui_sprites_container_beacon_button from 'mc-assets/dist/other-textures/latest/gui/sprites/container/beacon/button.png'
48
+ import _gui_sprites_container_beacon_button_disabled from 'mc-assets/dist/other-textures/latest/gui/sprites/container/beacon/button_disabled.png'
49
+ import _gui_sprites_container_beacon_button_selected from 'mc-assets/dist/other-textures/latest/gui/sprites/container/beacon/button_selected.png'
50
+ import _gui_sprites_container_beacon_button_highlighted from 'mc-assets/dist/other-textures/latest/gui/sprites/container/beacon/button_highlighted.png'
51
+ import _gui_sprites_container_beacon_confirm from 'mc-assets/dist/other-textures/latest/gui/sprites/container/beacon/confirm.png'
52
+ import _gui_sprites_container_beacon_cancel from 'mc-assets/dist/other-textures/latest/gui/sprites/container/beacon/cancel.png'
53
+ import _mob_effect_speed from 'mc-assets/dist/other-textures/latest/mob_effect/speed.png'
54
+ import _mob_effect_haste from 'mc-assets/dist/other-textures/latest/mob_effect/haste.png'
55
+ import _mob_effect_resistance from 'mc-assets/dist/other-textures/latest/mob_effect/resistance.png'
56
+ import _mob_effect_jump_boost from 'mc-assets/dist/other-textures/latest/mob_effect/jump_boost.png'
57
+ import _mob_effect_strength from 'mc-assets/dist/other-textures/latest/mob_effect/strength.png'
58
+ import _mob_effect_regeneration from 'mc-assets/dist/other-textures/latest/mob_effect/regeneration.png'
47
59
 
48
60
  /**
49
61
  * Versioned texture path → bundled asset URL (or undefined for remote fallback).
@@ -93,6 +105,18 @@ export const bundledTextureMap: Record<string, string | undefined> = {
93
105
  '1.21.11/textures/gui/sprites/container/enchanting_table/level_1_disabled.png': _gui_sprites_container_enchanting_table_level_1_disabled,
94
106
  '1.21.11/textures/gui/sprites/container/enchanting_table/level_2_disabled.png': _gui_sprites_container_enchanting_table_level_2_disabled,
95
107
  '1.21.11/textures/gui/sprites/container/enchanting_table/level_3_disabled.png': _gui_sprites_container_enchanting_table_level_3_disabled,
108
+ '1.21.11/textures/gui/sprites/container/beacon/button.png': _gui_sprites_container_beacon_button,
109
+ '1.21.11/textures/gui/sprites/container/beacon/button_disabled.png': _gui_sprites_container_beacon_button_disabled,
110
+ '1.21.11/textures/gui/sprites/container/beacon/button_selected.png': _gui_sprites_container_beacon_button_selected,
111
+ '1.21.11/textures/gui/sprites/container/beacon/button_highlighted.png': _gui_sprites_container_beacon_button_highlighted,
112
+ '1.21.11/textures/gui/sprites/container/beacon/confirm.png': _gui_sprites_container_beacon_confirm,
113
+ '1.21.11/textures/gui/sprites/container/beacon/cancel.png': _gui_sprites_container_beacon_cancel,
114
+ '1.21.11/textures/mob_effect/speed.png': _mob_effect_speed,
115
+ '1.21.11/textures/mob_effect/haste.png': _mob_effect_haste,
116
+ '1.21.11/textures/mob_effect/resistance.png': _mob_effect_resistance,
117
+ '1.21.11/textures/mob_effect/jump_boost.png': _mob_effect_jump_boost,
118
+ '1.21.11/textures/mob_effect/strength.png': _mob_effect_strength,
119
+ '1.21.11/textures/mob_effect/regeneration.png': _mob_effect_regeneration,
96
120
  }
97
121
 
98
122
 
@@ -143,6 +167,18 @@ export const allTexturePaths: readonly string[] = [
143
167
  'gui/sprites/container/enchanting_table/level_1_disabled.png',
144
168
  'gui/sprites/container/enchanting_table/level_2_disabled.png',
145
169
  'gui/sprites/container/enchanting_table/level_3_disabled.png',
170
+ 'gui/sprites/container/beacon/button.png',
171
+ 'gui/sprites/container/beacon/button_disabled.png',
172
+ 'gui/sprites/container/beacon/button_selected.png',
173
+ 'gui/sprites/container/beacon/button_highlighted.png',
174
+ 'gui/sprites/container/beacon/confirm.png',
175
+ 'gui/sprites/container/beacon/cancel.png',
176
+ 'mob_effect/speed.png',
177
+ 'mob_effect/haste.png',
178
+ 'mob_effect/resistance.png',
179
+ 'mob_effect/jump_boost.png',
180
+ 'mob_effect/strength.png',
181
+ 'mob_effect/regeneration.png',
146
182
  ]
147
183
 
148
184
  /**
@@ -13,12 +13,12 @@ function withSequentialIndexes(slots: RawSlot[]): SlotDefinition[] {
13
13
  })
14
14
  }
15
15
 
16
- function playerInv(yOffset: number): RawSlot[] {
16
+ function playerInv(yOffset: number, xStart = 8): RawSlot[] {
17
17
  const slots: RawSlot[] = []
18
18
  for (let row = 0; row < 3; row++) {
19
19
  for (let col = 0; col < 9; col++) {
20
20
  slots.push({
21
- x: 8 + col * SLOT_SIZE,
21
+ x: xStart + col * SLOT_SIZE,
22
22
  y: yOffset + row * SLOT_SIZE,
23
23
  group: 'inventory',
24
24
  })
@@ -26,7 +26,7 @@ function playerInv(yOffset: number): RawSlot[] {
26
26
  }
27
27
  for (let col = 0; col < 9; col++) {
28
28
  slots.push({
29
- x: 8 + col * SLOT_SIZE,
29
+ x: xStart + col * SLOT_SIZE,
30
30
  y: yOffset + 58,
31
31
  group: 'hotbar',
32
32
  })
@@ -546,9 +546,14 @@ export const inventoryDefinitions = makeInventoryDefinitions({
546
546
  backgroundTexture: '1.21.11/textures/gui/container/beacon.png',
547
547
  backgroundWidth: 230,
548
548
  backgroundHeight: 219,
549
+ properties: {
550
+ levels: { dataSlot: 0, description: 'Beacon pyramid level (0-4)' },
551
+ primaryEffect: { dataSlot: 1, description: 'Primary effect (encoded: 0=none, registry_id+1)' },
552
+ secondaryEffect: { dataSlot: 2, description: 'Secondary effect (encoded: 0=none, registry_id+1)' },
553
+ },
549
554
  slots: [
550
555
  { x: 136, y: 110, group: 'payment', label: 'Payment' },
551
- ...playerInv(136),
556
+ ...playerInv(137, 36),
552
557
  ],
553
558
  },
554
559
 
@@ -759,4 +764,18 @@ export const registryExtraTextures: string[] = [
759
764
  '1.21.11/textures/gui/sprites/container/enchanting_table/level_1_disabled.png',
760
765
  '1.21.11/textures/gui/sprites/container/enchanting_table/level_2_disabled.png',
761
766
  '1.21.11/textures/gui/sprites/container/enchanting_table/level_3_disabled.png',
767
+ // Beacon button sprites
768
+ '1.21.11/textures/gui/sprites/container/beacon/button.png',
769
+ '1.21.11/textures/gui/sprites/container/beacon/button_disabled.png',
770
+ '1.21.11/textures/gui/sprites/container/beacon/button_selected.png',
771
+ '1.21.11/textures/gui/sprites/container/beacon/button_highlighted.png',
772
+ '1.21.11/textures/gui/sprites/container/beacon/confirm.png',
773
+ '1.21.11/textures/gui/sprites/container/beacon/cancel.png',
774
+ // Beacon mob effect icons
775
+ '1.21.11/textures/mob_effect/speed.png',
776
+ '1.21.11/textures/mob_effect/haste.png',
777
+ '1.21.11/textures/mob_effect/resistance.png',
778
+ '1.21.11/textures/mob_effect/jump_boost.png',
779
+ '1.21.11/textures/mob_effect/strength.png',
780
+ '1.21.11/textures/mob_effect/regeneration.png',
762
781
  ]