165 lines
3.8 KiB
JavaScript
165 lines
3.8 KiB
JavaScript
/**
|
|
* TutorialOverlay.js
|
|
* New player tutorial overlay shown on first play.
|
|
* Displays 2-3 step instructions for controls.
|
|
*/
|
|
|
|
const {
|
|
SCREEN_WIDTH,
|
|
SCREEN_HEIGHT,
|
|
MAP_OFFSET_X,
|
|
MAP_WIDTH,
|
|
} = require('../base/GameGlobal');
|
|
|
|
class TutorialOverlay {
|
|
constructor() {
|
|
this._active = false;
|
|
this._step = 0;
|
|
this._totalSteps = 3;
|
|
|
|
this._steps = [
|
|
{
|
|
title: '移动坦克',
|
|
desc: '拖动左下角的摇杆\n控制坦克上下左右移动',
|
|
highlight: 'joystick',
|
|
},
|
|
{
|
|
title: '发射子弹',
|
|
desc: '点击右下角的按钮\n向前方发射子弹',
|
|
highlight: 'fire',
|
|
},
|
|
{
|
|
title: '保护基地',
|
|
desc: '消灭所有敌人\n不要让基地被摧毁!',
|
|
highlight: 'base',
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Show the tutorial.
|
|
*/
|
|
show() {
|
|
this._active = true;
|
|
this._step = 0;
|
|
}
|
|
|
|
/**
|
|
* Hide the tutorial.
|
|
*/
|
|
hide() {
|
|
this._active = false;
|
|
}
|
|
|
|
/** Whether the tutorial is active. */
|
|
get active() {
|
|
return this._active;
|
|
}
|
|
|
|
/**
|
|
* Handle touch to advance steps.
|
|
* @returns {boolean} Whether the tutorial consumed the touch.
|
|
*/
|
|
handleTouch() {
|
|
if (!this._active) return false;
|
|
|
|
this._step++;
|
|
if (this._step >= this._totalSteps) {
|
|
this._active = false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Render the tutorial overlay.
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
*/
|
|
render(ctx) {
|
|
if (!this._active) return;
|
|
|
|
const step = this._steps[this._step];
|
|
|
|
// Semi-transparent overlay
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
|
ctx.fillRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
|
|
|
|
const cx = SCREEN_WIDTH / 2;
|
|
const cy = SCREEN_HEIGHT / 2;
|
|
|
|
// Step indicator
|
|
ctx.fillStyle = '#AAAAAA';
|
|
ctx.font = '12px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(`${this._step + 1} / ${this._totalSteps}`, cx, cy - 80);
|
|
|
|
// Title
|
|
ctx.fillStyle = '#FFD700';
|
|
ctx.font = 'bold 24px Arial';
|
|
ctx.fillText(step.title, cx, cy - 40);
|
|
|
|
// Description (multi-line)
|
|
ctx.fillStyle = '#FFFFFF';
|
|
ctx.font = '16px Arial';
|
|
const lines = step.desc.split('\n');
|
|
for (let i = 0; i < lines.length; i++) {
|
|
ctx.fillText(lines[i], cx, cy + 10 + i * 24);
|
|
}
|
|
|
|
// Highlight area indicator
|
|
this._drawHighlight(ctx, step.highlight);
|
|
|
|
// Tap to continue
|
|
ctx.fillStyle = '#888888';
|
|
ctx.font = '13px Arial';
|
|
ctx.fillText('点击屏幕继续', cx, SCREEN_HEIGHT - 60);
|
|
}
|
|
|
|
/**
|
|
* Draw a highlight circle around the relevant UI element.
|
|
* @private
|
|
*/
|
|
_drawHighlight(ctx, type) {
|
|
ctx.save();
|
|
ctx.strokeStyle = '#FFD700';
|
|
ctx.lineWidth = 3;
|
|
ctx.setLineDash([8, 4]);
|
|
|
|
switch (type) {
|
|
case 'joystick':
|
|
ctx.beginPath();
|
|
ctx.arc(Math.floor(MAP_OFFSET_X / 2), SCREEN_HEIGHT - 100, 65, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
break;
|
|
case 'fire': {
|
|
const rightAreaStart = MAP_OFFSET_X + MAP_WIDTH;
|
|
ctx.beginPath();
|
|
ctx.arc(Math.floor(rightAreaStart + (SCREEN_WIDTH - rightAreaStart) / 2), SCREEN_HEIGHT - 100, 50, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
break;
|
|
}
|
|
case 'base': {
|
|
// Arrow pointing to base area
|
|
const baseCx = SCREEN_WIDTH / 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(baseCx, SCREEN_HEIGHT * 0.7);
|
|
ctx.lineTo(baseCx, SCREEN_HEIGHT * 0.8);
|
|
ctx.stroke();
|
|
ctx.fillStyle = '#FFD700';
|
|
ctx.beginPath();
|
|
ctx.moveTo(baseCx - 8, SCREEN_HEIGHT * 0.8);
|
|
ctx.lineTo(baseCx + 8, SCREEN_HEIGHT * 0.8);
|
|
ctx.lineTo(baseCx, SCREEN_HEIGHT * 0.8 + 10);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
break;
|
|
}
|
|
}
|
|
|
|
ctx.setLineDash([]);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
module.exports = TutorialOverlay;
|