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,537 @@
/**
* 首页场景
*/
import BaseScene from './BaseScene';
export default class HomeScene extends BaseScene {
constructor(main, params) {
super(main, params);
this.storyList = [];
this.scrollY = 0;
this.maxScrollY = 0;
this.isDragging = false;
this.lastTouchY = 0;
this.scrollVelocity = 0;
this.currentTab = 0; // 0: 首页, 1: 发现, 2: 我的
this.categories = ['全部', '都市言情', '悬疑推理', '古风宫廷', '校园青春', '修仙玄幻', '穿越重生', '职场商战', '科幻未来', '恐怖惊悚', '搞笑轻喜'];
this.selectedCategory = 0;
// 分类横向滚动
this.categoryScrollX = 0;
this.maxCategoryScrollX = 0;
this.isCategoryDragging = false;
this.lastTouchX = 0;
// 存储分类标签位置(用于点击判定)
this.categoryRects = [];
}
async init() {
// 加载故事列表
this.storyList = this.main.storyManager.storyList;
this.calculateMaxScroll();
}
// 获取过滤后的故事列表
getFilteredStories() {
if (this.selectedCategory === 0) {
return this.storyList; // 全部
}
const categoryName = this.categories[this.selectedCategory];
return this.storyList.filter(s => s.category === categoryName);
}
calculateMaxScroll() {
// 计算最大滚动距离
const stories = this.getFilteredStories();
const cardHeight = 120;
const gap = 15;
const startY = 150; // 故事列表起始位置
const tabHeight = 65;
// 内容总高度
const contentBottom = startY + stories.length * (cardHeight + gap);
// 可视区域底部减去底部Tab栏
const visibleBottom = this.screenHeight - tabHeight;
// 最大滚动距离 = 内容超出可视区域的部分
this.maxScrollY = Math.max(0, contentBottom - visibleBottom);
}
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.renderStars(ctx);
// 绘制顶部标题栏
this.renderHeader(ctx);
// 绘制分类标签
this.renderCategories(ctx);
// 绘制故事列表
this.renderStoryList(ctx);
// 绘制底部Tab栏
this.renderTabBar(ctx);
}
renderStars(ctx) {
ctx.fillStyle = 'rgba(255,255,255,0.3)';
const stars = [[30, 80], [80, 30], [150, 60], [200, 25], [280, 70], [320, 40]];
stars.forEach(([x, y]) => {
ctx.beginPath();
ctx.arc(x, y, 1, 0, Math.PI * 2);
ctx.fill();
});
}
renderHeader(ctx) {
// 标题带渐变
const titleGradient = ctx.createLinearGradient(20, 30, 200, 30);
titleGradient.addColorStop(0, '#ffd700');
titleGradient.addColorStop(1, '#ff6b6b');
ctx.fillStyle = titleGradient;
ctx.font = 'bold 26px sans-serif';
ctx.textAlign = 'left';
ctx.fillText('星域故事汇', 20, 50);
// 副标题
ctx.fillStyle = 'rgba(255,255,255,0.6)';
ctx.font = '13px sans-serif';
ctx.fillText('每个选择,都是一个新世界', 20, 75);
}
renderCategories(ctx) {
const startY = 95;
const tagHeight = 30;
let x = 15 - this.categoryScrollX;
const y = startY;
// 计算总宽度用于滚动限制
ctx.font = '13px sans-serif';
let totalWidth = 15;
this.categories.forEach((cat) => {
totalWidth += ctx.measureText(cat).width + 28 + 12;
});
this.maxCategoryScrollX = Math.max(0, totalWidth - this.screenWidth + 15);
// 清空并重新记录位置
this.categoryRects = [];
this.categories.forEach((cat, index) => {
const isSelected = index === this.selectedCategory;
const textWidth = ctx.measureText(cat).width + 28;
// 记录每个标签的位置
this.categoryRects.push({
left: x + this.categoryScrollX,
right: x + this.categoryScrollX + textWidth,
index: index
});
// 只渲染可见的标签
if (x + textWidth > 0 && x < this.screenWidth) {
if (isSelected) {
// 选中态:渐变背景
const tagGradient = ctx.createLinearGradient(x, y, x + textWidth, y);
tagGradient.addColorStop(0, '#ff6b6b');
tagGradient.addColorStop(1, '#ffd700');
ctx.fillStyle = tagGradient;
} else {
ctx.fillStyle = 'rgba(255,255,255,0.08)';
}
this.roundRect(ctx, x, y, textWidth, tagHeight, 15);
ctx.fill();
// 未选中态加边框
if (!isSelected) {
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
ctx.lineWidth = 1;
this.roundRect(ctx, x, y, textWidth, tagHeight, 15);
ctx.stroke();
}
// 标签文字
ctx.fillStyle = isSelected ? '#ffffff' : 'rgba(255,255,255,0.7)';
ctx.font = isSelected ? 'bold 13px sans-serif' : '13px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(cat, x + textWidth / 2, y + 20);
}
x += textWidth + 12;
});
}
renderStoryList(ctx) {
const startY = 150;
const cardHeight = 120;
const cardPadding = 15;
const cardMargin = 15;
const stories = this.getFilteredStories();
if (stories.length === 0) {
ctx.fillStyle = '#666666';
ctx.font = '14px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('暂无该分类的故事', this.screenWidth / 2, 250);
return;
}
// 设置裁剪区域,防止卡片渲染到分类标签区域
ctx.save();
ctx.beginPath();
ctx.rect(0, startY, this.screenWidth, this.screenHeight - startY - 65);
ctx.clip();
stories.forEach((story, index) => {
const y = startY + index * (cardHeight + cardMargin) - this.scrollY;
// 只渲染可见区域的卡片
if (y > startY - cardHeight && y < this.screenHeight - 65) {
this.renderStoryCard(ctx, story, cardMargin, y, this.screenWidth - cardMargin * 2, cardHeight);
}
});
ctx.restore();
}
renderStoryCard(ctx, story, x, y, width, height) {
// 卡片背景 - 毛玻璃效果
ctx.fillStyle = 'rgba(255,255,255,0.06)';
this.roundRect(ctx, x, y, width, height, 16);
ctx.fill();
// 卡片高光边框
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
ctx.lineWidth = 1;
this.roundRect(ctx, x, y, width, height, 16);
ctx.stroke();
// 封面区域 - 渐变色
const coverWidth = 85;
const coverHeight = height - 24;
const coverGradient = ctx.createLinearGradient(x + 12, y + 12, x + 12 + coverWidth, y + 12 + coverHeight);
const colors = this.getCategoryGradient(story.category);
coverGradient.addColorStop(0, colors[0]);
coverGradient.addColorStop(1, colors[1]);
ctx.fillStyle = coverGradient;
this.roundRect(ctx, x + 12, y + 12, coverWidth, coverHeight, 12);
ctx.fill();
// 封面上的分类名
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.font = 'bold 11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(story.category || '故事', x + 12 + coverWidth / 2, y + 12 + coverHeight / 2 + 4);
// 故事标题
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 17px sans-serif';
ctx.textAlign = 'left';
const titleX = x + 110;
const maxTextWidth = width - 120; // 可用文字宽度
const title = this.truncateText(ctx, story.title, maxTextWidth);
ctx.fillText(title, titleX, y + 35);
// 故事简介
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = '12px sans-serif';
const desc = story.description || '';
const shortDesc = this.truncateText(ctx, desc, maxTextWidth);
ctx.fillText(shortDesc, titleX, y + 58);
// 统计信息带图标
ctx.fillStyle = 'rgba(255,255,255,0.4)';
ctx.font = '11px sans-serif';
const playText = `${this.formatNumber(story.play_count)}`;
const likeText = `${this.formatNumber(story.like_count)}`;
ctx.fillText(playText, titleX, y + 90);
ctx.fillText(likeText, titleX + 70, y + 90);
// 精选标签 - 更醒目
if (story.is_featured) {
const tagGradient = ctx.createLinearGradient(x + width - 55, y + 12, x + width - 10, y + 12);
tagGradient.addColorStop(0, '#ff6b6b');
tagGradient.addColorStop(1, '#ffd700');
ctx.fillStyle = tagGradient;
this.roundRect(ctx, x + width - 55, y + 12, 45, 22, 11);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('精选', x + width - 32, y + 27);
}
}
getCategoryGradient(category) {
const gradients = {
'都市言情': ['#ff758c', '#ff7eb3'],
'悬疑推理': ['#667eea', '#764ba2'],
'古风宫廷': ['#f093fb', '#f5576c'],
'校园青春': ['#4facfe', '#00f2fe'],
'修仙玄幻': ['#43e97b', '#38f9d7'],
'穿越重生': ['#fa709a', '#fee140'],
'职场商战': ['#30cfd0', '#330867'],
'科幻未来': ['#a8edea', '#fed6e3'],
'恐怖惊悚': ['#434343', '#000000'],
'搞笑轻喜': ['#f6d365', '#fda085']
};
return gradients[category] || ['#667eea', '#764ba2'];
}
renderTabBar(ctx) {
const tabHeight = 65;
const y = this.screenHeight - tabHeight;
// Tab栏背景 - 毛玻璃
ctx.fillStyle = 'rgba(15, 12, 41, 0.95)';
ctx.fillRect(0, y, this.screenWidth, tabHeight);
// 顶部高光线
const lineGradient = ctx.createLinearGradient(0, y, this.screenWidth, y);
lineGradient.addColorStop(0, 'rgba(255,107,107,0.3)');
lineGradient.addColorStop(0.5, 'rgba(255,215,0,0.3)');
lineGradient.addColorStop(1, 'rgba(255,107,107,0.3)');
ctx.strokeStyle = lineGradient;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(this.screenWidth, y);
ctx.stroke();
const tabs = [
{ icon: '🏠', label: '首页' },
{ icon: '🔍', label: '发现' },
{ icon: '👤', label: '我的' }
];
const tabWidth = this.screenWidth / tabs.length;
tabs.forEach((tab, index) => {
const centerX = index * tabWidth + tabWidth / 2;
const isActive = index === this.currentTab;
// 选中态指示器
if (isActive) {
const indicatorGradient = ctx.createLinearGradient(centerX - 20, y + 3, centerX + 20, y + 3);
indicatorGradient.addColorStop(0, '#ff6b6b');
indicatorGradient.addColorStop(1, '#ffd700');
ctx.fillStyle = indicatorGradient;
this.roundRect(ctx, centerX - 20, y + 2, 40, 3, 1.5);
ctx.fill();
}
// 图标
ctx.font = '22px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(tab.icon, centerX, y + 32);
// 标签文字
ctx.fillStyle = isActive ? '#ffffff' : 'rgba(255,255,255,0.4)';
ctx.font = isActive ? 'bold 11px sans-serif' : '11px sans-serif';
ctx.fillText(tab.label, centerX, y + 52);
});
}
// 获取分类颜色
getCategoryColor(category) {
const colors = {
'都市言情': '#e94560',
'悬疑推理': '#4a90d9',
'古风宫廷': '#d4a574',
'校园青春': '#7ed957',
'修仙玄幻': '#9b59b6',
'穿越重生': '#f39c12',
'职场商战': '#3498db',
'科幻未来': '#1abc9c',
'恐怖惊悚': '#2c3e50',
'搞笑轻喜': '#f1c40f'
};
return colors[category] || '#666666';
}
// 格式化数字
formatNumber(num) {
if (num >= 10000) {
return (num / 10000).toFixed(1) + 'w';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k';
}
return num.toString();
}
// 截断文字以适应宽度
truncateText(ctx, text, maxWidth) {
if (!text) return '';
if (ctx.measureText(text).width <= maxWidth) {
return text;
}
let truncated = text;
while (truncated.length > 0 && ctx.measureText(truncated + '...').width > maxWidth) {
truncated = truncated.slice(0, -1);
}
return truncated + '...';
}
// 绘制圆角矩形
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.lastTouchX = touch.clientX;
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
this.hasMoved = false;
// 判断是否在分类区域y: 90-140
if (touch.clientY >= 90 && touch.clientY <= 140) {
this.isCategoryDragging = true;
this.isDragging = false;
} else {
this.isCategoryDragging = false;
this.isDragging = true;
}
this.scrollVelocity = 0;
}
onTouchMove(e) {
const touch = e.touches[0];
// 分类区域横向滑动
if (this.isCategoryDragging) {
const deltaX = this.lastTouchX - touch.clientX;
if (Math.abs(deltaX) > 2) {
this.hasMoved = true;
}
this.categoryScrollX += deltaX;
this.categoryScrollX = Math.max(0, Math.min(this.categoryScrollX, this.maxCategoryScrollX));
this.lastTouchX = touch.clientX;
return;
}
// 故事列表纵向滑动
if (this.isDragging) {
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;
this.isCategoryDragging = false;
// 如果有滑动,不处理点击
if (this.hasMoved) {
return;
}
// 检测点击
const touch = e.changedTouches[0];
const x = touch.clientX;
const y = touch.clientY;
// 检测Tab栏点击
if (y > this.screenHeight - 65) {
const tabWidth = this.screenWidth / 3;
const tabIndex = Math.floor(x / tabWidth);
this.handleTabClick(tabIndex);
return;
}
// 检测分类标签点击
if (y >= 90 && y <= 140) {
this.handleCategoryClick(x);
return;
}
// 检测故事卡片点击
this.handleStoryClick(x, y);
}
handleCategoryClick(x) {
// 考虑横向滚动偏移
const adjustedX = x + this.categoryScrollX;
// 使用保存的实际位置判断
for (const rect of this.categoryRects) {
if (adjustedX >= rect.left && adjustedX <= rect.right) {
if (this.selectedCategory !== rect.index) {
this.selectedCategory = rect.index;
this.scrollY = 0;
this.calculateMaxScroll();
console.log('选中分类:', this.categories[rect.index]);
}
return;
}
}
}
handleTabClick(tabIndex) {
if (tabIndex === this.currentTab) return;
this.currentTab = tabIndex;
if (tabIndex === 2) {
// 切换到个人中心
this.main.sceneManager.switchScene('profile');
}
}
handleStoryClick(x, y) {
const startY = 150;
const cardHeight = 120;
const cardMargin = 15;
const stories = this.getFilteredStories();
// 计算点击的是哪个故事
const adjustedY = y + this.scrollY;
const index = Math.floor((adjustedY - startY) / (cardHeight + cardMargin));
if (index >= 0 && index < stories.length) {
const story = stories[index];
// 跳转到故事播放场景
this.main.sceneManager.switchScene('story', { storyId: story.id });
}
}
}