/** * ShopScene.js * Simplified shop scene for monetization-lite. * Shows 3 products: Ad-Free (¥18), Gold Pack (¥6), Newcomer Pack (¥1). */ const { SCREEN_WIDTH, SCREEN_HEIGHT, COLORS, SCENE, } = require('../base/GameGlobal'); const { t } = require('../i18n/I18n'); // Layout const CARD_W = Math.min(SCREEN_WIDTH * 0.85, 320); const CARD_H = 70; const CARD_GAP = 12; const CARD_X = (SCREEN_WIDTH - CARD_W) / 2; const CARDS_START_Y = SCREEN_HEIGHT * 0.25; const ShopScene = { _buttons: {}, _message: '', _messageTimer: 0, enter() { this._buttons = {}; this._message = ''; this._messageTimer = 0; this._calculateLayout(); }, exit() {}, _calculateLayout() { let y = CARDS_START_Y; // Ad-Free card this._buttons.adFree = { x: CARD_X, y, w: CARD_W, h: CARD_H }; y += CARD_H + CARD_GAP; // Gold Pack card this._buttons.goldPack = { x: CARD_X, y, w: CARD_W, h: CARD_H }; y += CARD_H + CARD_GAP; // Newcomer Pack card (only if available) this._buttons.newcomerPack = { x: CARD_X, y, w: CARD_W, h: CARD_H }; y += CARD_H + CARD_GAP + 10; // Back button const backW = 100; const backH = 36; this._buttons.back = { x: (SCREEN_WIDTH - backW) / 2, y, w: backW, h: backH }; }, update(dt) { if (this._messageTimer > 0) { this._messageTimer -= dt; if (this._messageTimer <= 0) { this._message = ''; } } }, render(ctx) { // Background ctx.fillStyle = COLORS.MENU_BG; ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); // Title ctx.fillStyle = COLORS.MENU_TITLE; ctx.font = 'bold 24px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(t('shop.title') || 'Shop', SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.08); // Gold balance const gold = GameGlobal.currencyManager ? GameGlobal.currencyManager.getGold() : 0; ctx.fillStyle = '#FFD700'; ctx.font = 'bold 16px Arial'; ctx.fillText(`🪙 ${gold}`, SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.16); const pm = GameGlobal.paymentManager; // Ad-Free card const adFreePurchased = pm && pm.isAdFreePurchased(); this._renderProductCard(ctx, this._buttons.adFree, t('shop.adFree') || 'Remove Ads', t('shop.adFreeDesc') || 'Permanently remove interstitial ads', adFreePurchased ? (t('shop.adFreeOwned') || 'Owned') : '¥18', adFreePurchased ); // Gold Pack card this._renderProductCard(ctx, this._buttons.goldPack, t('shop.goldPack') || 'Gold Pack', t('shop.goldPackDesc') || '1000 Gold', '¥6', false ); // Newcomer Pack card const newcomerAvailable = pm && pm.isNewcomerPackAvailable(); if (newcomerAvailable) { const remainingMs = pm.getNewcomerPackRemainingMs(); const hours = Math.floor(remainingMs / (60 * 60 * 1000)); const mins = Math.floor((remainingMs % (60 * 60 * 1000)) / (60 * 1000)); const timeStr = `${hours}h ${mins}m`; this._renderProductCard(ctx, this._buttons.newcomerPack, t('shop.newcomerPack') || 'Newcomer Pack', `${t('shop.newcomerPackDesc') || '500 Gold'} (⏰ ${timeStr})`, '¥1', false, '#FF9800' // highlight color for limited time ); } else { // Show expired/purchased state this._renderProductCard(ctx, this._buttons.newcomerPack, t('shop.newcomerPack') || 'Newcomer Pack', t('shop.newcomerExpired') || 'Expired', '--', true ); } // Back button this._renderButton(ctx, this._buttons.back, t('common.back') || '← Back', '#666666'); // Toast message if (this._message) { ctx.fillStyle = 'rgba(0,0,0,0.7)'; const msgW = 200; const msgH = 36; ctx.fillRect(SCREEN_WIDTH / 2 - msgW / 2, SCREEN_HEIGHT * 0.92 - msgH / 2, msgW, msgH); ctx.fillStyle = '#FFFFFF'; ctx.font = '14px Arial'; ctx.textAlign = 'center'; ctx.fillText(this._message, SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.92); } }, _renderProductCard(ctx, rect, title, desc, priceLabel, disabled, highlightColor) { if (!rect) return; // Card background ctx.fillStyle = disabled ? 'rgba(255,255,255,0.02)' : 'rgba(255,255,255,0.06)'; ctx.strokeStyle = highlightColor || (disabled ? '#333333' : '#555555'); ctx.lineWidth = highlightColor ? 2 : 1; const r = 8; ctx.beginPath(); ctx.moveTo(rect.x + r, rect.y); ctx.lineTo(rect.x + rect.w - r, rect.y); ctx.arcTo(rect.x + rect.w, rect.y, rect.x + rect.w, rect.y + r, r); ctx.lineTo(rect.x + rect.w, rect.y + rect.h - r); ctx.arcTo(rect.x + rect.w, rect.y + rect.h, rect.x + rect.w - r, rect.y + rect.h, r); ctx.lineTo(rect.x + r, rect.y + rect.h); ctx.arcTo(rect.x, rect.y + rect.h, rect.x, rect.y + rect.h - r, r); ctx.lineTo(rect.x, rect.y + r); ctx.arcTo(rect.x, rect.y, rect.x + r, rect.y, r); ctx.closePath(); ctx.fill(); ctx.stroke(); // Title (left aligned) ctx.fillStyle = disabled ? '#666666' : '#FFFFFF'; ctx.font = 'bold 15px Arial'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(title, rect.x + 15, rect.y + rect.h * 0.35); // Description ctx.fillStyle = disabled ? '#444444' : '#AAAAAA'; ctx.font = '11px Arial'; ctx.fillText(desc, rect.x + 15, rect.y + rect.h * 0.65); // Price (right aligned) ctx.fillStyle = disabled ? '#444444' : (highlightColor || '#FFD700'); ctx.font = 'bold 16px Arial'; ctx.textAlign = 'right'; ctx.fillText(priceLabel, rect.x + rect.w - 15, rect.y + rect.h / 2); }, _renderButton(ctx, rect, label, color) { if (!rect) return; ctx.fillStyle = color; const r = 6; ctx.beginPath(); ctx.moveTo(rect.x + r, rect.y); ctx.lineTo(rect.x + rect.w - r, rect.y); ctx.arcTo(rect.x + rect.w, rect.y, rect.x + rect.w, rect.y + r, r); ctx.lineTo(rect.x + rect.w, rect.y + rect.h - r); ctx.arcTo(rect.x + rect.w, rect.y + rect.h, rect.x + rect.w - r, rect.y + rect.h, r); ctx.lineTo(rect.x + r, rect.y + rect.h); ctx.arcTo(rect.x, rect.y + rect.h, rect.x, rect.y + rect.h - r, r); ctx.lineTo(rect.x, rect.y + r); ctx.arcTo(rect.x, rect.y, rect.x + r, rect.y, r); ctx.closePath(); ctx.fill(); ctx.fillStyle = '#FFFFFF'; ctx.font = 'bold 14px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(label, rect.x + rect.w / 2, rect.y + rect.h / 2); }, _hitTest(tx, ty, rect) { if (!rect) return false; return tx >= rect.x && tx <= rect.x + rect.w && ty >= rect.y && ty <= rect.y + rect.h; }, _showMessage(msg) { this._message = msg; this._messageTimer = 2; }, handleTouch(eventType, e) { if (eventType !== 'touchstart') return; const touch = e.touches[0]; const tx = touch.clientX; const ty = touch.clientY; const pm = GameGlobal.paymentManager; // Ad-Free if (this._hitTest(tx, ty, this._buttons.adFree)) { if (pm && !pm.isAdFreePurchased()) { pm.purchaseAdFree((result) => { if (result.success) { this._showMessage('✅ Ad-Free activated!'); } else { this._showMessage(result.error || 'Purchase failed'); } }); } return; } // Gold Pack if (this._hitTest(tx, ty, this._buttons.goldPack)) { if (pm) { pm.purchaseGoldPack((result) => { if (result.success) { this._showMessage('✅ +1000 Gold!'); } else { this._showMessage(result.error || 'Purchase failed'); } }); } return; } // Newcomer Pack if (this._hitTest(tx, ty, this._buttons.newcomerPack)) { if (pm && pm.isNewcomerPackAvailable()) { pm.purchaseNewcomerPack((result) => { if (result.success) { this._showMessage('✅ +500 Gold!'); } else { this._showMessage(result.error || 'Purchase failed'); } }); } return; } // Back if (this._hitTest(tx, ty, this._buttons.back)) { GameGlobal.sceneManager.switchTo(SCENE.MENU); return; } }, }; module.exports = ShopScene;