feat: 星域故事汇小游戏初始版本

This commit is contained in:
2026-03-03 16:57:49 +08:00
commit cc0e39cccc
34 changed files with 6556 additions and 0 deletions

View File

@@ -0,0 +1,371 @@
/**
* 个人中心场景
*/
import BaseScene from './BaseScene';
export default class ProfileScene extends BaseScene {
constructor(main, params) {
super(main, params);
this.collections = [];
this.progress = [];
this.currentTab = 0; // 0: 收藏, 1: 历史
this.scrollY = 0;
this.maxScrollY = 0;
this.isDragging = false;
this.lastTouchY = 0;
this.scrollVelocity = 0;
this.hasMoved = false;
}
async init() {
await this.loadData();
}
async loadData() {
if (this.main.userManager.isLoggedIn) {
this.collections = await this.main.userManager.getCollections() || [];
this.progress = await this.main.userManager.getProgress() || [];
}
this.calculateMaxScroll();
}
calculateMaxScroll() {
const list = this.currentTab === 0 ? this.collections : this.progress;
const cardHeight = 90;
const gap = 12;
const headerHeight = 300;
const contentHeight = list.length * (cardHeight + gap) + headerHeight;
this.maxScrollY = Math.max(0, contentHeight - this.screenHeight + 20);
}
update() {
if (!this.isDragging && Math.abs(this.scrollVelocity) > 0.5) {
this.scrollY += this.scrollVelocity;
this.scrollVelocity *= 0.95;
this.scrollY = Math.max(0, Math.min(this.scrollY, this.maxScrollY));
}
}
render(ctx) {
// 背景渐变
const gradient = ctx.createLinearGradient(0, 0, 0, this.screenHeight);
gradient.addColorStop(0, '#0f0c29');
gradient.addColorStop(0.5, '#302b63');
gradient.addColorStop(1, '#24243e');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, this.screenWidth, this.screenHeight);
// 顶部返回
this.renderHeader(ctx);
// 用户信息卡片
this.renderUserCard(ctx);
// Tab切换
this.renderTabs(ctx);
// 列表内容
this.renderList(ctx);
}
renderHeader(ctx) {
// 顶部渐变遮罩
const headerGradient = ctx.createLinearGradient(0, 0, 0, 60);
headerGradient.addColorStop(0, 'rgba(0,0,0,0.5)');
headerGradient.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = headerGradient;
ctx.fillRect(0, 0, this.screenWidth, 60);
// 返回按钮
ctx.fillStyle = '#ffffff';
ctx.font = '16px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(' 返回', 15, 35);
// 标题
ctx.textAlign = 'center';
ctx.font = 'bold 17px sans-serif';
ctx.fillText('个人中心', this.screenWidth / 2, 35);
}
renderUserCard(ctx) {
const cardY = 60;
const cardHeight = 170;
const centerX = this.screenWidth / 2;
const user = this.main.userManager;
// 卡片背景
ctx.fillStyle = 'rgba(255,255,255,0.06)';
this.roundRect(ctx, 15, cardY, this.screenWidth - 30, cardHeight, 16);
ctx.fill();
// 头像
const avatarSize = 55;
const avatarY = cardY + 20;
const avatarGradient = ctx.createLinearGradient(centerX - 30, avatarY, centerX + 30, avatarY + avatarSize);
avatarGradient.addColorStop(0, '#ff6b6b');
avatarGradient.addColorStop(1, '#ffd700');
ctx.fillStyle = avatarGradient;
ctx.beginPath();
ctx.arc(centerX, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
ctx.fill();
// 头像文字
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 22px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(user.nickname ? user.nickname[0] : '游', centerX, avatarY + avatarSize / 2 + 8);
// 昵称
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 15px sans-serif';
ctx.fillText(user.nickname || '游客用户', centerX, avatarY + avatarSize + 20);
// 分割线
const lineY = avatarY + avatarSize + 35;
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(30, lineY);
ctx.lineTo(this.screenWidth - 30, lineY);
ctx.stroke();
// 统计信息
const statsY = lineY + 30;
const statWidth = (this.screenWidth - 30) / 3;
const statsData = [
{ num: this.progress.length, label: '游玩' },
{ num: this.collections.length, label: '收藏' },
{ num: this.progress.filter(p => p.is_completed).length, label: '结局' }
];
statsData.forEach((stat, i) => {
const x = 15 + statWidth * i + statWidth / 2;
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 18px sans-serif';
ctx.fillText(stat.num.toString(), x, statsY);
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '11px sans-serif';
ctx.fillText(stat.label, x, statsY + 16);
});
}
renderTabs(ctx) {
const tabY = 245;
const tabWidth = (this.screenWidth - 30) / 2;
const tabs = ['我的收藏', '游玩记录'];
tabs.forEach((tab, index) => {
const x = 15 + index * tabWidth;
const isActive = index === this.currentTab;
const centerX = x + tabWidth / 2;
// Tab背景
if (isActive) {
const tabGradient = ctx.createLinearGradient(x, tabY, x + tabWidth, tabY);
tabGradient.addColorStop(0, '#ff6b6b');
tabGradient.addColorStop(1, '#ffd700');
ctx.fillStyle = tabGradient;
this.roundRect(ctx, x + 10, tabY, tabWidth - 20, 32, 16);
ctx.fill();
}
// Tab文字
ctx.fillStyle = isActive ? '#ffffff' : 'rgba(255,255,255,0.5)';
ctx.font = isActive ? 'bold 14px sans-serif' : '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(tab, centerX, tabY + 21);
});
}
renderList(ctx) {
const list = this.currentTab === 0 ? this.collections : this.progress;
const startY = 295;
const cardHeight = 90;
const cardMargin = 12;
const padding = 15;
// 裁剪区域
ctx.save();
ctx.beginPath();
ctx.rect(0, startY - 10, this.screenWidth, this.screenHeight - startY + 10);
ctx.clip();
if (list.length === 0) {
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(
this.currentTab === 0 ? '还没有收藏的故事' : '还没有游玩记录',
this.screenWidth / 2,
startY + 50
);
ctx.restore();
return;
}
list.forEach((item, index) => {
const y = startY + index * (cardHeight + cardMargin) - this.scrollY;
if (y > -cardHeight && y < this.screenHeight) {
this.renderListCard(ctx, item, padding, y, this.screenWidth - padding * 2, cardHeight, index);
}
});
ctx.restore();
}
renderListCard(ctx, item, x, y, width, height, index) {
// 卡片背景
ctx.fillStyle = 'rgba(255,255,255,0.06)';
this.roundRect(ctx, x, y, width, height, 12);
ctx.fill();
// 封面
const coverSize = 65;
const coverColors = [
['#ff758c', '#ff7eb3'],
['#667eea', '#764ba2'],
['#4facfe', '#00f2fe'],
['#43e97b', '#38f9d7'],
['#fa709a', '#fee140']
];
const colors = coverColors[index % coverColors.length];
const coverGradient = ctx.createLinearGradient(x + 12, y + 12, x + 12 + coverSize, y + 12 + coverSize);
coverGradient.addColorStop(0, colors[0]);
coverGradient.addColorStop(1, colors[1]);
ctx.fillStyle = coverGradient;
this.roundRect(ctx, x + 12, y + 12, coverSize, coverSize, 10);
ctx.fill();
// 封面文字
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(item.category || '故事', x + 12 + coverSize / 2, y + 12 + coverSize / 2 + 4);
// 标题
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 15px sans-serif';
ctx.textAlign = 'left';
const title = item.story_title || item.title || '未知故事';
ctx.fillText(title.length > 12 ? title.substring(0, 12) + '...' : title, x + 90, y + 35);
// 状态
ctx.font = '12px sans-serif';
if (this.currentTab === 1 && item.is_completed) {
ctx.fillStyle = '#4ecca3';
ctx.fillText('✓ 已完成', x + 90, y + 58);
} else if (this.currentTab === 1) {
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.fillText('进行中...', x + 90, y + 58);
} else {
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.fillText(item.category || '', x + 90, y + 58);
}
// 继续按钮
const btnGradient = ctx.createLinearGradient(x + width - 65, y + 30, x + width - 10, y + 30);
btnGradient.addColorStop(0, '#ff6b6b');
btnGradient.addColorStop(1, '#ffd700');
ctx.fillStyle = btnGradient;
this.roundRect(ctx, x + width - 65, y + 30, 52, 28, 14);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('继续', x + width - 39, y + 49);
}
// 圆角矩形
roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
}
onTouchStart(e) {
const touch = e.touches[0];
this.lastTouchY = touch.clientY;
this.touchStartY = touch.clientY;
this.isDragging = true;
this.scrollVelocity = 0;
this.hasMoved = false;
}
onTouchMove(e) {
if (!this.isDragging) return;
const touch = e.touches[0];
const deltaY = this.lastTouchY - touch.clientY;
if (Math.abs(touch.clientY - this.touchStartY) > 5) {
this.hasMoved = true;
}
this.scrollVelocity = deltaY;
this.scrollY += deltaY;
this.scrollY = Math.max(0, Math.min(this.scrollY, this.maxScrollY));
this.lastTouchY = touch.clientY;
}
onTouchEnd(e) {
this.isDragging = false;
if (this.hasMoved) return;
const touch = e.changedTouches[0];
const x = touch.clientX;
const y = touch.clientY;
// 返回按钮
if (y < 50 && x < 80) {
this.main.sceneManager.switchScene('home');
return;
}
// Tab切换
if (y >= 240 && y <= 285) {
const tabWidth = (this.screenWidth - 30) / 2;
const newTab = x < 15 + tabWidth ? 0 : 1;
if (newTab !== this.currentTab) {
this.currentTab = newTab;
this.scrollY = 0;
this.calculateMaxScroll();
}
return;
}
// 卡片点击
this.handleCardClick(x, y);
}
handleCardClick(x, y) {
const list = this.currentTab === 0 ? this.collections : this.progress;
const startY = 295;
const cardHeight = 90;
const cardMargin = 12;
const padding = 15;
const adjustedY = y + this.scrollY;
const index = Math.floor((adjustedY - startY) / (cardHeight + cardMargin));
if (index >= 0 && index < list.length) {
const item = list[index];
const storyId = item.story_id || item.id;
const cardY = startY + index * (cardHeight + cardMargin) - this.scrollY;
const buttonX = padding + (this.screenWidth - padding * 2) - 65;
if (x >= buttonX && x <= buttonX + 52 && y >= cardY + 30 && y <= cardY + 58) {
this.main.sceneManager.switchScene('story', { storyId });
}
}
}
}