feat: 初始化西安旅行记忆互动地图游戏项目

完成项目基础搭建:配置 Vite 开发构建环境,集成 Phaser 3 与虚拟摇杆插件,实现地图漫游、景点触发、拍立得弹窗、进度存档等核心功能,添加占位资源与批量生成脚本,配置项目元数据与忽略规则。
This commit is contained in:
Stephen committed 2026-10-17 18:01:46 +08:00
commit a404e42ca8
42 files changed
+5489

No files matched your search

+116
View File
@@ -0,0 +1,116 @@
/**
* @fileoverview 占位照片生成脚本。
*
* 文件用途:
* - 依据 src/data/spots.js 的照片清单,批量生成 4:3 手账风占位 SVG,
* 供照片未就绪期的开发与演示使用。
*
* 核心职责:
* 1. 读取景点配置,为每张照片生成 `public/assets/photos/<id>_<序号>.svg`;
* 2. 占位视觉:暖色渐变 + 景点图标 + 名称与 caption 水印 + 虚线相框;
* 3. 文件已存在时跳过(--force 强制重生成)。
*
* 主要流程:
* node scripts/gen-placeholder-photos.mjs [--force] → 遍历 spots →
* 写出 SVG → 控制台汇总生成结果。
*
* 用法:
* node scripts/gen-placeholder-photos.mjs # 仅生成缺失文件
* node scripts/gen-placeholder-photos.mjs --force # 强制重新生成全部
*/
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { SPOTS } from '../src/data/spots.js';
/** 脚本所在目录。 */
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
/** 项目根目录。 */
const PROJECT_ROOT = resolve(SCRIPT_DIR, '..');
/** 照片输出目录。 */
const OUT_DIR = resolve(PROJECT_ROOT, 'public/assets/photos');
/** 占位卡宽度(像素)。 */
const W = 1200;
/** 占位卡高度(像素,4:3)。 */
const H = 900;
/**
* 按景点 id 生成稳定的色相(0-360)。
*
* @param {string} id 景点 id。
* @returns {number} 色相角度。
*/
function hueOf(id) {
let hash = 0;
for (const ch of id) {
hash = (hash * 31 + ch.charCodeAt(0)) % 360;
}
return hash;
}
/**
* 生成单张占位照片 SVG 内容。
*
* @param {Object} spot 景点配置。
* @param {Object} photo 照片配置(含 caption)。
* @returns {string} SVG 文本。
*/
function buildSvg(spot, photo) {
const hue = hueOf(spot.id);
const icon = spot.icon;
const escapedCaption = photo.caption
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;');
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="hsl(${hue} 38% 78%)" />
<stop offset="1" stop-color="hsl(${(hue + 24) % 360} 30% 64%)" />
</linearGradient>
</defs>
<rect width="${W}" height="${H}" fill="url(#bg)" />
<rect x="36" y="36" width="${W - 72}" height="${H - 72}" fill="none"
stroke="rgba(255,250,235,0.7)" stroke-width="4" stroke-dasharray="18 12" rx="10" />
<text x="${W / 2}" y="${H / 2 - 44}" font-size="150" text-anchor="middle">${icon}</text>
<text x="${W / 2}" y="${H / 2 + 92}" font-family="Kaiti SC, KaiTi, serif" font-size="64"
fill="rgba(70,50,28,0.82)" text-anchor="middle">${escapedCaption}</text>
<text x="${W / 2}" y="${H / 2 + 158}" font-family="Kaiti SC, KaiTi, serif" font-size="36"
fill="rgba(70,50,28,0.55)" text-anchor="middle" letter-spacing="8">· 占位照片 ·</text>
</svg>
`;
}
/**
* 脚本入口:遍历景点生成占位照片。
*
* @returns {Promise<void>} 完成后输出汇总。
*/
async function main() {
const force = process.argv.includes('--force');
mkdirSync(OUT_DIR, { recursive: true });
let created = 0;
let skipped = 0;
for (const spot of SPOTS) {
spot.photos.forEach((photo, index) => {
const fileName = photo.src.split('/').pop();
const outPath = resolve(OUT_DIR, fileName);
if (!force && existsSync(outPath)) {
skipped += 1;
return;
}
writeFileSync(outPath, buildSvg(spot, photo), 'utf8');
created += 1;
console.log(`[photos] 生成 ${fileName}`);
});
}
console.log(`[photos] 完成:新建 ${created} 张,跳过已存在 ${skipped} 张`);
}
main().catch((err) => {
console.error('[photos] 生成失败:', err);
process.exit(1);
});