300 lines
8.4 KiB
JavaScript
300 lines
8.4 KiB
JavaScript
/**
|
|
* SkinScene.js
|
|
* Tank skin gallery scene where players can preview, purchase, and equip skins.
|
|
*/
|
|
|
|
const {
|
|
SCREEN_WIDTH,
|
|
SCREEN_HEIGHT,
|
|
COLORS,
|
|
SCENE,
|
|
} = require('../base/GameGlobal');
|
|
const { t } = require('../i18n/I18n');
|
|
|
|
// Layout constants
|
|
const COLS = 2;
|
|
const CARD_GAP = 10;
|
|
const CARD_W = Math.min((SCREEN_WIDTH * 0.85 - CARD_GAP) / COLS, 150);
|
|
const CARD_H = 100;
|
|
const GRID_W = COLS * CARD_W + (COLS - 1) * CARD_GAP;
|
|
const GRID_LEFT = (SCREEN_WIDTH - GRID_W) / 2;
|
|
const CARDS_START_Y = SCREEN_HEIGHT * 0.18;
|
|
|
|
const SkinScene = {
|
|
_buttons: {},
|
|
_skinCards: [],
|
|
_message: '',
|
|
_messageTimer: 0,
|
|
_scrollY: 0,
|
|
|
|
enter() {
|
|
this._message = '';
|
|
this._messageTimer = 0;
|
|
this._scrollY = 0;
|
|
this._calculateLayout();
|
|
},
|
|
|
|
exit() {},
|
|
|
|
_calculateLayout() {
|
|
const sm = GameGlobal.skinManager;
|
|
if (!sm) return;
|
|
|
|
const skins = sm.getAllSkins();
|
|
this._skinCards = [];
|
|
|
|
for (let i = 0; i < skins.length; i++) {
|
|
const col = i % COLS;
|
|
const row = Math.floor(i / COLS);
|
|
const x = GRID_LEFT + col * (CARD_W + CARD_GAP);
|
|
const y = CARDS_START_Y + row * (CARD_H + CARD_GAP);
|
|
|
|
this._skinCards.push({
|
|
...skins[i],
|
|
rect: { x, y, w: CARD_W, h: CARD_H },
|
|
});
|
|
}
|
|
|
|
// Back button
|
|
const backW = 100;
|
|
const backH = 36;
|
|
const lastRow = Math.ceil(skins.length / COLS);
|
|
const backY = CARDS_START_Y + lastRow * (CARD_H + CARD_GAP) + 10;
|
|
this._buttons = {
|
|
back: { x: (SCREEN_WIDTH - backW) / 2, y: backY, 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 22px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(t('skin.title') || 'Tank Skins', SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.06);
|
|
|
|
// Gold balance
|
|
const gold = GameGlobal.currencyManager ? GameGlobal.currencyManager.getGold() : 0;
|
|
ctx.fillStyle = '#FFD700';
|
|
ctx.font = 'bold 14px Arial';
|
|
ctx.fillText(`🪙 ${gold}`, SCREEN_WIDTH / 2, SCREEN_HEIGHT * 0.12);
|
|
|
|
const sm = GameGlobal.skinManager;
|
|
if (!sm) return;
|
|
|
|
const equippedId = sm.getEquippedSkinId();
|
|
|
|
// Render skin cards
|
|
for (const card of this._skinCards) {
|
|
this._renderSkinCard(ctx, card, sm.isUnlocked(card.id), equippedId === card.id);
|
|
}
|
|
|
|
// Back button
|
|
this._renderButton(ctx, this._buttons.back, t('common.back') || '← Back', '#666666');
|
|
|
|
// Message toast
|
|
if (this._message) {
|
|
ctx.fillStyle = 'rgba(0,0,0,0.7)';
|
|
const msgW = 200;
|
|
const msgH = 30;
|
|
const msgX = (SCREEN_WIDTH - msgW) / 2;
|
|
const msgY = SCREEN_HEIGHT * 0.92;
|
|
ctx.fillRect(msgX, msgY, msgW, msgH);
|
|
ctx.fillStyle = '#FFFFFF';
|
|
ctx.font = '13px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(this._message, SCREEN_WIDTH / 2, msgY + msgH / 2);
|
|
}
|
|
},
|
|
|
|
_renderSkinCard(ctx, card, isUnlocked, isEquipped) {
|
|
const { rect } = card;
|
|
|
|
// Card background
|
|
if (isEquipped) {
|
|
ctx.fillStyle = 'rgba(76, 175, 80, 0.3)';
|
|
ctx.strokeStyle = '#4CAF50';
|
|
} else if (isUnlocked) {
|
|
ctx.fillStyle = 'rgba(255,255,255,0.08)';
|
|
ctx.strokeStyle = '#666666';
|
|
} else {
|
|
ctx.fillStyle = 'rgba(255,255,255,0.03)';
|
|
ctx.strokeStyle = '#444444';
|
|
}
|
|
ctx.lineWidth = isEquipped ? 2.5 : 1.5;
|
|
|
|
// Rounded rect
|
|
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();
|
|
|
|
const cx = rect.x + rect.w / 2;
|
|
|
|
// Tank preview (mini tank icon)
|
|
this._drawMiniTank(ctx, cx, rect.y + 28, card);
|
|
|
|
// Skin name
|
|
ctx.fillStyle = '#FFFFFF';
|
|
ctx.font = 'bold 12px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(t(card.nameKey) || card.id, cx, rect.y + 58);
|
|
|
|
// Status / price
|
|
if (isEquipped) {
|
|
ctx.fillStyle = '#4CAF50';
|
|
ctx.font = 'bold 11px Arial';
|
|
ctx.fillText(t('skin.equipped') || '✓ Equipped', cx, rect.y + rect.h - 16);
|
|
} else if (isUnlocked) {
|
|
ctx.fillStyle = '#AAAAAA';
|
|
ctx.font = '11px Arial';
|
|
ctx.fillText(t('skin.owned') || 'Owned', cx, rect.y + rect.h - 16);
|
|
} else {
|
|
const canAfford = GameGlobal.currencyManager && GameGlobal.currencyManager.hasGold(card.cost);
|
|
ctx.fillStyle = canAfford ? '#FFD700' : '#FF4444';
|
|
ctx.font = 'bold 11px Arial';
|
|
ctx.fillText(`🪙 ${card.cost}`, cx, rect.y + rect.h - 16);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Draw a mini tank preview with skin colors.
|
|
*/
|
|
_drawMiniTank(ctx, cx, cy, card) {
|
|
const size = 14;
|
|
const bodyColor = card.colors ? card.colors.body : '#FFD700';
|
|
const turretColor = card.colors ? card.colors.turret : '#B8860B';
|
|
const trackColor = card.colors ? card.colors.track : '#8B6914';
|
|
|
|
ctx.save();
|
|
ctx.translate(cx, cy);
|
|
|
|
// Body
|
|
ctx.fillStyle = bodyColor;
|
|
ctx.fillRect(-size, -size / 2, size * 2, size);
|
|
|
|
// Turret (barrel)
|
|
ctx.fillStyle = turretColor;
|
|
ctx.fillRect(-2, -size / 2 - 10, 4, 10);
|
|
|
|
// Center detail
|
|
ctx.fillStyle = turretColor;
|
|
ctx.fillRect(-size * 0.3, -size * 0.3, size * 0.6, size * 0.6);
|
|
|
|
// Tracks
|
|
ctx.fillStyle = trackColor;
|
|
ctx.fillRect(-size - 3, -size / 2, 3, size);
|
|
ctx.fillRect(size, -size / 2, 3, size);
|
|
|
|
ctx.restore();
|
|
},
|
|
|
|
_renderButton(ctx, rect, label, color) {
|
|
if (!rect) return;
|
|
|
|
ctx.fillStyle = color;
|
|
ctx.strokeStyle = '#333333';
|
|
ctx.lineWidth = 1;
|
|
|
|
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.stroke();
|
|
|
|
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 sm = GameGlobal.skinManager;
|
|
if (!sm) return;
|
|
|
|
// Check skin cards
|
|
for (const card of this._skinCards) {
|
|
if (this._hitTest(tx, ty, card.rect)) {
|
|
if (sm.isUnlocked(card.id)) {
|
|
// Equip
|
|
if (sm.getEquippedSkinId() !== card.id) {
|
|
const result = sm.equipSkin(card.id);
|
|
if (result.success) {
|
|
this._showMessage(t('skin.equipSuccess') || '✓ Skin equipped!');
|
|
}
|
|
}
|
|
} else {
|
|
// Purchase
|
|
const result = sm.purchaseSkin(card.id);
|
|
if (result.success) {
|
|
this._showMessage(t('skin.purchaseSuccess') || '✓ Skin unlocked!');
|
|
// Auto-equip after purchase
|
|
sm.equipSkin(card.id);
|
|
} else if (result.error === 'Insufficient gold') {
|
|
this._showMessage(t('currency.insufficient') || 'Insufficient Gold');
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Back button
|
|
if (this._hitTest(tx, ty, this._buttons.back)) {
|
|
GameGlobal.sceneManager.switchTo(SCENE.MENU);
|
|
return;
|
|
}
|
|
},
|
|
};
|
|
|
|
module.exports = SkinScene;
|