JavaScript · 13091 bytes Raw Blame History
1 // Shop UI overlay for dougk
2 // DOM-based shop interface that appears when Donny or Ollie approach
3
4 import gameState from '../gameState.js'
5 import inventory from './inventory.js'
6 import { OUTFITS, BUILDINGS, CHARACTERS, getOutfitsForCharacter, getAllBuildings, getItem } from './items.js'
7 import { playPurchase, playShopOpen, playShopClose } from '../sounds.js'
8
9 let shopOverlay = null
10 let currentTab = 'outfits'
11 let currentCharacter = CHARACTERS.DOUG
12 let currentShopkeeper = null
13 let onCloseCallback = null
14 let onPurchaseCallback = null
15
16 const STYLES = `
17 .shop-overlay {
18 position: fixed;
19 top: 0;
20 left: 0;
21 right: 0;
22 bottom: 0;
23 background: rgba(0, 0, 0, 0.7);
24 display: flex;
25 justify-content: center;
26 align-items: center;
27 z-index: 2000;
28 font-family: 'Courier New', monospace;
29 }
30
31 .shop-container {
32 background: linear-gradient(135deg, #2a1f1a 0%, #3d2e26 100%);
33 border: 4px solid #8b6914;
34 border-radius: 16px;
35 padding: 20px;
36 max-width: 500px;
37 width: 90%;
38 max-height: 80vh;
39 overflow-y: auto;
40 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
41 }
42
43 .shop-header {
44 display: flex;
45 justify-content: space-between;
46 align-items: center;
47 margin-bottom: 16px;
48 padding-bottom: 12px;
49 border-bottom: 2px solid #8b6914;
50 }
51
52 .shop-title {
53 color: #ffd700;
54 font-size: 24px;
55 font-weight: bold;
56 margin: 0;
57 }
58
59 .shop-koi-count {
60 color: #ffd700;
61 font-size: 18px;
62 display: flex;
63 align-items: center;
64 gap: 6px;
65 }
66
67 .shop-close {
68 background: #8b0000;
69 color: white;
70 border: none;
71 border-radius: 50%;
72 width: 32px;
73 height: 32px;
74 font-size: 18px;
75 cursor: pointer;
76 display: flex;
77 align-items: center;
78 justify-content: center;
79 }
80
81 .shop-close:hover {
82 background: #a00000;
83 }
84
85 .shop-tabs {
86 display: flex;
87 gap: 8px;
88 margin-bottom: 16px;
89 }
90
91 .shop-tab {
92 flex: 1;
93 padding: 10px 16px;
94 background: #4a3828;
95 border: 2px solid #6b4423;
96 border-radius: 8px;
97 color: #c9a96e;
98 font-size: 14px;
99 font-weight: bold;
100 cursor: pointer;
101 transition: all 0.2s;
102 }
103
104 .shop-tab:hover {
105 background: #5a4838;
106 }
107
108 .shop-tab.active {
109 background: #8b6914;
110 border-color: #ffd700;
111 color: #fff;
112 }
113
114 .character-selector {
115 display: flex;
116 gap: 8px;
117 margin-bottom: 16px;
118 }
119
120 .character-btn {
121 flex: 1;
122 padding: 8px;
123 background: #3a2a1a;
124 border: 2px solid #5a4a3a;
125 border-radius: 6px;
126 color: #a89070;
127 font-size: 12px;
128 cursor: pointer;
129 transition: all 0.2s;
130 }
131
132 .character-btn:hover {
133 background: #4a3a2a;
134 }
135
136 .character-btn.active {
137 background: #6b5030;
138 border-color: #c9a96e;
139 color: #ffd700;
140 }
141
142 .item-grid {
143 display: grid;
144 grid-template-columns: repeat(2, 1fr);
145 gap: 12px;
146 }
147
148 .item-card {
149 background: #3a2a1a;
150 border: 2px solid #5a4a3a;
151 border-radius: 8px;
152 padding: 12px;
153 text-align: center;
154 transition: all 0.2s;
155 }
156
157 .item-card:hover {
158 border-color: #8b6914;
159 }
160
161 .item-card.owned {
162 background: #2a3a2a;
163 border-color: #4a8b4a;
164 }
165
166 .item-card.equipped {
167 border-color: #ffd700;
168 box-shadow: 0 0 8px rgba(255, 215, 0, 0.3);
169 }
170
171 .item-card.cant-afford {
172 opacity: 0.5;
173 }
174
175 .item-name {
176 color: #e8d8c8;
177 font-size: 14px;
178 font-weight: bold;
179 margin-bottom: 8px;
180 }
181
182 .item-price {
183 color: #ffd700;
184 font-size: 16px;
185 margin-bottom: 8px;
186 display: flex;
187 align-items: center;
188 justify-content: center;
189 gap: 4px;
190 }
191
192 .item-btn {
193 width: 100%;
194 padding: 8px 12px;
195 border: none;
196 border-radius: 6px;
197 font-size: 12px;
198 font-weight: bold;
199 cursor: pointer;
200 transition: all 0.2s;
201 }
202
203 .item-btn.buy {
204 background: #8b6914;
205 color: white;
206 }
207
208 .item-btn.buy:hover {
209 background: #a07a1a;
210 }
211
212 .item-btn.buy:disabled {
213 background: #4a4a4a;
214 cursor: not-allowed;
215 }
216
217 .item-btn.equip {
218 background: #4a8b4a;
219 color: white;
220 }
221
222 .item-btn.equip:hover {
223 background: #5a9b5a;
224 }
225
226 .item-btn.unequip {
227 background: #8b4a4a;
228 color: white;
229 }
230
231 .item-btn.unequip:hover {
232 background: #9b5a5a;
233 }
234
235 .empty-message {
236 color: #888;
237 text-align: center;
238 padding: 40px;
239 font-style: italic;
240 }
241 `
242
243 function injectStyles() {
244 if (document.getElementById('shop-styles')) return
245 const style = document.createElement('style')
246 style.id = 'shop-styles'
247 style.textContent = STYLES
248 document.head.appendChild(style)
249 }
250
251 function createShopOverlay() {
252 injectStyles()
253
254 const overlay = document.createElement('div')
255 overlay.className = 'shop-overlay'
256 overlay.innerHTML = `
257 <div class="shop-container">
258 <div class="shop-header">
259 <h2 class="shop-title">${getShopTitle()}</h2>
260 <div class="shop-koi-count">
261 <span style="font-size: 20px;">🐟</span>
262 <span id="shop-koi-count">${gameState.getKoi()}</span>
263 </div>
264 <button class="shop-close">&times;</button>
265 </div>
266
267 <div id="shop-content"></div>
268 </div>
269 `
270
271 // Event listeners
272 overlay.querySelector('.shop-close').addEventListener('click', closeShop)
273 overlay.addEventListener('click', (e) => {
274 if (e.target === overlay) closeShop()
275 })
276
277 return overlay
278 }
279
280 function getShopTitle() {
281 if (currentShopkeeper === 'donny') return "Donny's Fine Structures"
282 if (currentShopkeeper === 'ollie') return "Ollie's Outfit Oddities"
283 return 'Shop'
284 }
285
286 function updateTabStyles() {
287 shopOverlay.querySelectorAll('.shop-tab').forEach(tab => {
288 tab.classList.toggle('active', tab.dataset.tab === currentTab)
289 })
290 }
291
292 function updateShopContent() {
293 const content = shopOverlay.querySelector('#shop-content')
294
295 // Donny sells buildings, Ollie sells outfits
296 if (currentShopkeeper === 'donny') {
297 content.innerHTML = renderBuildingsTab()
298 attachBuildingListeners(content)
299 } else if (currentShopkeeper === 'ollie') {
300 content.innerHTML = renderOutfitsTab()
301 attachOutfitListeners(content)
302 }
303
304 // Update koi count
305 const koiCount = shopOverlay.querySelector('#shop-koi-count')
306 if (koiCount) koiCount.textContent = gameState.getKoi()
307 }
308
309 function renderOutfitsTab() {
310 const characterBtns = Object.values(CHARACTERS).map(char => `
311 <button class="character-btn ${currentCharacter === char ? 'active' : ''}" data-character="${char}">
312 ${char.charAt(0).toUpperCase() + char.slice(1)}
313 </button>
314 `).join('')
315
316 const outfits = getOutfitsForCharacter(currentCharacter)
317
318 if (outfits.length === 0) {
319 return `
320 <div class="character-selector">${characterBtns}</div>
321 <div class="empty-message">No outfits available for ${currentCharacter}</div>
322 `
323 }
324
325 const itemCards = outfits.map(outfit => {
326 const owned = inventory.owns(outfit.id)
327 const equipped = inventory.isEquipped(outfit.character, outfit.id)
328 const canAfford = gameState.getKoi() >= outfit.price
329
330 let btnClass, btnText
331 if (!owned) {
332 btnClass = 'buy'
333 btnText = canAfford ? 'Buy' : 'Need more 🐟'
334 } else if (equipped) {
335 btnClass = 'unequip'
336 btnText = 'Unequip'
337 } else {
338 btnClass = 'equip'
339 btnText = 'Equip'
340 }
341
342 const cardClasses = ['item-card']
343 if (owned) cardClasses.push('owned')
344 if (equipped) cardClasses.push('equipped')
345 if (!owned && !canAfford) cardClasses.push('cant-afford')
346
347 return `
348 <div class="${cardClasses.join(' ')}" data-item-id="${outfit.id}">
349 <div class="item-name">${outfit.name}</div>
350 ${!owned ? `<div class="item-price">🐟 ${outfit.price}</div>` : ''}
351 <button class="item-btn ${btnClass}" ${!owned && !canAfford ? 'disabled' : ''} data-action="${btnClass}" data-item-id="${outfit.id}">
352 ${btnText}
353 </button>
354 </div>
355 `
356 }).join('')
357
358 return `
359 <div class="character-selector">${characterBtns}</div>
360 <div class="item-grid">${itemCards}</div>
361 `
362 }
363
364 function renderBuildingsTab() {
365 const buildings = getAllBuildings()
366
367 const itemCards = buildings.map(building => {
368 const owned = inventory.owns(building.id)
369 const canAfford = gameState.getKoi() >= building.price
370
371 let btnClass, btnText
372 if (owned) {
373 btnClass = 'equip'
374 btnText = 'Place'
375 } else {
376 btnClass = 'buy'
377 btnText = canAfford ? 'Buy' : 'Need more 🐟'
378 }
379
380 const cardClasses = ['item-card']
381 if (owned) cardClasses.push('owned')
382 if (!owned && !canAfford) cardClasses.push('cant-afford')
383
384 return `
385 <div class="${cardClasses.join(' ')}" data-item-id="${building.id}">
386 <div class="item-name">${building.name}</div>
387 ${!owned ? `<div class="item-price">🐟 ${building.price}</div>` : ''}
388 <button class="item-btn ${btnClass}" ${!owned && !canAfford ? 'disabled' : ''} data-action="${btnClass}" data-item-id="${building.id}">
389 ${btnText}
390 </button>
391 </div>
392 `
393 }).join('')
394
395 return `<div class="item-grid">${itemCards}</div>`
396 }
397
398 function attachOutfitListeners(content) {
399 // Character selector
400 content.querySelectorAll('.character-btn').forEach(btn => {
401 btn.addEventListener('click', () => {
402 currentCharacter = btn.dataset.character
403 updateShopContent()
404 })
405 })
406
407 // Item buttons
408 content.querySelectorAll('.item-btn').forEach(btn => {
409 btn.addEventListener('click', () => {
410 const itemId = btn.dataset.itemId
411 const action = btn.dataset.action
412 const item = getItem(itemId)
413
414 if (action === 'buy') {
415 if (gameState.spendKoi(item.price)) {
416 inventory.purchase(itemId)
417 // Recalculate affordable items after purchase
418 gameState.onPurchase()
419 // Auto-equip after buying (this also unequips same-type items)
420 const unequippedIds = inventory.equip(item.character, itemId)
421 // Notify about unequipped items first
422 if (onPurchaseCallback && unequippedIds) {
423 for (const oldId of unequippedIds) {
424 const oldItem = getItem(oldId)
425 if (oldItem) onPurchaseCallback(oldItem, 'unequip')
426 }
427 }
428 if (onPurchaseCallback) onPurchaseCallback(item, 'equip')
429 playPurchase()
430 updateShopContent()
431 }
432 } else if (action === 'equip') {
433 // Equip returns any auto-unequipped items of the same type
434 const unequippedIds = inventory.equip(item.character, itemId)
435 // Notify about unequipped items first
436 if (onPurchaseCallback && unequippedIds) {
437 for (const oldId of unequippedIds) {
438 const oldItem = getItem(oldId)
439 if (oldItem) onPurchaseCallback(oldItem, 'unequip')
440 }
441 }
442 if (onPurchaseCallback) onPurchaseCallback(item, 'equip')
443 updateShopContent()
444 } else if (action === 'unequip') {
445 inventory.unequip(item.character, itemId)
446 if (onPurchaseCallback) onPurchaseCallback(item, 'unequip')
447 updateShopContent()
448 }
449 })
450 })
451 }
452
453 function attachBuildingListeners(content) {
454 content.querySelectorAll('.item-btn').forEach(btn => {
455 btn.addEventListener('click', () => {
456 const itemId = btn.dataset.itemId
457 const action = btn.dataset.action
458 const item = getItem(itemId)
459
460 if (action === 'buy') {
461 if (gameState.spendKoi(item.price)) {
462 inventory.purchase(itemId)
463 // Recalculate affordable items after purchase
464 gameState.onPurchase()
465 if (onPurchaseCallback) onPurchaseCallback(item)
466 playPurchase()
467 updateShopContent()
468 }
469 } else if (action === 'equip') {
470 // Building placement - close shop and enter placement mode
471 // Save callback before closeShop clears it
472 const callback = onPurchaseCallback
473 closeShop()
474 if (callback) callback(item, 'place')
475 }
476 })
477 })
478 }
479
480 export function openShop(shopkeeper, container, callbacks = {}) {
481 if (shopOverlay) return // Already open
482
483 currentShopkeeper = shopkeeper
484 onCloseCallback = callbacks.onClose
485 onPurchaseCallback = callbacks.onPurchase
486
487 // Set default tab based on shopkeeper specialty
488 // Donny sells buildings, Ollie sells outfits
489 currentTab = shopkeeper === 'donny' ? 'buildings' : 'outfits'
490
491 shopOverlay = createShopOverlay()
492 container.appendChild(shopOverlay)
493 updateShopContent()
494
495 // Listen for koi count changes
496 gameState.addListener(updateKoiDisplay)
497
498 playShopOpen()
499 }
500
501 function updateKoiDisplay(count) {
502 if (shopOverlay) {
503 const koiCount = shopOverlay.querySelector('#shop-koi-count')
504 if (koiCount) koiCount.textContent = count
505 }
506 }
507
508 export function closeShop() {
509 if (!shopOverlay) return
510
511 playShopClose()
512
513 gameState.removeListener(updateKoiDisplay)
514 shopOverlay.remove()
515 shopOverlay = null
516
517 if (onCloseCallback) {
518 onCloseCallback()
519 onCloseCallback = null
520 }
521
522 currentShopkeeper = null
523 onPurchaseCallback = null
524 }
525
526 export function isShopOpen() {
527 return shopOverlay !== null
528 }