This commit is contained in:
taojiacheng committed 2025-11-29 13:34:41 +08:00
1 parent e0254f4ef3
commit b9f18fda39
37 files changed
+1611 -1020

No files matched your search

+6 -6
View File
@@ -1,8 +1,8 @@
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"printWidth": 100,
"trailingComma": "es5",
"plugins": ["prettier-plugin-tailwindcss"]
"printWidth": 80,
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "never",
"overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
"plugins": ["prettier-plugin-tailwindcss","prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
}
-1
View File
@@ -36,4 +36,3 @@ OPENAI_MODEL=gpt-4o-mini
- `.env.local` 文件不会被提交到 Git(已在 .gitignore 中)
- 修改环境变量后需要重启开发服务器
- 生产环境需要在部署平台(如 Vercel)配置环境变量
+17 -18
View File
@@ -81,14 +81,14 @@
## 🚀 MVP 开发路线图(建议 2 周内上线)
| 阶段 | 目标 | 时间 |
| --------------- | -------------------------------------------------------- | ---- |
| **第 1 天** | 确定核心数据结构(角色、装备、战力公式) | ✅ |
| **第 2-3 天** | 实现 AI 角色生成 + 前端展示页面(纯静态) | ⏳ |
| **第 4-5 天** | 加入装备/神通生成 + 本地存储(localStorage) | ⏳ |
| **第 6-7 天** | 实现战力计算 + 模拟对战 + 战斗文本生成 | ⏳ |
| **第 8-10 天** | 接入后端(如 Supabase/Firebase)→ 支持用户登录、保存角色 | ⏳ |
| **第 11-14 天** | 上线排行榜 + 挑战功能 + 部署到 Vercel/Render | 🚀 |
| 阶段 | 目标 | 时间 |
| --- | --- | --- |
| **第 1 天** | 确定核心数据结构(角色、装备、战力公式) | ✅ |
| **第 2-3 天** | 实现 AI 角色生成 + 前端展示页面(纯静态) | ⏳ |
| **第 4-5 天** | 加入装备/神通生成 + 本地存储(localStorage) | ⏳ |
| **第 6-7 天** | 实现战力计算 + 模拟对战 + 战斗文本生成 | ⏳ |
| **第 8-10 天** | 接入后端(如 Supabase/Firebase)→ 支持用户登录、保存角色 | ⏳ |
| **第 11-14 天** | 上线排行榜 + 挑战功能 + 部署到 Vercel/Render | 🚀 |
> 💡 初期甚至可以不用用户系统:用“角色 ID 链接”分享(如 `/battle?char=abc123`),降低门槛。
@@ -183,7 +183,6 @@ interface Cultivator {
### 2. **战力计算机制(简单但有趣)**
- `basePower` 由境界决定:
- 炼气:1~100
- 筑基:101~300
- 金丹:301~600
@@ -225,15 +224,15 @@ interface Cultivator {
## 🛠 第三阶段:开发路线图(按天拆解)
| 天数 | 目标 | 输出物 |
| ---------- | --------------------------------------------------- | -------------------------------- |
| **Day 1** | 定义数据结构 + 写好 AI 角色生成 prompt + 测试调用 | 能输入文字 → 输出结构化角色 JSON |
| **Day 2** | 实现前端页面:输入框 + “生成角色”按钮 + 展示角色卡 | 静态页面,数据存在 useState |
| **Day 3** | 实现“挑战”按钮:调用战斗播报 API,显示小说式结果 | 能看到一段热血战斗描写! |
| **Day 4** | 加入本地“排行榜”:保存 5 个最强角色(localStorage) | 显示 Top 5,可点击挑战 |
| **Day 5** | 优化 UI:加修仙风格(字体、背景、音效?) | 更沉浸 |
| **Day 6** | 部署到 Vercel(免费) | 生成一个可分享的链接! |
| **Day 7+** | 收集反馈 → 迭代(加装备?加用户系统?) | 进入正向循环 |
| 天数 | 目标 | 输出物 |
| --- | --- | --- |
| **Day 1** | 定义数据结构 + 写好 AI 角色生成 prompt + 测试调用 | 能输入文字 → 输出结构化角色 JSON |
| **Day 2** | 实现前端页面:输入框 + “生成角色”按钮 + 展示角色卡 | 静态页面,数据存在 useState |
| **Day 3** | 实现“挑战”按钮:调用战斗播报 API,显示小说式结果 | 能看到一段热血战斗描写! |
| **Day 4** | 加入本地“排行榜”:保存 5 个最强角色(localStorage) | 显示 Top 5,可点击挑战 |
| **Day 5** | 优化 UI:加修仙风格(字体、背景、音效?) | 更沉浸 |
| **Day 6** | 部署到 Vercel(免费) | 生成一个可分享的链接! |
| **Day 7+** | 收集反馈 → 迭代(加装备?加用户系统?) | 进入正向循环 |
---
+42 -42
View File
@@ -1,12 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import {
createCultivator,
getCultivatorsByUserId,
getCultivatorById,
deleteCultivator,
} from "@/lib/repositories/cultivatorRepository";
import type { Cultivator, BattleProfile } from "@/types/cultivator";
import { createClient } from "@/lib/supabase/server";
getCultivatorById,
getCultivatorsByUserId,
} from '@/lib/repositories/cultivatorRepository';
import { createClient } from '@/lib/supabase/server';
import type { BattleProfile, Cultivator } from '@/types/cultivator';
import { NextRequest, NextResponse } from 'next/server';
/**
* POST /api/cultivators
@@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
} = await supabase.auth.getSession();
if (sessionError || !session) {
return NextResponse.json({ error: "未授权访问" }, { status: 401 });
return NextResponse.json({ error: '未授权访问' }, { status: 401 });
}
const body = await request.json();
@@ -33,17 +33,17 @@ export async function POST(request: NextRequest) {
// 验证输入数据完整性
if (
!cultivatorData ||
typeof cultivatorData !== "object" ||
typeof cultivatorData !== 'object' ||
!cultivatorData.name ||
!cultivatorData.prompt ||
!battleProfile ||
typeof battleProfile !== "object" ||
typeof battleProfile !== 'object' ||
!battleProfile.attributes ||
!battleProfile.skills
) {
return NextResponse.json(
{ error: "请提供完整的角色数据" },
{ status: 400 }
{ error: '请提供完整的角色数据' },
{ status: 400 },
);
}
@@ -53,29 +53,29 @@ export async function POST(request: NextRequest) {
battleProfile.skills.length === 0
) {
return NextResponse.json(
{ error: "角色必须至少有一个技能" },
{ status: 400 }
{ error: '角色必须至少有一个技能' },
{ status: 400 },
);
}
// 验证战斗属性数据
if (
typeof battleProfile.attributes.vitality !== "number" ||
typeof battleProfile.attributes.spirit !== "number" ||
typeof battleProfile.attributes.wisdom !== "number" ||
typeof battleProfile.attributes.speed !== "number"
typeof battleProfile.attributes.vitality !== 'number' ||
typeof battleProfile.attributes.spirit !== 'number' ||
typeof battleProfile.attributes.wisdom !== 'number' ||
typeof battleProfile.attributes.speed !== 'number'
) {
return NextResponse.json(
{ error: "请提供有效的战斗属性" },
{ status: 400 }
{ error: '请提供有效的战斗属性' },
{ status: 400 },
);
}
// 使用事务创建角色,确保数据一致性
const createdCultivator = await createCultivator(
session.user.id,
cultivatorData as Omit<Cultivator, "id" | "battleProfile">,
battleProfile as BattleProfile
cultivatorData as Omit<Cultivator, 'id' | 'battleProfile'>,
battleProfile as BattleProfile,
);
return NextResponse.json({
@@ -83,15 +83,15 @@ export async function POST(request: NextRequest) {
data: createdCultivator,
});
} catch (error) {
console.error("创建角色 API 错误:", error);
console.error('创建角色 API 错误:', error);
// 安全处理错误信息,避免泄露敏感信息
const errorMessage =
process.env.NODE_ENV === "development"
process.env.NODE_ENV === 'development'
? error instanceof Error
? error.message
: "创建角色失败,请稍后重试"
: "创建角色失败,请稍后重试";
: '创建角色失败,请稍后重试'
: '创建角色失败,请稍后重试';
return NextResponse.json({ error: errorMessage }, { status: 500 });
}
@@ -113,19 +113,19 @@ export async function GET(request: NextRequest) {
} = await supabase.auth.getSession();
if (sessionError || !session) {
return NextResponse.json({ error: "未授权访问" }, { status: 401 });
return NextResponse.json({ error: '未授权访问' }, { status: 401 });
}
// 获取角色ID参数(可选)
const searchParams = request.nextUrl.searchParams;
const cultivatorId = searchParams.get("id");
const cultivatorId = searchParams.get('id');
if (cultivatorId) {
// 获取单个角色
const cultivator = await getCultivatorById(session.user.id, cultivatorId);
if (!cultivator) {
return NextResponse.json({ error: "角色不存在" }, { status: 404 });
return NextResponse.json({ error: '角色不存在' }, { status: 404 });
}
return NextResponse.json({
@@ -142,15 +142,15 @@ export async function GET(request: NextRequest) {
});
}
} catch (error) {
console.error("获取角色 API 错误:", error);
console.error('获取角色 API 错误:', error);
// 安全处理错误信息,避免泄露敏感信息
const errorMessage =
process.env.NODE_ENV === "development"
process.env.NODE_ENV === 'development'
? error instanceof Error
? error.message
: "获取角色失败,请稍后重试"
: "获取角色失败,请稍后重试";
: '获取角色失败,请稍后重试'
: '获取角色失败,请稍后重试';
return NextResponse.json({ error: errorMessage }, { status: 500 });
}
@@ -172,15 +172,15 @@ export async function DELETE(request: NextRequest) {
} = await supabase.auth.getSession();
if (sessionError || !session) {
return NextResponse.json({ error: "未授权访问" }, { status: 401 });
return NextResponse.json({ error: '未授权访问' }, { status: 401 });
}
// 获取角色ID参数
const searchParams = request.nextUrl.searchParams;
const cultivatorId = searchParams.get("id");
const cultivatorId = searchParams.get('id');
if (!cultivatorId) {
return NextResponse.json({ error: "请提供角色ID" }, { status: 400 });
return NextResponse.json({ error: '请提供角色ID' }, { status: 400 });
}
// 删除角色
@@ -188,25 +188,25 @@ export async function DELETE(request: NextRequest) {
if (!success) {
return NextResponse.json(
{ error: "删除角色失败或角色不存在" },
{ status: 404 }
{ error: '删除角色失败或角色不存在' },
{ status: 404 },
);
}
return NextResponse.json({
success: true,
message: "角色删除成功",
message: '角色删除成功',
});
} catch (error) {
console.error("删除角色 API 错误:", error);
console.error('删除角色 API 错误:', error);
// 安全处理错误信息,避免泄露敏感信息
const errorMessage =
process.env.NODE_ENV === "development"
process.env.NODE_ENV === 'development'
? error instanceof Error
? error.message
: "删除角色失败,请稍后重试"
: "删除角色失败,请稍后重试";
: '删除角色失败,请稍后重试'
: '删除角色失败,请稍后重试';
return NextResponse.json({ error: errorMessage }, { status: 500 });
}
+27 -27
View File
@@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { generateBattleReportStream } from "../../../utils/aiClient";
import { getBattleReportPrompt } from "../../../utils/prompts";
import type { Cultivator } from "../../../types/cultivator";
import { createClient } from "@/lib/supabase/server";
import { createClient } from '@/lib/supabase/server';
import { NextRequest, NextResponse } from 'next/server';
import type { Cultivator } from '../../../types/cultivator';
import { generateBattleReportStream } from '../../../utils/aiClient';
import { getBattleReportPrompt } from '../../../utils/prompts';
/**
* POST /api/generate-battle-report
@@ -20,7 +20,7 @@ export async function POST(request: NextRequest) {
} = await supabase.auth.getSession();
if (sessionError || !session) {
return NextResponse.json({ error: "未授权访问" }, { status: 401 });
return NextResponse.json({ error: '未授权访问' }, { status: 401 });
}
const body = await request.json();
@@ -29,23 +29,23 @@ export async function POST(request: NextRequest) {
// 增强输入验证
if (
!player ||
typeof player !== "object" ||
typeof player !== 'object' ||
!player.id ||
!player.name ||
!opponent ||
typeof opponent !== "object" ||
typeof opponent !== 'object' ||
!opponent.id ||
!opponent.name ||
!battleSummary ||
typeof battleSummary !== "object" ||
typeof battleSummary !== 'object' ||
!Array.isArray(battleSummary.log)
) {
return new Response(
JSON.stringify({ error: "请提供完整的角色与战斗日志信息" }),
JSON.stringify({ error: '请提供完整的角色与战斗日志信息' }),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
headers: { 'Content-Type': 'application/json' },
},
);
}
@@ -81,25 +81,25 @@ export async function POST(request: NextRequest) {
userPrompt,
(chunk: string) => {
// 发送内容块
const data = JSON.stringify({ type: "chunk", content: chunk });
const data = JSON.stringify({ type: 'chunk', content: chunk });
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
}
},
);
// 发送结束标记
controller.enqueue(encoder.encode('data: {"type":"done"}\n\n'));
controller.close();
} catch (error) {
console.error("生成战斗播报流式输出错误:", error);
console.error('生成战斗播报流式输出错误:', error);
// 安全处理错误信息
const errorMessage =
process.env.NODE_ENV === "development"
process.env.NODE_ENV === 'development'
? error instanceof Error
? error.message
: "生成战斗播报失败"
: "生成战斗播报失败";
: '生成战斗播报失败'
: '生成战斗播报失败';
const errorData = JSON.stringify({
type: "error",
type: 'error',
error: errorMessage,
});
controller.enqueue(encoder.encode(`data: ${errorData}\n\n`));
@@ -110,25 +110,25 @@ export async function POST(request: NextRequest) {
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
} catch (error) {
console.error("生成战斗播报 API 错误:", error);
console.error('生成战斗播报 API 错误:', error);
// 安全处理错误信息
const errorMessage =
process.env.NODE_ENV === "development"
process.env.NODE_ENV === 'development'
? error instanceof Error
? error.message
: "生成战斗播报失败,请稍后重试"
: "生成战斗播报失败,请稍后重试";
: '生成战斗播报失败,请稍后重试'
: '生成战斗播报失败,请稍后重试';
return new Response(JSON.stringify({ error: errorMessage }), {
status: 500,
headers: { "Content-Type": "application/json" },
headers: { 'Content-Type': 'application/json' },
});
}
}
+12 -12
View File
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { generateCharacter } from "../../../utils/aiClient";
import { getCharacterGenerationPrompt } from "../../../utils/prompts";
import { createClient } from "@/lib/supabase/server";
import { createClient } from '@/lib/supabase/server';
import { NextRequest, NextResponse } from 'next/server';
import { generateCharacter } from '../../../utils/aiClient';
import { getCharacterGenerationPrompt } from '../../../utils/prompts';
/**
* POST /api/generate-character
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
} = await supabase.auth.getSession();
if (sessionError || !session) {
return NextResponse.json({ error: "未授权访问" }, { status: 401 });
return NextResponse.json({ error: '未授权访问' }, { status: 401 });
}
const body = await request.json();
@@ -28,12 +28,12 @@ export async function POST(request: NextRequest) {
// 输入验证
if (
!userInput ||
typeof userInput !== "string" ||
typeof userInput !== 'string' ||
userInput.trim().length < 5
) {
return NextResponse.json(
{ error: "请提供至少5个字符的角色描述" },
{ status: 400 }
{ error: '请提供至少5个字符的角色描述' },
{ status: 400 },
);
}
@@ -48,15 +48,15 @@ export async function POST(request: NextRequest) {
data: aiResponse,
});
} catch (error) {
console.error("生成角色 API 错误:", error);
console.error('生成角色 API 错误:', error);
// 安全处理错误信息,避免泄露敏感信息
const errorMessage =
process.env.NODE_ENV === "development"
process.env.NODE_ENV === 'development'
? error instanceof Error
? error.message
: "生成角色失败,请稍后重试"
: "生成角色失败,请稍后重试";
: '生成角色失败,请稍后重试'
: '生成角色失败,请稍后重试';
return NextResponse.json({ error: errorMessage }, { status: 500 });
}
+43 -43
View File
@@ -1,12 +1,12 @@
"use client";
'use client';
import { useState, useEffect, Suspense } from "react";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import type { Cultivator } from "@/types/cultivator";
import { getDefaultBoss } from "@/utils/prompts";
import { mockRankings } from "@/data/mockRankings";
import { simulateBattle, BattleEngineResult } from "@/engine/battleEngine";
import { mockRankings } from '@/data/mockRankings';
import { BattleEngineResult, simulateBattle } from '@/engine/battleEngine';
import type { Cultivator } from '@/types/cultivator';
import { getDefaultBoss } from '@/utils/prompts';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { Suspense, useEffect, useState } from 'react';
/**
* 对战播报页内容组件
@@ -16,26 +16,26 @@ function BattlePageContent() {
const [player, setPlayer] = useState<Cultivator | null>(null);
const [opponent, setOpponent] = useState<Cultivator | null>(null);
const [battleResult, setBattleResult] = useState<BattleEngineResult | null>(
null
null,
);
const [streamingReport, setStreamingReport] = useState<string>("");
const [finalReport, setFinalReport] = useState<string>(""); // 保存最终的完整播报
const [streamingReport, setStreamingReport] = useState<string>('');
const [finalReport, setFinalReport] = useState<string>(''); // 保存最终的完整播报
const [isStreaming, setIsStreaming] = useState(false);
const [loading, setLoading] = useState(false);
// 初始化
useEffect(() => {
const playerData = sessionStorage.getItem("player");
const playerData = sessionStorage.getItem('player');
if (playerData) {
try {
const playerObj = JSON.parse(playerData) as Cultivator;
setPlayer(playerObj);
} catch (e) {
console.error("解析玩家数据失败:", e);
console.error('解析玩家数据失败:', e);
}
}
const opponentId = searchParams.get("opponent");
const opponentId = searchParams.get('opponent');
if (opponentId) {
const foundOpponent = mockRankings.find((c) => c.id === opponentId);
if (foundOpponent) {
@@ -64,19 +64,19 @@ function BattlePageContent() {
setLoading(true);
setIsStreaming(true);
setStreamingReport("");
setFinalReport(""); // 清空最终播报
setStreamingReport('');
setFinalReport(''); // 清空最终播报
setBattleResult(null);
try {
const result = simulateBattle(player, opponent);
console.log("战斗结果:", result);
console.log('战斗结果:', result);
setBattleResult(result);
const response = await fetch("/api/generate-battle-report", {
method: "POST",
const response = await fetch('/api/generate-battle-report', {
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify({
player,
@@ -94,18 +94,18 @@ function BattlePageContent() {
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "生成战斗播报失败");
throw new Error(errorData.error || '生成战斗播报失败');
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let buffer = "";
let buffer = '';
if (!reader) {
throw new Error("无法读取响应流");
throw new Error('无法读取响应流');
}
let fullReport = "";
let fullReport = '';
while (true) {
const { done, value } = await reader.read();
@@ -115,37 +115,37 @@ function BattlePageContent() {
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith("data: ")) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === "chunk") {
if (data.type === 'chunk') {
fullReport += data.content;
setStreamingReport(fullReport);
} else if (data.type === "done") {
} else if (data.type === 'done') {
setIsStreaming(false);
// 保存完整的播报内容,不清空
setFinalReport(fullReport);
setStreamingReport(fullReport);
} else if (data.type === "error") {
throw new Error(data.error || "生成战斗播报失败");
} else if (data.type === 'error') {
throw new Error(data.error || '生成战斗播报失败');
}
} catch (e) {
console.error("解析 SSE 数据失败:", e);
console.error('解析 SSE 数据失败:', e);
}
}
}
}
} catch (error) {
console.error("战斗失败:", error);
console.error('战斗失败:', error);
setIsStreaming(false);
setStreamingReport("");
setFinalReport("");
alert(error instanceof Error ? error.message : "战斗失败");
setStreamingReport('');
setFinalReport('');
alert(error instanceof Error ? error.message : '战斗失败');
} finally {
setLoading(false);
}
@@ -154,8 +154,8 @@ function BattlePageContent() {
// 再战一次
const handleBattleAgain = () => {
setBattleResult(null);
setStreamingReport("");
setFinalReport("");
setStreamingReport('');
setFinalReport('');
setIsStreaming(false);
handleBattle();
};
@@ -178,7 +178,7 @@ function BattlePageContent() {
const displayReport =
streamingReport ||
finalReport ||
(battleResult ? `${battleResult.winner.name} 获胜!` : "");
(battleResult ? `${battleResult.winner.name} 获胜!` : '');
return (
<div className="bg-paper min-h-screen p-4">
@@ -221,13 +221,13 @@ function BattlePageContent() {
{/* 播报内容 */}
<div className="battle-report text-ink leading-relaxed text-center pl-4">
{displayReport
.split("\n")
.filter((line) => line.trim() !== "")
.split('\n')
.filter((line) => line.trim() !== '')
.map((line, index) => (
<p key={index} className="mb-2 whitespace-pre-line">
<span dangerouslySetInnerHTML={{ __html: line }} />
{isStreaming &&
index === displayReport.split("\n").length - 2 && (
index === displayReport.split('\n').length - 2 && (
<span className="inline-block ml-1 animate-pulse text-crimson">
▊
</span>
@@ -263,7 +263,7 @@ function BattlePageContent() {
</Link>
<button
onClick={() => {
alert("分享功能开发中...");
alert('分享功能开发中...');
}}
className="btn-outline flex items-center"
>
+35 -34
View File
@@ -1,19 +1,19 @@
"use client";
'use client';
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import type { Cultivator } from "@/types/cultivator";
import { createCultivatorFromAI } from "@/utils/cultivatorUtils";
import {
AlchemyFurnaceIcon,
InkstoneIcon,
ScrollIcon,
} from "@/components/SVGIcon";
import { useAuth } from "@/lib/auth/AuthContext";
} from '@/components/SVGIcon';
import { useAuth } from '@/lib/auth/AuthContext';
import type { Cultivator } from '@/types/cultivator';
import { createCultivatorFromAI } from '@/utils/cultivatorUtils';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
const getCombatRating = (cultivator: Cultivator | null): string => {
if (!cultivator?.battleProfile) return "--";
if (!cultivator?.battleProfile) return '--';
const { vitality, spirit, wisdom, speed } =
cultivator.battleProfile.attributes;
return Math.round((vitality + spirit + wisdom + speed) / 4).toString();
@@ -25,7 +25,7 @@ const getCombatRating = (cultivator: Cultivator | null): string => {
export default function CreatePage() {
const router = useRouter();
const { user } = useAuth();
const [userPrompt, setUserPrompt] = useState("");
const [userPrompt, setUserPrompt] = useState('');
const [loading, setLoading] = useState(false);
const [player, setPlayer] = useState<Cultivator | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -33,7 +33,7 @@ export default function CreatePage() {
// 生成角色
const handleGenerateCharacter = async () => {
if (!userPrompt.trim()) {
setError("请输入角色描述");
setError('请输入角色描述');
return;
}
@@ -43,10 +43,10 @@ export default function CreatePage() {
try {
// 调用AI生成角色
const aiResponse = await fetch("/api/generate-character", {
method: "POST",
const aiResponse = await fetch('/api/generate-character', {
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify({ userInput: userPrompt }),
});
@@ -54,34 +54,34 @@ export default function CreatePage() {
const aiResult = await aiResponse.json();
if (!aiResponse.ok || !aiResult.success) {
throw new Error(aiResult.error || "生成角色失败");
throw new Error(aiResult.error || '生成角色失败');
}
// 解析AI响应,创建角色对象
const aiData = aiResult.data;
const cultivator = createCultivatorFromAI(aiData, userPrompt);
console.log("cultivator", cultivator);
console.log('cultivator', cultivator);
// 保存角色到数据库
if (user) {
const saveResponse = await fetch("/api/cultivators", {
method: "POST",
const saveResponse = await fetch('/api/cultivators', {
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify({
cultivatorData: {
...cultivator,
battleProfile: undefined,
},
battleProfile: cultivator.battleProfile!
battleProfile: cultivator.battleProfile!,
}),
});
const saveResult = await saveResponse.json();
if (!saveResponse.ok || !saveResult.success) {
throw new Error(saveResult.error || "保存角色失败");
throw new Error(saveResult.error || '保存角色失败');
}
setPlayer(saveResult.data);
@@ -90,8 +90,9 @@ export default function CreatePage() {
setPlayer(cultivator);
}
} catch (error) {
console.error("生成角色失败:", error);
const errorMessage = error instanceof Error ? error.message : "生成角色失败,请检查控制台";
console.error('生成角色失败:', error);
const errorMessage =
error instanceof Error ? error.message : '生成角色失败,请检查控制台';
setError(errorMessage);
} finally {
setLoading(false);
@@ -101,8 +102,8 @@ export default function CreatePage() {
// 立即挑战
const handleChallenge = () => {
if (player) {
sessionStorage.setItem("player", JSON.stringify(player));
router.push("/battle");
sessionStorage.setItem('player', JSON.stringify(player));
router.push('/battle');
}
};
@@ -133,7 +134,7 @@ export default function CreatePage() {
value={userPrompt}
onChange={(e) => setUserPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
handleGenerateCharacter();
}
}}
@@ -201,7 +202,7 @@ export default function CreatePage() {
<div>
<span className="text-ink/70">元素:</span>
<span className="text-ink font-semibold ml-1">
{player.battleProfile?.element || "无"}
{player.battleProfile?.element || '无'}
</span>
</div>
<div>
@@ -284,7 +285,7 @@ export default function CreatePage() {
</p>
<p className="text-ink/80">
威力:{skill.power} | 效果:
{skill.effects?.join(", ") || "无"}
{skill.effects?.join(', ') || '无'}
</p>
</div>
))}
@@ -308,21 +309,21 @@ export default function CreatePage() {
{eq.bonus &&
Object.entries(eq.bonus)
.map(([key, value]) => {
if (key === "elementBoost") {
if (key === 'elementBoost') {
return `${Object.entries(
value as Record<string, number>
value as Record<string, number>,
)
.map(
([elem, boost]) =>
`${elem}系技能威力+${(
boost * 100
).toFixed(0)}%`
).toFixed(0)}%`,
)
.join(", ")}`;
.join(', ')}`;
}
return `${key} +${value}`;
})
.join(", ")}
.join(', ')}
</p>
</div>
))}
+38 -20
View File
@@ -4,12 +4,13 @@ import { useState } from 'react';
import type { Cultivator } from '../../types/cultivator';
import { getDefaultBoss } from '../../utils/prompts';
// AI 调用已移至 API 路由
import { BattleEngineResult, simulateBattle } from '../../engine/battleEngine';
import { createCultivatorFromAI } from '../../utils/cultivatorUtils';
import { simulateBattle, BattleEngineResult } from '../../engine/battleEngine';
const getCombatRating = (cultivator: Cultivator | null): string => {
if (!cultivator?.battleProfile) return '--';
const { vitality, spirit, wisdom, speed } = cultivator.battleProfile.attributes;
const { vitality, spirit, wisdom, speed } =
cultivator.battleProfile.attributes;
return Math.round((vitality + spirit + wisdom + speed) / 4).toString();
};
@@ -24,7 +25,9 @@ export default function DemoPage() {
const [userPrompt, setUserPrompt] = useState('');
const [loading, setLoading] = useState(false);
const [player, setPlayer] = useState<Cultivator | null>(null);
const [battleResult, setBattleResult] = useState<BattleEngineResult | null>(null);
const [battleResult, setBattleResult] = useState<BattleEngineResult | null>(
null,
);
const [streamingReport, setStreamingReport] = useState<string>(''); // 流式生成的内容
const [isStreaming, setIsStreaming] = useState(false); // 是否正在流式生成
const [finalReport, setFinalReport] = useState<string>(''); // 最终战报内容
@@ -64,7 +67,8 @@ export default function DemoPage() {
setBattleResult(null);
} catch (error) {
console.error('生成角色失败:', error);
const errorMessage = error instanceof Error ? error.message : '生成角色失败,请检查控制台';
const errorMessage =
error instanceof Error ? error.message : '生成角色失败,请检查控制台';
alert(errorMessage);
} finally {
setLoading(false);
@@ -133,14 +137,14 @@ export default function DemoPage() {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
// 解码数据
buffer += decoder.decode(value, { stream: true });
// 处理完整的 SSE 消息
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 保留不完整的行
@@ -149,7 +153,7 @@ export default function DemoPage() {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'chunk') {
// 接收到内容块,实时更新 UI
fullReport += data.content;
@@ -171,10 +175,11 @@ export default function DemoPage() {
}
} catch (error) {
console.error('战斗失败:', error);
setIsStreaming(false);
setStreamingReport('');
setFinalReport('');
const errorMessage = error instanceof Error ? error.message : '战斗失败,请检查控制台';
setIsStreaming(false);
setStreamingReport('');
setFinalReport('');
const errorMessage =
error instanceof Error ? error.message : '战斗失败,请检查控制台';
alert(errorMessage);
} finally {
setLoading(false);
@@ -193,7 +198,9 @@ export default function DemoPage() {
{/* 角色生成区域 */}
<div className="bg-white/10 backdrop-blur-lg rounded-lg p-6 mb-6">
<h2 className="text-2xl font-semibold text-white mb-4">1. 生成角色</h2>
<h2 className="text-2xl font-semibold text-white mb-4">
1. 生成角色
</h2>
<div className="space-y-4">
<textarea
value={userPrompt}
@@ -242,12 +249,17 @@ export default function DemoPage() {
<p className="text-blue-200">先天气运</p>
<div className="mt-2 space-y-1 text-sm">
{player.preHeavenFates.map((fate, idx) => (
<div key={fate.name + idx} className="bg-white/5 p-2 rounded border border-white/10">
<div
key={fate.name + idx}
className="bg-white/5 p-2 rounded border border-white/10"
>
<p className="font-semibold text-white">
{fate.name} · {fate.type}
</p>
<p className="text-white/80">{fate.effect}</p>
<p className="text-white/60 text-xs italic">{fate.description}</p>
<p className="text-white/60 text-xs italic">
{fate.description}
</p>
</div>
))}
</div>
@@ -283,7 +295,9 @@ export default function DemoPage() {
</div>
<div>
<p className="text-blue-200">战力评估</p>
<p className="text-2xl font-bold text-orange-300">{getCombatRating(player)}</p>
<p className="text-2xl font-bold text-orange-300">
{getCombatRating(player)}
</p>
</div>
</div>
</div>
@@ -308,20 +322,26 @@ export default function DemoPage() {
<div className="flex items-center justify-between p-4 bg-green-500/20 rounded-lg">
<div>
<p className="text-green-200">胜利者</p>
<p className="text-2xl font-bold">{battleResult.winner.name}</p>
<p className="text-2xl font-bold">
{battleResult.winner.name}
</p>
<p className="text-sm text-green-300">
战力评估: {getCombatRating(battleResult.winner)}
</p>
</div>
{battleResult.triggeredMiracle && (
<div className="px-4 py-2 bg-yellow-500/30 rounded-lg">
<p className="text-yellow-200 font-bold">✨ 触发顿悟!</p>
<p className="text-yellow-200 font-bold">
✨ 触发顿悟!
</p>
</div>
)}
</div>
<div className="p-4 bg-red-500/20 rounded-lg">
<p className="text-red-200">失败者</p>
<p className="text-xl font-bold">{battleResult.loser.name}</p>
<p className="text-xl font-bold">
{battleResult.loser.name}
</p>
<p className="text-sm text-red-300">
战力评估: {getCombatRating(battleResult.loser)}
</p>
@@ -368,5 +388,3 @@ export default function DemoPage() {
</div>
);
}
+13 -7
View File
@@ -1,4 +1,4 @@
@import "tailwindcss";
@import 'tailwindcss';
/* 引入本地字体 */
@font-face {
@@ -25,7 +25,7 @@
--color-crimson: #c1121f; /* 朱砂红 */
--color-teal: #4a7c59; /* 青黛 */
--color-wood: #8b4513; /* 木色 */
/* 字体变量 */
--font-zcool-xiao-wei: 'ZCOOL XiaoWei', serif;
--font-ma-shan-zheng: 'Ma Shan Zheng', cursive;
@@ -45,21 +45,27 @@ body {
}
/* 标题字体样式 */
h1, h2, h3, h4, h5, h6, .font-ma-shan-zheng {
h1,
h2,
h3,
h4,
h5,
h6,
.font-ma-shan-zheng {
font-family: var(--font-ma-shan-zheng), Arial, Helvetica, sans-serif;
}
/* 宣纸背景 */
.bg-paper {
background-color: var(--color-paper);
background-image: url("/assets/paper.png");
background-image: url('/assets/paper.png');
background-size: 300px;
min-height: 100vh;
}
.bg-paper-light {
background-color: rgba(248, 243, 230, 0.8);
background-image: url("/assets/paper.png");
background-image: url('/assets/paper.png');
background-size: 300px;
}
@@ -97,7 +103,7 @@ h1, h2, h3, h4, h5, h6, .font-ma-shan-zheng {
transition: all 0.3s ease;
border: 2px solid var(--color-ink);
cursor: pointer;
}
}
.btn-outline:hover {
background-color: var(--color-ink);
@@ -112,7 +118,7 @@ h1, h2, h3, h4, h5, h6, .font-ma-shan-zheng {
.character-scroll::before,
.character-scroll::after {
content: "";
content: '';
display: block;
height: 20px;
background: var(--color-wood);
+6 -8
View File
@@ -1,11 +1,11 @@
import type { Metadata } from "next";
import "./globals.css";
import { AuthProvider } from '../lib/auth/AuthContext';
import type { Metadata } from 'next';
import AnonymousUserManager from '../components/AnonymousUserManager';
import { AuthProvider } from '../lib/auth/AuthContext';
import './globals.css';
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
@@ -15,9 +15,7 @@ export default function RootLayout({
}>) {
return (
<html lang="zh-CN">
<body
className="antialiased"
>
<body className="antialiased">
<AuthProvider>
<AnonymousUserManager />
{children}
+10 -4
View File
@@ -1,9 +1,13 @@
'use client';
import Link from 'next/link';
import { AlchemyFurnaceIcon, CloudDivider, DragonPatternIcon } from '@/components/SVGIcon';
import {
AlchemyFurnaceIcon,
CloudDivider,
DragonPatternIcon,
} from '@/components/SVGIcon';
import { mockRankings } from '@/data/mockRankings';
import type { Cultivator } from '@/types/cultivator';
import Link from 'next/link';
const getCombatRating = (cultivator: Cultivator): string => {
const profile = cultivator.battleProfile;
@@ -58,12 +62,14 @@ export default function HomePage() {
<span className="text-sm text-ink/80 flex flex-col text-right">
<span>{cultivator.cultivationLevel}</span>
<span className="text-ink/60">
灵根 {cultivator.spiritRoot} · 战力 {getCombatRating(cultivator)}
灵根 {cultivator.spiritRoot} · 战力{' '}
{getCombatRating(cultivator)}
</span>
</span>
</div>
<div className="text-xs text-ink/60 mt-1">
{cultivator.preHeavenFates?.map((f) => f.name).join('|') || '——'}
{cultivator.preHeavenFates?.map((f) => f.name).join('|') ||
'——'}
</div>
<Link
href={`/battle?opponent=${cultivator.id}`}
+3 -3
View File
@@ -1,7 +1,7 @@
"use client";
'use client';
import { useEffect } from "react";
import { useAuth } from "../lib/auth/AuthContext";
import { useEffect } from 'react';
import { useAuth } from '../lib/auth/AuthContext';
const AnonymousUserManager = () => {
const { user, isLoading, createAnonymousUser } = useAuth();
+6 -3
View File
@@ -54,7 +54,9 @@ export default function CultivatorCard({
<h3 className="text-xl font-bold text-[#e0c5a3] truncate">
{cultivator.name}
</h3>
<p className="text-sm text-[#4cc9f0]">{cultivator.cultivationLevel}</p>
<p className="text-sm text-[#4cc9f0]">
{cultivator.cultivationLevel}
</p>
</div>
<div className="mb-2 space-y-1">
@@ -64,7 +66,9 @@ export default function CultivatorCard({
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-[#e0c5a3]/70">战力评估:</span>
<span className="font-bold text-[#c1121f]">{getCombatRating(cultivator)}</span>
<span className="font-bold text-[#c1121f]">
{getCombatRating(cultivator)}
</span>
</div>
</div>
@@ -96,4 +100,3 @@ export default function CultivatorCard({
</div>
);
}
+3 -2
View File
@@ -45,7 +45,9 @@ export default function RankingList({
{/* Top 5 */}
{topList.length > 0 && (
<div>
<h2 className="mb-3 text-lg font-semibold text-[#e0c5a3]">排行榜 Top 5</h2>
<h2 className="mb-3 text-lg font-semibold text-[#e0c5a3]">
排行榜 Top 5
</h2>
<div className="space-y-3">
{topList.map((cultivator, idx) => (
<CultivatorCard
@@ -61,4 +63,3 @@ export default function RankingList({
</div>
);
}
+552 -117
View File
@@ -6,24 +6,84 @@
/**
* 丹炉图标(传统三足鼎样式,带冒烟动画)
*/
export function AlchemyFurnaceIcon({ className = "w-6 h-6" }: { className?: string }) {
export function AlchemyFurnaceIcon({
className = 'w-6 h-6',
}: {
className?: string;
}) {
return (
<svg className={className} viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* 炉体 */}
<ellipse cx="32" cy="40" rx="18" ry="12" fill="#8b4513" stroke="#654321" strokeWidth="1.5"/>
<ellipse cx="32" cy="36" rx="14" ry="8" fill="#a0522d"/>
<ellipse
cx="32"
cy="40"
rx="18"
ry="12"
fill="#8b4513"
stroke="#654321"
strokeWidth="1.5"
/>
<ellipse cx="32" cy="36" rx="14" ry="8" fill="#a0522d" />
{/* 炉口 */}
<ellipse cx="32" cy="28" rx="12" ry="6" fill="#654321"/>
<ellipse cx="32" cy="26" rx="10" ry="4" fill="#2c1810" opacity="0.6"/>
<ellipse cx="32" cy="28" rx="12" ry="6" fill="#654321" />
<ellipse cx="32" cy="26" rx="10" ry="4" fill="#2c1810" opacity="0.6" />
{/* 三足 */}
<path d="M 20 52 L 16 64 M 32 52 L 32 64 M 44 52 L 48 64" stroke="#654321" strokeWidth="2.5" strokeLinecap="round"/>
<path
d="M 20 52 L 16 64 M 32 52 L 32 64 M 44 52 L 48 64"
stroke="#654321"
strokeWidth="2.5"
strokeLinecap="round"
/>
{/* 炉耳 */}
<path d="M 14 32 Q 10 28 10 24" stroke="#8b4513" strokeWidth="2" fill="none" strokeLinecap="round"/>
<path d="M 50 32 Q 54 28 54 24" stroke="#8b4513" strokeWidth="2" fill="none" strokeLinecap="round"/>
<path
d="M 14 32 Q 10 28 10 24"
stroke="#8b4513"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
<path
d="M 50 32 Q 54 28 54 24"
stroke="#8b4513"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
{/* 冒烟 */}
<path d="M 28 20 Q 26 12 28 4" stroke="#5a4a42" strokeWidth="1.5" fill="none" opacity="0.6" className="smoke" strokeLinecap="round"/>
<path d="M 32 18 Q 32 10 32 2" stroke="#5a4a42" strokeWidth="1.5" fill="none" opacity="0.5" className="smoke" style={{ animationDelay: '0.3s' }} strokeLinecap="round"/>
<path d="M 36 20 Q 38 12 36 4" stroke="#5a4a42" strokeWidth="1.5" fill="none" opacity="0.6" className="smoke" style={{ animationDelay: '0.6s' }} strokeLinecap="round"/>
<path
d="M 28 20 Q 26 12 28 4"
stroke="#5a4a42"
strokeWidth="1.5"
fill="none"
opacity="0.6"
className="smoke"
strokeLinecap="round"
/>
<path
d="M 32 18 Q 32 10 32 2"
stroke="#5a4a42"
strokeWidth="1.5"
fill="none"
opacity="0.5"
className="smoke"
style={{ animationDelay: '0.3s' }}
strokeLinecap="round"
/>
<path
d="M 36 20 Q 38 12 36 4"
stroke="#5a4a42"
strokeWidth="1.5"
fill="none"
opacity="0.6"
className="smoke"
style={{ animationDelay: '0.6s' }}
strokeLinecap="round"
/>
</svg>
);
}
@@ -31,21 +91,88 @@ export function AlchemyFurnaceIcon({ className = "w-6 h-6" }: { className?: stri
/**
* 古籍图标(打开的书本)
*/
export function AncientBookIcon({ className = "w-6 h-6" }: { className?: string }) {
export function AncientBookIcon({
className = 'w-6 h-6',
}: {
className?: string;
}) {
return (
<svg className={className} viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* 书页 */}
<path d="M 8 12 L 8 52 Q 8 56 12 56 L 28 56 Q 32 56 32 52 L 32 12 Q 32 8 28 8 L 12 8 Q 8 8 8 12 Z" fill="#f8f3e6" stroke="#2c1810" strokeWidth="1.5"/>
<path d="M 32 12 L 32 52 Q 32 56 36 56 L 52 56 Q 56 56 56 52 L 56 12 Q 56 8 52 8 L 36 8 Q 32 8 32 12 Z" fill="#f8f3e6" stroke="#2c1810" strokeWidth="1.5"/>
<path
d="M 8 12 L 8 52 Q 8 56 12 56 L 28 56 Q 32 56 32 52 L 32 12 Q 32 8 28 8 L 12 8 Q 8 8 8 12 Z"
fill="#f8f3e6"
stroke="#2c1810"
strokeWidth="1.5"
/>
<path
d="M 32 12 L 32 52 Q 32 56 36 56 L 52 56 Q 56 56 56 52 L 56 12 Q 56 8 52 8 L 36 8 Q 32 8 32 12 Z"
fill="#f8f3e6"
stroke="#2c1810"
strokeWidth="1.5"
/>
{/* 书脊 */}
<line x1="32" y1="8" x2="32" y2="56" stroke="#8b4513" strokeWidth="2"/>
<line x1="32" y1="8" x2="32" y2="56" stroke="#8b4513" strokeWidth="2" />
{/* 文字线条(模拟文字) */}
<line x1="14" y1="20" x2="26" y2="20" stroke="#5a4a42" strokeWidth="1" opacity="0.4"/>
<line x1="14" y1="28" x2="26" y2="28" stroke="#5a4a42" strokeWidth="1" opacity="0.4"/>
<line x1="14" y1="36" x2="26" y2="36" stroke="#5a4a42" strokeWidth="1" opacity="0.4"/>
<line x1="38" y1="20" x2="50" y2="20" stroke="#5a4a42" strokeWidth="1" opacity="0.4"/>
<line x1="38" y1="28" x2="50" y2="28" stroke="#5a4a42" strokeWidth="1" opacity="0.4"/>
<line x1="38" y1="36" x2="50" y2="36" stroke="#5a4a42" strokeWidth="1" opacity="0.4"/>
<line
x1="14"
y1="20"
x2="26"
y2="20"
stroke="#5a4a42"
strokeWidth="1"
opacity="0.4"
/>
<line
x1="14"
y1="28"
x2="26"
y2="28"
stroke="#5a4a42"
strokeWidth="1"
opacity="0.4"
/>
<line
x1="14"
y1="36"
x2="26"
y2="36"
stroke="#5a4a42"
strokeWidth="1"
opacity="0.4"
/>
<line
x1="38"
y1="20"
x2="50"
y2="20"
stroke="#5a4a42"
strokeWidth="1"
opacity="0.4"
/>
<line
x1="38"
y1="28"
x2="50"
y2="28"
stroke="#5a4a42"
strokeWidth="1"
opacity="0.4"
/>
<line
x1="38"
y1="36"
x2="50"
y2="36"
stroke="#5a4a42"
strokeWidth="1"
opacity="0.4"
/>
</svg>
);
}
@@ -53,23 +180,59 @@ export function AncientBookIcon({ className = "w-6 h-6" }: { className?: string
/**
* 龙纹图标(简化龙形)
*/
export function DragonPatternIcon({ className = "w-5 h-5" }: { className?: string }) {
export function DragonPatternIcon({
className = 'w-5 h-5',
}: {
className?: string;
}) {
return (
<svg className={className} viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* 龙身 */}
<path d="M 8 32 Q 16 20 24 24 Q 32 28 40 20 Q 48 12 56 20" stroke="#c1121f" strokeWidth="2.5" fill="none" strokeLinecap="round"/>
<path
d="M 8 32 Q 16 20 24 24 Q 32 28 40 20 Q 48 12 56 20"
stroke="#c1121f"
strokeWidth="2.5"
fill="none"
strokeLinecap="round"
/>
{/* 龙头 */}
<circle cx="56" cy="20" r="4" fill="#c1121f"/>
<path d="M 60 18 L 62 16 M 60 22 L 62 24" stroke="#c1121f" strokeWidth="2" strokeLinecap="round"/>
<circle cx="56" cy="20" r="4" fill="#c1121f" />
<path
d="M 60 18 L 62 16 M 60 22 L 62 24"
stroke="#c1121f"
strokeWidth="2"
strokeLinecap="round"
/>
{/* 龙尾 */}
<path d="M 8 32 Q 4 28 6 24" stroke="#c1121f" strokeWidth="2.5" fill="none" strokeLinecap="round"/>
<path
d="M 8 32 Q 4 28 6 24"
stroke="#c1121f"
strokeWidth="2.5"
fill="none"
strokeLinecap="round"
/>
{/* 龙爪 */}
<path d="M 24 24 L 22 28 M 28 26 L 26 30" stroke="#c1121f" strokeWidth="2" strokeLinecap="round"/>
<path d="M 40 20 L 38 24 M 44 22 L 42 26" stroke="#c1121f" strokeWidth="2" strokeLinecap="round"/>
<path
d="M 24 24 L 22 28 M 28 26 L 26 30"
stroke="#c1121f"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M 40 20 L 38 24 M 44 22 L 42 26"
stroke="#c1121f"
strokeWidth="2"
strokeLinecap="round"
/>
{/* 龙鳞 */}
<circle cx="20" cy="26" r="1.5" fill="#c1121f" opacity="0.6"/>
<circle cx="32" cy="24" r="1.5" fill="#c1121f" opacity="0.6"/>
<circle cx="44" cy="18" r="1.5" fill="#c1121f" opacity="0.6"/>
<circle cx="20" cy="26" r="1.5" fill="#c1121f" opacity="0.6" />
<circle cx="32" cy="24" r="1.5" fill="#c1121f" opacity="0.6" />
<circle cx="44" cy="18" r="1.5" fill="#c1121f" opacity="0.6" />
</svg>
);
}
@@ -77,23 +240,101 @@ export function DragonPatternIcon({ className = "w-5 h-5" }: { className?: strin
/**
* 竹简图标(竹片串成的简册)
*/
export function BambooScrollIcon({ className = "w-6 h-6" }: { className?: string }) {
export function BambooScrollIcon({
className = 'w-6 h-6',
}: {
className?: string;
}) {
return (
<svg className={className} viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* 竹片 */}
<rect x="12" y="8" width="40" height="6" rx="1" fill="#4a7c59" stroke="#2c1810" strokeWidth="0.5"/>
<rect x="12" y="18" width="40" height="6" rx="1" fill="#4a7c59" stroke="#2c1810" strokeWidth="0.5"/>
<rect x="12" y="28" width="40" height="6" rx="1" fill="#4a7c59" stroke="#2c1810" strokeWidth="0.5"/>
<rect x="12" y="38" width="40" height="6" rx="1" fill="#4a7c59" stroke="#2c1810" strokeWidth="0.5"/>
<rect x="12" y="48" width="40" height="6" rx="1" fill="#4a7c59" stroke="#2c1810" strokeWidth="0.5"/>
<rect
x="12"
y="8"
width="40"
height="6"
rx="1"
fill="#4a7c59"
stroke="#2c1810"
strokeWidth="0.5"
/>
<rect
x="12"
y="18"
width="40"
height="6"
rx="1"
fill="#4a7c59"
stroke="#2c1810"
strokeWidth="0.5"
/>
<rect
x="12"
y="28"
width="40"
height="6"
rx="1"
fill="#4a7c59"
stroke="#2c1810"
strokeWidth="0.5"
/>
<rect
x="12"
y="38"
width="40"
height="6"
rx="1"
fill="#4a7c59"
stroke="#2c1810"
strokeWidth="0.5"
/>
<rect
x="12"
y="48"
width="40"
height="6"
rx="1"
fill="#4a7c59"
stroke="#2c1810"
strokeWidth="0.5"
/>
{/* 连接线 */}
<line x1="20" y1="8" x2="20" y2="54" stroke="#2c1810" strokeWidth="1"/>
<line x1="32" y1="8" x2="32" y2="54" stroke="#2c1810" strokeWidth="1"/>
<line x1="44" y1="8" x2="44" y2="54" stroke="#2c1810" strokeWidth="1"/>
<line x1="20" y1="8" x2="20" y2="54" stroke="#2c1810" strokeWidth="1" />
<line x1="32" y1="8" x2="32" y2="54" stroke="#2c1810" strokeWidth="1" />
<line x1="44" y1="8" x2="44" y2="54" stroke="#2c1810" strokeWidth="1" />
{/* 文字线条 */}
<line x1="14" y1="11" x2="18" y2="11" stroke="#2c1810" strokeWidth="0.8" opacity="0.5"/>
<line x1="14" y1="21" x2="18" y2="21" stroke="#2c1810" strokeWidth="0.8" opacity="0.5"/>
<line x1="14" y1="31" x2="18" y2="31" stroke="#2c1810" strokeWidth="0.8" opacity="0.5"/>
<line
x1="14"
y1="11"
x2="18"
y2="11"
stroke="#2c1810"
strokeWidth="0.8"
opacity="0.5"
/>
<line
x1="14"
y1="21"
x2="18"
y2="21"
stroke="#2c1810"
strokeWidth="0.8"
opacity="0.5"
/>
<line
x1="14"
y1="31"
x2="18"
y2="31"
stroke="#2c1810"
strokeWidth="0.8"
opacity="0.5"
/>
</svg>
);
}
@@ -101,13 +342,43 @@ export function BambooScrollIcon({ className = "w-6 h-6" }: { className?: string
/**
* 云纹图标(传统云纹图案)
*/
export function CloudPatternIcon({ className = "w-6 h-6" }: { className?: string }) {
export function CloudPatternIcon({
className = 'w-6 h-6',
}: {
className?: string;
}) {
return (
<svg className={className} viewBox="0 0 64 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 64 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* 云纹曲线 */}
<path d="M 0 16 Q 8 8 16 16 T 32 16 T 48 16 T 64 16" stroke="#2c1810" strokeWidth="1.5" fill="none" opacity="0.3" strokeLinecap="round"/>
<path d="M 0 20 Q 10 10 20 20 T 40 20 T 60 20" stroke="#2c1810" strokeWidth="1.5" fill="none" opacity="0.25" strokeLinecap="round"/>
<path d="M 4 12 Q 12 4 20 12 Q 28 4 36 12 Q 44 4 52 12 Q 60 4 64 12" stroke="#2c1810" strokeWidth="1" fill="none" opacity="0.2" strokeLinecap="round"/>
<path
d="M 0 16 Q 8 8 16 16 T 32 16 T 48 16 T 64 16"
stroke="#2c1810"
strokeWidth="1.5"
fill="none"
opacity="0.3"
strokeLinecap="round"
/>
<path
d="M 0 20 Q 10 10 20 20 T 40 20 T 60 20"
stroke="#2c1810"
strokeWidth="1.5"
fill="none"
opacity="0.25"
strokeLinecap="round"
/>
<path
d="M 4 12 Q 12 4 20 12 Q 28 4 36 12 Q 44 4 52 12 Q 60 4 64 12"
stroke="#2c1810"
strokeWidth="1"
fill="none"
opacity="0.2"
strokeLinecap="round"
/>
</svg>
);
}
@@ -115,20 +386,54 @@ export function CloudPatternIcon({ className = "w-6 h-6" }: { className?: string
/**
* 砚台图标(传统文房四宝)
*/
export function InkstoneIcon({ className = "w-6 h-6" }: { className?: string }) {
export function InkstoneIcon({
className = 'w-6 h-6',
}: {
className?: string;
}) {
return (
<svg className={className} viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* 砚台主体 */}
<ellipse cx="32" cy="48" rx="24" ry="8" fill="#2c1810" stroke="#1a1008" strokeWidth="1.5"/>
<ellipse cx="32" cy="44" rx="20" ry="6" fill="#5a4a42"/>
<ellipse
cx="32"
cy="48"
rx="24"
ry="8"
fill="#2c1810"
stroke="#1a1008"
strokeWidth="1.5"
/>
<ellipse cx="32" cy="44" rx="20" ry="6" fill="#5a4a42" />
{/* 墨池 */}
<ellipse cx="32" cy="40" rx="14" ry="4" fill="#1a1008"/>
<ellipse cx="32" cy="40" rx="14" ry="4" fill="#1a1008" />
{/* 砚台边缘 */}
<path d="M 12 44 Q 8 40 8 36 Q 8 32 12 28 Q 16 24 20 24 Q 24 24 28 28 Q 32 24 36 24 Q 40 24 44 28 Q 48 32 48 36 Q 48 40 44 44"
stroke="#8b4513" strokeWidth="2" fill="none" strokeLinecap="round"/>
<path
d="M 12 44 Q 8 40 8 36 Q 8 32 12 28 Q 16 24 20 24 Q 24 24 28 28 Q 32 24 36 24 Q 40 24 44 28 Q 48 32 48 36 Q 48 40 44 44"
stroke="#8b4513"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
{/* 装饰纹路 */}
<path d="M 20 32 Q 24 30 28 32" stroke="#8b4513" strokeWidth="1" fill="none" opacity="0.6"/>
<path d="M 36 32 Q 40 30 44 32" stroke="#8b4513" strokeWidth="1" fill="none" opacity="0.6"/>
<path
d="M 20 32 Q 24 30 28 32"
stroke="#8b4513"
strokeWidth="1"
fill="none"
opacity="0.6"
/>
<path
d="M 36 32 Q 40 30 44 32"
stroke="#8b4513"
strokeWidth="1"
fill="none"
opacity="0.6"
/>
</svg>
);
}
@@ -136,21 +441,93 @@ export function InkstoneIcon({ className = "w-6 h-6" }: { className?: string })
/**
* 卷轴图标(展开的卷轴)
*/
export function ScrollIcon({ className = "w-6 h-6" }: { className?: string }) {
export function ScrollIcon({ className = 'w-6 h-6' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 64 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 64 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* 卷轴木轴 */}
<rect x="4" y="16" width="6" height="16" rx="2" fill="#8b4513" stroke="#654321" strokeWidth="1"/>
<rect x="54" y="16" width="6" height="16" rx="2" fill="#8b4513" stroke="#654321" strokeWidth="1"/>
<rect
x="4"
y="16"
width="6"
height="16"
rx="2"
fill="#8b4513"
stroke="#654321"
strokeWidth="1"
/>
<rect
x="54"
y="16"
width="6"
height="16"
rx="2"
fill="#8b4513"
stroke="#654321"
strokeWidth="1"
/>
{/* 卷轴纸面 */}
<rect x="10" y="12" width="44" height="24" rx="2" fill="#f8f3e6" stroke="#2c1810" strokeWidth="1.5"/>
<rect
x="10"
y="12"
width="44"
height="24"
rx="2"
fill="#f8f3e6"
stroke="#2c1810"
strokeWidth="1.5"
/>
{/* 文字线条 */}
<line x1="14" y1="20" x2="50" y2="20" stroke="#5a4a42" strokeWidth="1" opacity="0.4"/>
<line x1="14" y1="26" x2="50" y2="26" stroke="#5a4a42" strokeWidth="1" opacity="0.4"/>
<line x1="14" y1="32" x2="50" y2="32" stroke="#5a4a42" strokeWidth="1" opacity="0.4"/>
<line
x1="14"
y1="20"
x2="50"
y2="20"
stroke="#5a4a42"
strokeWidth="1"
opacity="0.4"
/>
<line
x1="14"
y1="26"
x2="50"
y2="26"
stroke="#5a4a42"
strokeWidth="1"
opacity="0.4"
/>
<line
x1="14"
y1="32"
x2="50"
y2="32"
stroke="#5a4a42"
strokeWidth="1"
opacity="0.4"
/>
{/* 卷轴装饰线 */}
<line x1="12" y1="14" x2="52" y2="14" stroke="#8b4513" strokeWidth="0.5" opacity="0.5"/>
<line x1="12" y1="34" x2="52" y2="34" stroke="#8b4513" strokeWidth="0.5" opacity="0.5"/>
<line
x1="12"
y1="14"
x2="52"
y2="14"
stroke="#8b4513"
strokeWidth="0.5"
opacity="0.5"
/>
<line
x1="12"
y1="34"
x2="52"
y2="34"
stroke="#8b4513"
strokeWidth="0.5"
opacity="0.5"
/>
</svg>
);
}
@@ -158,29 +535,54 @@ export function ScrollIcon({ className = "w-6 h-6" }: { className?: string }) {
/**
* 印章图标(篆体印章,可自定义文字)
*/
export function SealIcon({
className = "w-16 h-16",
text = "胜",
color = "#c1121f"
}: {
export function SealIcon({
className = 'w-16 h-16',
text = '胜',
color = '#c1121f',
}: {
className?: string;
text?: string;
color?: string;
}) {
return (
<svg className={className} viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 100 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* 印章边框(方形,带圆角) */}
<rect x="8" y="8" width="84" height="84" rx="4" fill="none" stroke={color} strokeWidth="4" opacity="0.9"/>
<rect
x="8"
y="8"
width="84"
height="84"
rx="4"
fill="none"
stroke={color}
strokeWidth="4"
opacity="0.9"
/>
{/* 内部装饰线 */}
<rect x="12" y="12" width="76" height="76" rx="2" fill="none" stroke={color} strokeWidth="1" opacity="0.5"/>
<rect
x="12"
y="12"
width="76"
height="76"
rx="2"
fill="none"
stroke={color}
strokeWidth="1"
opacity="0.5"
/>
{/* 文字(使用更粗的字体模拟篆体) */}
<text
x="50"
y="65"
fontSize="56"
fill={color}
textAnchor="middle"
fontFamily="serif"
<text
x="50"
y="65"
fontSize="56"
fill={color}
textAnchor="middle"
fontFamily="serif"
fontWeight="900"
opacity="0.9"
style={{ fontVariant: 'normal', letterSpacing: '0' }}
@@ -194,13 +596,31 @@ export function SealIcon({
/**
* 古籍批注图标(朱批标记)
*/
export function AnnotationIcon({ className = "w-4 h-4" }: { className?: string }) {
export function AnnotationIcon({
className = 'w-4 h-4',
}: {
className?: string;
}) {
return (
<svg className={className} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* 竖线 */}
<line x1="8" y1="4" x2="8" y2="28" stroke="#c1121f" strokeWidth="3" strokeLinecap="round" opacity="0.6"/>
<line
x1="8"
y1="4"
x2="8"
y2="28"
stroke="#c1121f"
strokeWidth="3"
strokeLinecap="round"
opacity="0.6"
/>
{/* 批注标记点 */}
<circle cx="8" cy="16" r="3" fill="#c1121f" opacity="0.4"/>
<circle cx="8" cy="16" r="3" fill="#c1121f" opacity="0.4" />
</svg>
);
}
@@ -208,31 +628,41 @@ export function AnnotationIcon({ className = "w-4 h-4" }: { className?: string }
/**
* 云纹分隔线(用于页面分隔)
*/
export function CloudDivider({ className = "w-full h-10" }: { className?: string }) {
export function CloudDivider({
className = 'w-full h-10',
}: {
className?: string;
}) {
return (
<svg className={className} viewBox="0 0 400 40" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 400 40"
preserveAspectRatio="none"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* 多层云纹 */}
<path
d="M 0 20 Q 50 10 100 20 T 200 20 T 300 20 T 400 20"
stroke="#2c1810"
strokeWidth="1.5"
fill="none"
<path
d="M 0 20 Q 50 10 100 20 T 200 20 T 300 20 T 400 20"
stroke="#2c1810"
strokeWidth="1.5"
fill="none"
opacity="0.25"
strokeLinecap="round"
/>
<path
d="M 0 25 Q 60 12 120 25 T 240 25 T 360 25 T 400 25"
stroke="#2c1810"
strokeWidth="1.5"
fill="none"
<path
d="M 0 25 Q 60 12 120 25 T 240 25 T 360 25 T 400 25"
stroke="#2c1810"
strokeWidth="1.5"
fill="none"
opacity="0.2"
strokeLinecap="round"
/>
<path
d="M 20 15 Q 70 5 130 15 Q 190 5 250 15 Q 310 5 370 15"
stroke="#2c1810"
strokeWidth="1"
fill="none"
<path
d="M 20 15 Q 70 5 130 15 Q 190 5 250 15 Q 310 5 370 15"
stroke="#2c1810"
strokeWidth="1"
fill="none"
opacity="0.15"
strokeLinecap="round"
/>
@@ -243,24 +673,29 @@ export function CloudDivider({ className = "w-full h-10" }: { className?: string
/**
* 火焰图标(用于天榜标题)
*/
export function FlameIcon({ className = "w-5 h-5" }: { className?: string }) {
export function FlameIcon({ className = 'w-5 h-5' }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 32 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 32 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* 火焰主体 */}
<path
d="M 16 4 Q 12 8 10 14 Q 8 20 10 26 Q 12 32 16 36 Q 20 32 22 26 Q 24 20 22 14 Q 20 8 16 4 Z"
fill="#c1121f"
<path
d="M 16 4 Q 12 8 10 14 Q 8 20 10 26 Q 12 32 16 36 Q 20 32 22 26 Q 24 20 22 14 Q 20 8 16 4 Z"
fill="#c1121f"
opacity="0.8"
/>
{/* 内焰 */}
<path
d="M 16 8 Q 14 12 13 16 Q 12 20 13 24 Q 14 28 16 30 Q 18 28 19 24 Q 20 20 19 16 Q 18 12 16 8 Z"
fill="#ff6b6b"
<path
d="M 16 8 Q 14 12 13 16 Q 12 20 13 24 Q 14 28 16 30 Q 18 28 19 24 Q 20 20 19 16 Q 18 12 16 8 Z"
fill="#ff6b6b"
opacity="0.9"
/>
{/* 火苗 */}
<ellipse cx="14" cy="12" rx="2" ry="4" fill="#ffd700" opacity="0.7"/>
<ellipse cx="18" cy="12" rx="2" ry="4" fill="#ffd700" opacity="0.7"/>
<ellipse cx="14" cy="12" rx="2" ry="4" fill="#ffd700" opacity="0.7" />
<ellipse cx="18" cy="12" rx="2" ry="4" fill="#ffd700" opacity="0.7" />
</svg>
);
}
+198 -198
View File
@@ -1,4 +1,4 @@
import type { Cultivator } from "@/types/cultivator";
import type { Cultivator } from '@/types/cultivator';
/**
* 假数据:排行榜
@@ -6,37 +6,37 @@ import type { Cultivator } from "@/types/cultivator";
*/
export const mockRankings: Cultivator[] = [
{
id: "mock-001",
name: "玄霄子",
prompt: "正道天骄,剑道通神",
cultivationLevel: "元婴中期",
spiritRoot: "变异·雷灵根",
appearance: "青衫长剑,眉目如星,周身雷光隐现",
backstory: "出身名门,自幼习剑,三十岁便突破元婴,被誉为千年一遇的剑道奇才",
id: 'mock-001',
name: '玄霄子',
prompt: '正道天骄,剑道通神',
cultivationLevel: '元婴中期',
spiritRoot: '变异·雷灵根',
appearance: '青衫长剑,眉目如星,周身雷光隐现',
backstory: '出身名门,自幼习剑,三十岁便突破元婴,被誉为千年一遇的剑道奇才',
preHeavenFates: [
{
name: "紫府通明",
type: "吉",
effect: "spirit +15",
description: "元神澄澈,雷意自生。",
name: '紫府通明',
type: '吉',
effect: 'spirit +15',
description: '元神澄澈,雷意自生。',
},
{
name: "剑胆琴心",
type: "吉",
effect: "speed +8",
description: "剑心清明,出手如光。",
name: '剑胆琴心',
type: '吉',
effect: 'speed +8',
description: '剑心清明,出手如光。',
},
{
name: "雷霆之子",
type: "吉",
effect: "雷系技能威力 +20%",
description: "天生雷息,号令九霄。",
name: '雷霆之子',
type: '吉',
effect: '雷系技能威力 +20%',
description: '天生雷息,号令九霄。',
},
],
battleProfile: {
maxHp: 300,
hp: 300,
element: "雷",
element: '雷',
attributes: {
vitality: 100,
spirit: 85,
@@ -45,30 +45,30 @@ export const mockRankings: Cultivator[] = [
},
skills: [
{
name: "万雷御剑",
type: "attack",
name: '万雷御剑',
type: 'attack',
power: 80,
element: "雷",
effects: ["stun"],
element: '雷',
effects: ['stun'],
},
{
name: "霄光护体",
type: "buff",
name: '霄光护体',
type: 'buff',
power: 60,
element: "雷",
effects: ["speed_up", "spirit_up"],
element: '雷',
effects: ['speed_up', 'spirit_up'],
},
{
name: "天罡雷息",
type: "heal",
name: '天罡雷息',
type: 'heal',
power: 70,
element: "土",
effects: ["heal"],
element: '土',
effects: ['heal'],
},
],
equipment: [
{
name: "惊雷剑",
name: '惊雷剑',
bonus: {
spirit: 8,
elementBoost: { 雷: 0.15 },
@@ -78,37 +78,37 @@ export const mockRankings: Cultivator[] = [
},
},
{
id: "mock-002",
name: "血手人屠",
prompt: "魔道巨擘,嗜血成性",
cultivationLevel: "元婴后期",
spiritRoot: "血魔灵根",
appearance: "黑袍遮身,双目赤红,周身血雾缭绕,手持一柄血色长刀",
backstory: "曾是正道天骄,因心魔入体堕入魔道,以吞噬修士精血为乐,凶名赫赫",
id: 'mock-002',
name: '血手人屠',
prompt: '魔道巨擘,嗜血成性',
cultivationLevel: '元婴后期',
spiritRoot: '血魔灵根',
appearance: '黑袍遮身,双目赤红,周身血雾缭绕,手持一柄血色长刀',
backstory: '曾是正道天骄,因心魔入体堕入魔道,以吞噬修士精血为乐,凶名赫赫',
preHeavenFates: [
{
name: "血煞命格",
type: "凶",
effect: "wisdom -5, vitality +10",
description: "煞气入骨,杀戮难止。",
name: '血煞命格',
type: '凶',
effect: 'wisdom -5, vitality +10',
description: '煞气入骨,杀戮难止。',
},
{
name: "魔心不灭",
type: "吉",
effect: "max_hp +25",
description: "心魔化盾,万劫不灭。",
name: '魔心不灭',
type: '吉',
effect: 'max_hp +25',
description: '心魔化盾,万劫不灭。',
},
{
name: "夺魂摄魄",
type: "凶",
effect: "攻击附带夺魂",
description: "以魂养身,邪威滔天。",
name: '夺魂摄魄',
type: '凶',
effect: '攻击附带夺魂',
description: '以魂养身,邪威滔天。',
},
],
battleProfile: {
maxHp: 350,
hp: 350,
element: "火",
element: '火',
attributes: {
vitality: 110,
spirit: 80,
@@ -117,30 +117,30 @@ export const mockRankings: Cultivator[] = [
},
skills: [
{
name: "血海滔天",
type: "attack",
name: '血海滔天',
type: 'attack',
power: 85,
element: "火",
effects: ["burn"],
element: '火',
effects: ['burn'],
},
{
name: "魔焰护身",
type: "buff",
name: '魔焰护身',
type: 'buff',
power: 55,
element: "火",
effects: ["speed_up"],
element: '火',
effects: ['speed_up'],
},
{
name: "血元回潮",
type: "heal",
name: '血元回潮',
type: 'heal',
power: 65,
element: "土",
effects: ["heal"],
element: '土',
effects: ['heal'],
},
],
equipment: [
{
name: "血炼魔刀",
name: '血炼魔刀',
bonus: {
vitality: 8,
elementBoost: { 火: 0.15 },
@@ -150,37 +150,37 @@ export const mockRankings: Cultivator[] = [
},
},
{
id: "mock-003",
name: "林无尘",
prompt: "靠炼丹逆袭的废柴少主",
cultivationLevel: "金丹巅峰",
spiritRoot: "变异·冰焰双生",
appearance: "白衣染霜,眉心一点赤焰印记,腰间挂着一只古朴丹炉",
backstory: "幼时被逐出宗门,靠一本残破丹方崛起,以丹道证得金丹大道",
id: 'mock-003',
name: '林无尘',
prompt: '靠炼丹逆袭的废柴少主',
cultivationLevel: '金丹巅峰',
spiritRoot: '变异·冰焰双生',
appearance: '白衣染霜,眉心一点赤焰印记,腰间挂着一只古朴丹炉',
backstory: '幼时被逐出宗门,靠一本残破丹方崛起,以丹道证得金丹大道',
preHeavenFates: [
{
name: "丹心通神",
type: "吉",
effect: "spirit +12",
description: "丹火入魂,心念通灵。",
name: '丹心通神',
type: '吉',
effect: 'spirit +12',
description: '丹火入魂,心念通灵。',
},
{
name: "逆脉修行",
type: "凶",
effect: "speed -5, wisdom +5",
description: "以逆行道脉锤炼己身。",
name: '逆脉修行',
type: '凶',
effect: 'speed -5, wisdom +5',
description: '以逆行道脉锤炼己身。',
},
{
name: "九重丹劫",
type: "吉",
effect: "max_hp +20",
description: "历经九劫,肉身如炉。",
name: '九重丹劫',
type: '吉',
effect: 'max_hp +20',
description: '历经九劫,肉身如炉。',
},
],
battleProfile: {
maxHp: 250,
hp: 250,
element: "火",
element: '火',
attributes: {
vitality: 90,
spirit: 75,
@@ -189,30 +189,30 @@ export const mockRankings: Cultivator[] = [
},
skills: [
{
name: "冰焰双绝",
type: "attack",
name: '冰焰双绝',
type: 'attack',
power: 75,
element: "火",
effects: ["burn"],
element: '火',
effects: ['burn'],
},
{
name: "丹心护体",
type: "heal",
name: '丹心护体',
type: 'heal',
power: 75,
element: "土",
effects: ["heal"],
element: '土',
effects: ['heal'],
},
{
name: "逆脉爆发",
type: "buff",
name: '逆脉爆发',
type: 'buff',
power: 50,
element: "火",
effects: ["spirit_up"],
element: '火',
effects: ['spirit_up'],
},
],
equipment: [
{
name: "玄火丹炉",
name: '玄火丹炉',
bonus: {
spirit: 8,
elementBoost: { 火: 0.1 },
@@ -222,37 +222,37 @@ export const mockRankings: Cultivator[] = [
},
},
{
id: "mock-004",
name: "云瑶仙子",
prompt: "清冷孤傲的剑仙",
cultivationLevel: "金丹后期",
spiritRoot: "水灵根",
appearance: "白衣胜雪,长发如瀑,手持一柄寒冰长剑,气质清冷如月",
backstory: "出身水月宗,以清冷孤傲闻名,剑法出神入化,被誉为水月第一剑",
id: 'mock-004',
name: '云瑶仙子',
prompt: '清冷孤傲的剑仙',
cultivationLevel: '金丹后期',
spiritRoot: '水灵根',
appearance: '白衣胜雪,长发如瀑,手持一柄寒冰长剑,气质清冷如月',
backstory: '出身水月宗,以清冷孤傲闻名,剑法出神入化,被誉为水月第一剑',
preHeavenFates: [
{
name: "冰心玉骨",
type: "吉",
effect: "wisdom +10",
description: "心如寒霜,不染尘埃。",
name: '冰心玉骨',
type: '吉',
effect: 'wisdom +10',
description: '心如寒霜,不染尘埃。',
},
{
name: "水月镜花",
type: "吉",
effect: "control 技能成功率 +10%",
description: "虚实难测,幻灭自如。",
name: '水月镜花',
type: '吉',
effect: 'control 技能成功率 +10%',
description: '虚实难测,幻灭自如。',
},
{
name: "霜魄孤鸣",
type: "凶",
effect: "speed +5, 无法结伴",
description: "天性孤高,独行天下。",
name: '霜魄孤鸣',
type: '凶',
effect: 'speed +5, 无法结伴',
description: '天性孤高,独行天下。',
},
],
battleProfile: {
maxHp: 220,
hp: 220,
element: "水",
element: '水',
attributes: {
vitality: 80,
spirit: 75,
@@ -261,30 +261,30 @@ export const mockRankings: Cultivator[] = [
},
skills: [
{
name: "寒霜剑雨",
type: "attack",
name: '寒霜剑雨',
type: 'attack',
power: 75,
element: "水",
effects: ["slow"],
element: '水',
effects: ['slow'],
},
{
name: "水月镜花",
type: "control",
name: '水月镜花',
type: 'control',
power: 65,
element: "水",
effects: ["stun"],
element: '水',
effects: ['stun'],
},
{
name: "冰心诀",
type: "heal",
name: '冰心诀',
type: 'heal',
power: 65,
element: "水",
effects: ["heal"],
element: '水',
effects: ['heal'],
},
],
equipment: [
{
name: "寒霜古剑",
name: '寒霜古剑',
bonus: {
speed: 5,
elementBoost: { 水: 0.12 },
@@ -294,37 +294,37 @@ export const mockRankings: Cultivator[] = [
},
},
{
id: "mock-005",
name: "墨渊",
prompt: "神秘的黑衣剑客",
cultivationLevel: "金丹中期",
spiritRoot: "暗灵根",
appearance: "黑衣如墨,面容冷峻,腰间佩剑,周身散发阴冷气息",
backstory: "来历不明,以暗杀闻名,剑法诡异莫测,令人闻风丧胆",
id: 'mock-005',
name: '墨渊',
prompt: '神秘的黑衣剑客',
cultivationLevel: '金丹中期',
spiritRoot: '暗灵根',
appearance: '黑衣如墨,面容冷峻,腰间佩剑,周身散发阴冷气息',
backstory: '来历不明,以暗杀闻名,剑法诡异莫测,令人闻风丧胆',
preHeavenFates: [
{
name: "无影无踪",
type: "吉",
effect: "speed +10",
description: "身形如烟,往来无迹。",
name: '无影无踪',
type: '吉',
effect: 'speed +10',
description: '身形如烟,往来无迹。',
},
{
name: "噬魂夺魄",
type: "凶",
effect: "攻击附带夺魄",
description: "魂飞魄散,难入轮回。",
name: '噬魂夺魄',
type: '凶',
effect: '攻击附带夺魄',
description: '魂飞魄散,难入轮回。',
},
{
name: "夜行孤星",
type: "凶",
effect: "max_hp -10, crit +8%",
description: "夜色之下,星光独泣。",
name: '夜行孤星',
type: '凶',
effect: 'max_hp -10, crit +8%',
description: '夜色之下,星光独泣。',
},
],
battleProfile: {
maxHp: 200,
hp: 200,
element: "木",
element: '木',
attributes: {
vitality: 75,
spirit: 70,
@@ -333,30 +333,30 @@ export const mockRankings: Cultivator[] = [
},
skills: [
{
name: "暗影突袭",
type: "attack",
name: '暗影突袭',
type: 'attack',
power: 75,
element: "木",
effects: ["bleed"],
element: '木',
effects: ['bleed'],
},
{
name: "无踪遁形",
type: "buff",
name: '无踪遁形',
type: 'buff',
power: 55,
element: "木",
effects: ["speed_up"],
element: '木',
effects: ['speed_up'],
},
{
name: "噬魂锁",
type: "control",
name: '噬魂锁',
type: 'control',
power: 65,
element: "木",
effects: ["stun"],
element: '木',
effects: ['stun'],
},
],
equipment: [
{
name: "影蚀匕首",
name: '影蚀匕首',
bonus: {
speed: 6,
elementBoost: { 木: 0.08 },
@@ -366,37 +366,37 @@ export const mockRankings: Cultivator[] = [
},
},
{
id: "mock-006",
name: "青莲剑仙",
prompt: "潇洒不羁的剑客",
cultivationLevel: "金丹初期",
spiritRoot: "木灵根",
appearance: "青衫飘逸,手持青莲剑,气质潇洒不羁,如诗如画",
backstory: "出身青莲剑宗,以潇洒不羁闻名,剑法如诗如画,被誉为青莲剑仙",
id: 'mock-006',
name: '青莲剑仙',
prompt: '潇洒不羁的剑客',
cultivationLevel: '金丹初期',
spiritRoot: '木灵根',
appearance: '青衫飘逸,手持青莲剑,气质潇洒不羁,如诗如画',
backstory: '出身青莲剑宗,以潇洒不羁闻名,剑法如诗如画,被誉为青莲剑仙',
preHeavenFates: [
{
name: "青莲剑骨",
type: "吉",
effect: "spirit +8",
description: "剑骨如莲,生生不息。",
name: '青莲剑骨',
type: '吉',
effect: 'spirit +8',
description: '剑骨如莲,生生不息。',
},
{
name: "云游四海",
type: "吉",
effect: "speed +6",
description: "浪迹天涯,心无所系。",
name: '云游四海',
type: '吉',
effect: 'speed +6',
description: '浪迹天涯,心无所系。',
},
{
name: "风流债",
type: "凶",
effect: "wisdom -3",
description: "情债缠身,心神易乱。",
name: '风流债',
type: '凶',
effect: 'wisdom -3',
description: '情债缠身,心神易乱。',
},
],
battleProfile: {
maxHp: 180,
hp: 180,
element: "木",
element: '木',
attributes: {
vitality: 70,
spirit: 65,
@@ -405,30 +405,30 @@ export const mockRankings: Cultivator[] = [
},
skills: [
{
name: "青莲剑势",
type: "attack",
name: '青莲剑势',
type: 'attack',
power: 70,
element: "木",
element: '木',
effects: [],
},
{
name: "莲华护身",
type: "buff",
name: '莲华护身',
type: 'buff',
power: 52,
element: "木",
effects: ["speed_up", "spirit_up"],
element: '木',
effects: ['speed_up', 'spirit_up'],
},
{
name: "生生不息",
type: "heal",
name: '生生不息',
type: 'heal',
power: 60,
element: "土",
effects: ["heal"],
element: '土',
effects: ['heal'],
},
],
equipment: [
{
name: "青莲古剑",
name: '青莲古剑',
bonus: {
spirit: 4,
elementBoost: { 木: 0.1 },
+4 -4
View File
@@ -1,9 +1,9 @@
import { defineConfig } from "drizzle-kit";
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: "postgresql",
schema: "./lib/drizzle/schema.ts",
tablesFilter: ["wanjiedaoyou_*"],
dialect: 'postgresql',
schema: './lib/drizzle/schema.ts',
tablesFilter: ['wanjiedaoyou_*'],
dbCredentials: {
url: `${process.env.DATABASE_URL}`,
},
+15 -43
View File
@@ -65,12 +65,8 @@
"name": "wanjiedaoyou_active_effects_battle_profile_id_wanjiedaoyou_battle_profiles_id_fk",
"tableFrom": "wanjiedaoyou_active_effects",
"tableTo": "wanjiedaoyou_battle_profiles",
"columnsFrom": [
"battle_profile_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["battle_profile_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -161,12 +157,8 @@
"name": "wanjiedaoyou_battle_profiles_cultivator_id_wanjiedaoyou_cultivators_id_fk",
"tableFrom": "wanjiedaoyou_battle_profiles",
"tableTo": "wanjiedaoyou_cultivators",
"columnsFrom": [
"cultivator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cultivator_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -176,9 +168,7 @@
"wanjiedaoyou_battle_profiles_cultivator_id_unique": {
"name": "wanjiedaoyou_battle_profiles_cultivator_id_unique",
"nullsNotDistinct": false,
"columns": [
"cultivator_id"
]
"columns": ["cultivator_id"]
}
},
"policies": {},
@@ -277,12 +267,8 @@
"name": "wanjiedaoyou_cultivators_user_id_wanjiedaoyou_users_id_fk",
"tableFrom": "wanjiedaoyou_cultivators",
"tableTo": "wanjiedaoyou_users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -336,12 +322,8 @@
"name": "wanjiedaoyou_equipment_cultivator_id_wanjiedaoyou_cultivators_id_fk",
"tableFrom": "wanjiedaoyou_equipment",
"tableTo": "wanjiedaoyou_cultivators",
"columnsFrom": [
"cultivator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cultivator_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -407,12 +389,8 @@
"name": "wanjiedaoyou_pre_heaven_fates_cultivator_id_wanjiedaoyou_cultivators_id_fk",
"tableFrom": "wanjiedaoyou_pre_heaven_fates",
"tableTo": "wanjiedaoyou_cultivators",
"columnsFrom": [
"cultivator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cultivator_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -484,12 +462,8 @@
"name": "wanjiedaoyou_skills_cultivator_id_wanjiedaoyou_cultivators_id_fk",
"tableFrom": "wanjiedaoyou_skills",
"tableTo": "wanjiedaoyou_cultivators",
"columnsFrom": [
"cultivator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cultivator_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -558,9 +532,7 @@
"wanjiedaoyou_users_email_unique": {
"name": "wanjiedaoyou_users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
"columns": ["email"]
}
},
"policies": {},
@@ -579,4 +551,4 @@
"schemas": {},
"tables": {}
}
}
}
+12 -34
View File
@@ -65,12 +65,8 @@
"name": "wanjiedaoyou_active_effects_battle_profile_id_wanjiedaoyou_battle_profiles_id_fk",
"tableFrom": "wanjiedaoyou_active_effects",
"tableTo": "wanjiedaoyou_battle_profiles",
"columnsFrom": [
"battle_profile_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["battle_profile_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -161,12 +157,8 @@
"name": "wanjiedaoyou_battle_profiles_cultivator_id_wanjiedaoyou_cultivators_id_fk",
"tableFrom": "wanjiedaoyou_battle_profiles",
"tableTo": "wanjiedaoyou_cultivators",
"columnsFrom": [
"cultivator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cultivator_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -176,9 +168,7 @@
"wanjiedaoyou_battle_profiles_cultivator_id_unique": {
"name": "wanjiedaoyou_battle_profiles_cultivator_id_unique",
"nullsNotDistinct": false,
"columns": [
"cultivator_id"
]
"columns": ["cultivator_id"]
}
},
"policies": {},
@@ -322,12 +312,8 @@
"name": "wanjiedaoyou_equipment_cultivator_id_wanjiedaoyou_cultivators_id_fk",
"tableFrom": "wanjiedaoyou_equipment",
"tableTo": "wanjiedaoyou_cultivators",
"columnsFrom": [
"cultivator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cultivator_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -393,12 +379,8 @@
"name": "wanjiedaoyou_pre_heaven_fates_cultivator_id_wanjiedaoyou_cultivators_id_fk",
"tableFrom": "wanjiedaoyou_pre_heaven_fates",
"tableTo": "wanjiedaoyou_cultivators",
"columnsFrom": [
"cultivator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cultivator_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -470,12 +452,8 @@
"name": "wanjiedaoyou_skills_cultivator_id_wanjiedaoyou_cultivators_id_fk",
"tableFrom": "wanjiedaoyou_skills",
"tableTo": "wanjiedaoyou_cultivators",
"columnsFrom": [
"cultivator_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["cultivator_id"],
"columnsTo": ["id"],
"onDelete": "no action",
"onUpdate": "no action"
}
@@ -498,4 +476,4 @@
"schemas": {},
"tables": {}
}
}
}
+1 -1
View File
@@ -17,4 +17,4 @@
"breakpoints": true
}
]
}
}
+88 -88
View File
@@ -1,11 +1,11 @@
import type {
Cultivator,
BattleProfile,
Skill,
ElementType,
ActiveEffect,
} from "@/types/cultivator";
import { ensureBattleProfile, cloneBattleProfile } from "@/utils/battleProfile";
BattleProfile,
Cultivator,
ElementType,
Skill,
} from '@/types/cultivator';
import { cloneBattleProfile, ensureBattleProfile } from '@/utils/battleProfile';
export interface BattleEngineResult {
winner: Cultivator;
@@ -27,22 +27,22 @@ const MAX_TURNS = 20;
const ELEMENT_MOD: Record<string, number> = {
// 雷系克制关系
"雷-金": 1.3,
"雷-木": 1.3,
"土-雷": 1.3,
"金-雷": 0.7,
"木-雷": 0.7,
'雷-金': 1.3,
'雷-木': 1.3,
'土-雷': 1.3,
'金-雷': 0.7,
'木-雷': 0.7,
// 五行相克关系
"金-木": 1.3,
"木-土": 1.3,
"土-水": 1.3,
"水-火": 1.3,
"火-金": 1.3,
"木-金": 0.7,
"土-木": 0.7,
"水-土": 0.7,
"火-水": 0.7,
"金-火": 0.7,
'金-木': 1.3,
'木-土': 1.3,
'土-水': 1.3,
'水-火': 1.3,
'火-金': 1.3,
'木-金': 0.7,
'土-木': 0.7,
'水-土': 0.7,
'火-水': 0.7,
'金-火': 0.7,
};
const clamp = (value: number, min: number, max: number) =>
@@ -61,12 +61,12 @@ const randomChoice = <T>(items: T[], fallback: T): T =>
export function simulateBattle(
player: Cultivator,
opponent: Cultivator
opponent: Cultivator,
): BattleEngineResult {
const playerProfile = cloneBattleProfile(ensureBattleProfile(player));
console.log("playerProfile", playerProfile);
console.log('playerProfile', playerProfile);
const opponentProfile = cloneBattleProfile(ensureBattleProfile(opponent));
console.log("opponentProfile", opponentProfile);
console.log('opponentProfile', opponentProfile);
const p: Combatant = {
...playerProfile,
@@ -155,11 +155,11 @@ export function simulateBattle(
function executeAction(
attacker: Combatant,
defender: Combatant,
log: string[]
log: string[],
) {
// 检查是否眩晕
const isStunned = attacker.activeEffects.some(
(effect) => effect.type === "stun"
(effect) => effect.type === 'stun',
);
if (isStunned) {
log.push(`${attacker.name} 处于眩晕状态,无法行动!`);
@@ -168,44 +168,44 @@ function executeAction(
const skill = selectSkill(attacker, defender);
switch (skill.type) {
case "attack":
case "control": {
case 'attack':
case 'control': {
const { damage, isCrit, elementNote } = calculateDamage(
attacker,
defender,
skill
skill,
);
applyDamage(defender, damage);
const critText = isCrit ? "(暴击)" : "";
const critText = isCrit ? '(暴击)' : '';
log.push(
`${attacker.name} 施展 ${skill.name}${critText},对 ${defender.name} 造成 ${damage} 点伤害${elementNote}。`
`${attacker.name} 施展 ${skill.name}${critText},对 ${defender.name} 造成 ${damage} 点伤害${elementNote}。`,
);
// 应用持续效果
applyEffects(defender, skill.effects || [], attacker.name, log);
break;
}
case "heal": {
case 'heal': {
const heal = Math.round(
skill.power * 0.5 + attacker.attributes.spirit * 0.3
skill.power * 0.5 + attacker.attributes.spirit * 0.3,
);
attacker.hp = clamp(attacker.hp + heal, 0, attacker.maxHp);
log.push(`${attacker.name} 运转 ${skill.name},回复 ${heal} 点气血。`);
break;
}
case "buff": {
if (skill.effects?.includes("speed_up")) {
case 'buff': {
if (skill.effects?.includes('speed_up')) {
attacker.attributes.speed = clamp(
attacker.attributes.speed + 5,
40,
120
120,
);
}
if (skill.effects?.includes("spirit_up")) {
if (skill.effects?.includes('spirit_up')) {
attacker.attributes.spirit = clamp(
attacker.attributes.spirit + 5,
40,
120
120,
);
}
// 应用持续buff效果
@@ -227,60 +227,60 @@ function processActiveEffects(target: Combatant, log: string[]) {
for (const effect of target.activeEffects) {
// 效果结算
switch (effect.type) {
case "bleed":
case 'bleed':
// 流血效果:每回合造成固定伤害
applyDamage(target, effect.value);
log.push(
`${target.name} 受到 ${effect.name} 影响,流血不止,损失 ${effect.value} 点气血!`
`${target.name} 受到 ${effect.name} 影响,流血不止,损失 ${effect.value} 点气血!`,
);
break;
case "burn":
case 'burn':
// 燃烧效果:每回合造成固定伤害
applyDamage(target, effect.value);
log.push(
`${target.name} 身上 ${effect.name} 燃烧,损失 ${effect.value} 点气血!`
`${target.name} 身上 ${effect.name} 燃烧,损失 ${effect.value} 点气血!`,
);
break;
case "stun":
case 'stun':
// 眩晕效果:已经在executeAction中检查,这里只是减少持续时间
break;
case "poison":
case 'poison':
// 中毒效果:每回合造成固定伤害
applyDamage(target, effect.value);
log.push(
`${target.name} 身中 ${effect.name},毒性发作,损失 ${effect.value} 点气血!`
`${target.name} 身中 ${effect.name},毒性发作,损失 ${effect.value} 点气血!`,
);
break;
case "spirit_up":
case 'spirit_up':
// 灵力提升效果:每回合持续生效,提升灵力
target.attributes.spirit = clamp(
target.attributes.spirit + effect.value,
40,
120
120,
);
break;
case "vitality_up":
case 'vitality_up':
// 体魄提升效果:每回合持续生效,提升体魄
target.attributes.vitality = clamp(
target.attributes.vitality + effect.value,
40,
120
120,
);
break;
case "wisdom_up":
case 'wisdom_up':
// 悟性提升效果:每回合持续生效,提升悟性
target.attributes.wisdom = clamp(
target.attributes.wisdom + effect.value,
40,
120
120,
);
break;
case "speed_up":
case 'speed_up':
// 速度提升效果:每回合持续生效,提升速度
target.attributes.speed = clamp(
target.attributes.speed + effect.value,
40,
120
120,
);
break;
}
@@ -293,7 +293,7 @@ function processActiveEffects(target: Combatant, log: string[]) {
newEffects.push(effect);
} else {
// 效果结束提示
if (effect.type !== "stun") {
if (effect.type !== 'stun') {
log.push(`${target.name} 身上的 ${effect.name} 效果消失了。`);
}
}
@@ -307,102 +307,102 @@ function applyEffects(
target: Combatant,
effects: string[],
source: string,
log: string[]
log: string[],
) {
if (!effects || effects.length === 0) return;
for (const effect of effects) {
switch (effect) {
case "bleed":
case 'bleed':
// 流血效果:持续3回合,每回合造成10点伤害
target.activeEffects.push({
name: "流血",
type: "bleed",
name: '流血',
type: 'bleed',
value: 2,
duration: 5,
source,
});
log.push(`${target.name} 被 ${source} 的招式击中,伤口血流不止!`);
break;
case "burn":
case 'burn':
// 燃烧效果:持续2回合,每回合造成15点伤害
target.activeEffects.push({
name: "烈焰",
type: "burn",
name: '烈焰',
type: 'burn',
value: 6,
duration: 2,
source,
});
log.push(`${target.name} 被 ${source} 的招式击中,身上燃起熊熊烈焰!`);
break;
case "stun":
case 'stun':
// 眩晕效果:当前回合无法行动(需要在行动前检查)
target.activeEffects.push({
name: "眩晕",
type: "stun",
name: '眩晕',
type: 'stun',
value: 0,
duration: 1,
source,
});
log.push(
`${target.name} 被 ${source} 的招式击中,头晕目眩,无法行动!`
`${target.name} 被 ${source} 的招式击中,头晕目眩,无法行动!`,
);
break;
case "poison":
case 'poison':
// 中毒效果:持续4回合,每回合造成8点伤害
target.activeEffects.push({
name: "剧毒",
type: "poison",
name: '剧毒',
type: 'poison',
value: 6,
duration: 2,
source,
});
log.push(`${target.name} 中了 ${source} 的毒,毒素开始扩散!`);
break;
case "heal":
case 'heal':
// 治疗效果:立即回复生命值
const healAmount = Math.round(target.maxHp * 0.2); // 回复20%最大生命值
target.hp = clamp(target.hp + healAmount, 0, target.maxHp);
log.push(`${target.name} 受到治疗效果,回复 ${healAmount} 点生命值!`);
break;
case "spirit_up":
case 'spirit_up':
// 灵力提升效果:持续3回合,提升10点灵力
target.activeEffects.push({
name: "灵力提升",
type: "spirit_up",
name: '灵力提升',
type: 'spirit_up',
value: 10,
duration: 3,
source,
});
log.push(`${target.name} 灵力涌动,精神大振!`);
break;
case "vitality_up":
case 'vitality_up':
// 体魄提升效果:持续3回合,提升10点体魄
target.activeEffects.push({
name: "体魄提升",
type: "vitality_up",
name: '体魄提升',
type: 'vitality_up',
value: 10,
duration: 3,
source,
});
log.push(`${target.name} 体魄增强,气血旺盛!`);
break;
case "wisdom_up":
case 'wisdom_up':
// 悟性提升效果:持续3回合,提升10点悟性
target.activeEffects.push({
name: "悟性提升",
type: "wisdom_up",
name: '悟性提升',
type: 'wisdom_up',
value: 10,
duration: 3,
source,
});
log.push(`${target.name} 悟性大开,灵台清明!`);
break;
case "speed_up":
case 'speed_up':
// 速度提升效果:持续3回合,提升10点速度
target.activeEffects.push({
name: "速度提升",
type: "speed_up",
name: '速度提升',
type: 'speed_up',
value: 10,
duration: 3,
source,
@@ -415,10 +415,10 @@ function applyEffects(
function selectSkill(attacker: Combatant, defender: Combatant): Skill {
const attackSkills = attacker.skills.filter(
(s) => s.type === "attack" || s.type === "control"
(s) => s.type === 'attack' || s.type === 'control',
);
const healSkills = attacker.skills.filter((s) => s.type === "heal");
const buffSkills = attacker.skills.filter((s) => s.type === "buff");
const healSkills = attacker.skills.filter((s) => s.type === 'heal');
const buffSkills = attacker.skills.filter((s) => s.type === 'buff');
if (attacker.hp < attacker.maxHp * 0.3 && healSkills.length) {
return randomChoice(healSkills, attacker.skills[0]);
@@ -442,7 +442,7 @@ function selectSkill(attacker: Combatant, defender: Combatant): Skill {
function calculateDamage(
attacker: Combatant,
defender: Combatant,
skill: Skill
skill: Skill,
) {
let basePower = skill.power;
let spirit = attacker.attributes.spirit;
@@ -490,7 +490,7 @@ function calculateDamage(
damage,
isCrit,
elementNote:
elementMod > 1 ? "(属性克制)" : elementMod < 1 ? "(被克制)" : "",
elementMod > 1 ? '(属性克制)' : elementMod < 1 ? '(被克制)' : '',
};
}
@@ -500,8 +500,8 @@ function applyDamage(target: Combatant, damage: number) {
}
function getElementModifier(
attacking: ElementType = "无",
defending: ElementType = "无"
attacking: ElementType = '无',
defending: ElementType = '无',
) {
if (!attacking || !defending) return 1;
return ELEMENT_MOD[`${attacking}-${defending}`] ?? 1;
+9 -9
View File
@@ -1,14 +1,14 @@
"use client";
'use client';
import { Session, User } from '@supabase/supabase-js';
import React, {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from "react";
import { createClient } from "../supabase/client";
import { Session, User } from "@supabase/supabase-js";
useContext,
useEffect,
useState,
} from 'react';
import { createClient } from '../supabase/client';
interface AuthContextType {
session: Session | null;
@@ -21,7 +21,7 @@ interface AuthContextType {
createAnonymousUser: () => Promise<{ error: any }>;
linkAnonymousUser: (
email: string,
password: string
password: string,
) => Promise<{ error: any }>;
}
@@ -119,7 +119,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
+3 -3
View File
@@ -1,6 +1,6 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const connectionString = process.env.DATABASE_URL;
+62 -62
View File
@@ -1,100 +1,100 @@
import {
pgTable,
varchar,
text,
integer,
jsonb,
pgTable,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core";
varchar,
} from 'drizzle-orm/pg-core';
// 角色表
export const cultivators = pgTable("wanjiedaoyou_cultivators", {
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id").notNull(),
name: varchar("name", { length: 100 }).notNull(),
prompt: text("prompt").notNull(),
cultivationLevel: varchar("cultivation_level", { length: 50 }).notNull(),
spiritRoot: varchar("spirit_root", { length: 50 }).notNull(),
appearance: text("appearance"),
backstory: text("backstory"),
gender: varchar("gender", { length: 20 }),
origin: varchar("origin", { length: 100 }),
personality: text("personality"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at")
export const cultivators = pgTable('wanjiedaoyou_cultivators', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull(),
name: varchar('name', { length: 100 }).notNull(),
prompt: text('prompt').notNull(),
cultivationLevel: varchar('cultivation_level', { length: 50 }).notNull(),
spiritRoot: varchar('spirit_root', { length: 50 }).notNull(),
appearance: text('appearance'),
backstory: text('backstory'),
gender: varchar('gender', { length: 20 }),
origin: varchar('origin', { length: 100 }),
personality: text('personality'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at')
.defaultNow()
.$onUpdate(() => new Date()),
});
// 战斗属性表
export const battleProfiles = pgTable("wanjiedaoyou_battle_profiles", {
id: uuid("id").primaryKey().defaultRandom(),
cultivatorId: uuid("cultivator_id")
export const battleProfiles = pgTable('wanjiedaoyou_battle_profiles', {
id: uuid('id').primaryKey().defaultRandom(),
cultivatorId: uuid('cultivator_id')
.references(() => cultivators.id)
.notNull()
.unique(),
maxHp: integer("max_hp").notNull(),
hp: integer("hp").notNull(),
vitality: integer("vitality").notNull(),
spirit: integer("spirit").notNull(),
wisdom: integer("wisdom").notNull(),
speed: integer("speed").notNull(),
element: varchar("element", { length: 20 }),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at")
maxHp: integer('max_hp').notNull(),
hp: integer('hp').notNull(),
vitality: integer('vitality').notNull(),
spirit: integer('spirit').notNull(),
wisdom: integer('wisdom').notNull(),
speed: integer('speed').notNull(),
element: varchar('element', { length: 20 }),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at')
.defaultNow()
.$onUpdate(() => new Date()),
});
// 技能表
export const skills = pgTable("wanjiedaoyou_skills", {
id: uuid("id").primaryKey().defaultRandom(),
cultivatorId: uuid("cultivator_id")
export const skills = pgTable('wanjiedaoyou_skills', {
id: uuid('id').primaryKey().defaultRandom(),
cultivatorId: uuid('cultivator_id')
.references(() => cultivators.id)
.notNull(),
name: varchar("name", { length: 100 }).notNull(),
type: varchar("type", { length: 20 }).notNull(), // attack, heal, control, buff
power: integer("power").notNull(),
element: varchar("element", { length: 20 }).notNull(),
effects: jsonb("effects"),
createdAt: timestamp("created_at").defaultNow(),
name: varchar('name', { length: 100 }).notNull(),
type: varchar('type', { length: 20 }).notNull(), // attack, heal, control, buff
power: integer('power').notNull(),
element: varchar('element', { length: 20 }).notNull(),
effects: jsonb('effects'),
createdAt: timestamp('created_at').defaultNow(),
});
// 装备表
export const equipment = pgTable("wanjiedaoyou_equipment", {
id: uuid("id").primaryKey().defaultRandom(),
cultivatorId: uuid("cultivator_id")
export const equipment = pgTable('wanjiedaoyou_equipment', {
id: uuid('id').primaryKey().defaultRandom(),
cultivatorId: uuid('cultivator_id')
.references(() => cultivators.id)
.notNull(),
name: varchar("name", { length: 100 }).notNull(),
bonus: jsonb("bonus"),
createdAt: timestamp("created_at").defaultNow(),
name: varchar('name', { length: 100 }).notNull(),
bonus: jsonb('bonus'),
createdAt: timestamp('created_at').defaultNow(),
});
// 先天命格表
export const preHeavenFates = pgTable("wanjiedaoyou_pre_heaven_fates", {
id: uuid("id").primaryKey().defaultRandom(),
cultivatorId: uuid("cultivator_id")
export const preHeavenFates = pgTable('wanjiedaoyou_pre_heaven_fates', {
id: uuid('id').primaryKey().defaultRandom(),
cultivatorId: uuid('cultivator_id')
.references(() => cultivators.id)
.notNull(),
name: varchar("name", { length: 100 }).notNull(),
type: varchar("type", { length: 10 }).notNull(), // 吉, 凶
effect: text("effect").notNull(),
description: text("description").notNull(),
createdAt: timestamp("created_at").defaultNow(),
name: varchar('name', { length: 100 }).notNull(),
type: varchar('type', { length: 10 }).notNull(), // 吉, 凶
effect: text('effect').notNull(),
description: text('description').notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
// 活跃效果表
export const activeEffects = pgTable("wanjiedaoyou_active_effects", {
id: uuid("id").primaryKey().defaultRandom(),
battleProfileId: uuid("battle_profile_id")
export const activeEffects = pgTable('wanjiedaoyou_active_effects', {
id: uuid('id').primaryKey().defaultRandom(),
battleProfileId: uuid('battle_profile_id')
.references(() => battleProfiles.id)
.notNull(),
name: varchar("name", { length: 100 }).notNull(),
type: varchar("type", { length: 50 }).notNull(),
value: integer("value").notNull(),
duration: integer("duration").notNull(),
source: varchar("source", { length: 100 }).notNull(),
createdAt: timestamp("created_at").defaultNow(),
name: varchar('name', { length: 100 }).notNull(),
type: varchar('type', { length: 50 }).notNull(),
value: integer('value').notNull(),
duration: integer('duration').notNull(),
source: varchar('source', { length: 100 }).notNull(),
createdAt: timestamp('created_at').defaultNow(),
});
+41 -47
View File
@@ -1,18 +1,12 @@
import { db } from "../drizzle/db";
import * as schema from "../drizzle/schema";
import { eq, and } from "drizzle-orm";
import type {
Cultivator,
BattleProfile,
Skill,
Equipment,
PreHeavenFate,
} from "../../types/cultivator";
import { and, eq } from 'drizzle-orm';
import type { BattleProfile, Cultivator } from '../../types/cultivator';
import { db } from '../drizzle/db';
import * as schema from '../drizzle/schema';
export async function createCultivator(
userId: string,
cultivatorData: Omit<Cultivator, "id" | "battleProfile">,
battleProfile: BattleProfile
cultivatorData: Omit<Cultivator, 'id' | 'battleProfile'>,
battleProfile: BattleProfile,
): Promise<Cultivator> {
// 开始事务,确保原子性操作
const result = await db.transaction(async (tx) => {
@@ -60,7 +54,7 @@ export async function createCultivator(
power: skill.power,
element: skill.element,
effects: skill.effects ? JSON.stringify(skill.effects) : undefined,
}))
})),
);
}
@@ -71,7 +65,7 @@ export async function createCultivator(
cultivatorId: cultivator.id,
name: equipment.name,
bonus: equipment.bonus ? JSON.stringify(equipment.bonus) : undefined,
}))
})),
);
}
@@ -87,7 +81,7 @@ export async function createCultivator(
type: fate.type,
effect: fate.effect,
description: fate.description,
}))
})),
);
}
@@ -104,7 +98,7 @@ export async function createCultivator(
export async function getCultivatorById(
userId: string,
cultivatorId: string
cultivatorId: string,
): Promise<Cultivator | null> {
// 使用join查询获取角色基本信息和战斗属性
const cultivatorWithBattleProfile = await db
@@ -115,13 +109,13 @@ export async function getCultivatorById(
.from(schema.cultivators)
.innerJoin(
schema.battleProfiles,
eq(schema.battleProfiles.cultivatorId, schema.cultivators.id)
eq(schema.battleProfiles.cultivatorId, schema.cultivators.id),
)
.where(
and(
eq(schema.cultivators.id, cultivatorId),
eq(schema.cultivators.userId, userId)
)
eq(schema.cultivators.userId, userId),
),
);
if (cultivatorWithBattleProfile.length === 0) {
@@ -155,14 +149,14 @@ export async function getCultivatorById(
prompt: cultivator.prompt,
cultivationLevel: cultivator.cultivationLevel,
spiritRoot: cultivator.spiritRoot,
appearance: cultivator.appearance || "",
backstory: cultivator.backstory || "",
appearance: cultivator.appearance || '',
backstory: cultivator.backstory || '',
gender: cultivator.gender || undefined,
origin: cultivator.origin || undefined,
personality: cultivator.personality || undefined,
preHeavenFates: preHeavenFatesResult.map((fate) => ({
name: fate.name,
type: fate.type as "吉" | "凶",
type: fate.type as '吉' | '凶',
effect: fate.effect,
description: fate.description,
})),
@@ -177,7 +171,7 @@ export async function getCultivatorById(
},
skills: skillsResult.map((skill) => ({
name: skill.name,
type: skill.type as "attack" | "heal" | "control" | "buff",
type: skill.type as 'attack' | 'heal' | 'control' | 'buff',
power: skill.power,
element: skill.element as any,
effects: skill.effects
@@ -198,7 +192,7 @@ export async function getCultivatorById(
}
export async function getCultivatorsByUserId(
userId: string
userId: string,
): Promise<Cultivator[]> {
// 获取用户的所有角色基本信息和战斗属性
const cultivatorsWithBattleProfiles = await db
@@ -209,7 +203,7 @@ export async function getCultivatorsByUserId(
.from(schema.cultivators)
.innerJoin(
schema.battleProfiles,
eq(schema.battleProfiles.cultivatorId, schema.cultivators.id)
eq(schema.battleProfiles.cultivatorId, schema.cultivators.id),
)
.where(eq(schema.cultivators.userId, userId));
@@ -240,14 +234,14 @@ export async function getCultivatorsByUserId(
prompt: cultivator.prompt,
cultivationLevel: cultivator.cultivationLevel,
spiritRoot: cultivator.spiritRoot,
appearance: cultivator.appearance || "",
backstory: cultivator.backstory || "",
appearance: cultivator.appearance || '',
backstory: cultivator.backstory || '',
gender: cultivator.gender || undefined,
origin: cultivator.origin || undefined,
personality: cultivator.personality || undefined,
preHeavenFates: preHeavenFatesResult.map((fate) => ({
name: fate.name,
type: fate.type as "吉" | "凶",
type: fate.type as '吉' | '凶',
effect: fate.effect,
description: fate.description,
})),
@@ -262,7 +256,7 @@ export async function getCultivatorsByUserId(
},
skills: skillsResult.map((skill) => ({
name: skill.name,
type: skill.type as "attack" | "heal" | "control" | "buff",
type: skill.type as 'attack' | 'heal' | 'control' | 'buff',
power: skill.power,
element: skill.element as any,
effects: skill.effects
@@ -278,8 +272,8 @@ export async function getCultivatorsByUserId(
element: battleProfileData.element as any,
},
} as Cultivator;
}
)
},
),
);
return fullCultivators;
@@ -288,7 +282,7 @@ export async function getCultivatorsByUserId(
export async function updateCultivator(
userId: string,
cultivatorId: string,
updates: Partial<Omit<Cultivator, "id" | "battleProfile">>
updates: Partial<Omit<Cultivator, 'id' | 'battleProfile'>>,
): Promise<Cultivator | null> {
// 权限验证:确保只有角色所有者可以更新角色
const existingCultivator = await db
@@ -297,8 +291,8 @@ export async function updateCultivator(
.where(
and(
eq(schema.cultivators.id, cultivatorId),
eq(schema.cultivators.userId, userId)
)
eq(schema.cultivators.userId, userId),
),
);
if (existingCultivator.length === 0) {
@@ -312,8 +306,8 @@ export async function updateCultivator(
.where(
and(
eq(schema.cultivators.id, cultivatorId),
eq(schema.cultivators.userId, userId)
)
eq(schema.cultivators.userId, userId),
),
);
// 返回更新后的完整角色信息
@@ -322,7 +316,7 @@ export async function updateCultivator(
export async function deleteCultivator(
userId: string,
cultivatorId: string
cultivatorId: string,
): Promise<boolean> {
// 权限验证:确保只有角色所有者可以删除角色
const existingCultivator = await db
@@ -331,8 +325,8 @@ export async function deleteCultivator(
.where(
and(
eq(schema.cultivators.id, cultivatorId),
eq(schema.cultivators.userId, userId)
)
eq(schema.cultivators.userId, userId),
),
);
if (existingCultivator.length === 0) {
@@ -367,8 +361,8 @@ export async function deleteCultivator(
.where(
and(
eq(schema.cultivators.id, cultivatorId),
eq(schema.cultivators.userId, userId)
)
eq(schema.cultivators.userId, userId),
),
);
});
@@ -378,7 +372,7 @@ export async function deleteCultivator(
export async function updateBattleProfile(
userId: string,
cultivatorId: string,
updates: Partial<BattleProfile>
updates: Partial<BattleProfile>,
): Promise<BattleProfile | null> {
// 权限验证:确保只有角色所有者可以更新战斗属性
const existingCultivator = await db
@@ -387,8 +381,8 @@ export async function updateBattleProfile(
.where(
and(
eq(schema.cultivators.id, cultivatorId),
eq(schema.cultivators.userId, userId)
)
eq(schema.cultivators.userId, userId),
),
);
if (existingCultivator.length === 0) {
@@ -440,7 +434,7 @@ export async function updateBattleProfile(
power: skill.power,
element: skill.element,
effects: skill.effects ? JSON.stringify(skill.effects) : undefined,
}))
})),
);
}
@@ -457,7 +451,7 @@ export async function updateBattleProfile(
cultivatorId,
name: equipment.name,
bonus: equipment.bonus ? JSON.stringify(equipment.bonus) : undefined,
}))
})),
);
}
});
@@ -497,7 +491,7 @@ export async function updateBattleProfile(
},
skills: skillsResult.map((skill) => ({
name: skill.name,
type: skill.type as "attack" | "heal" | "control" | "buff",
type: skill.type as 'attack' | 'heal' | 'control' | 'buff',
power: skill.power,
element: skill.element as any,
effects: skill.effects ? JSON.parse(skill.effects as string) : undefined,
+4 -1
View File
@@ -3,7 +3,10 @@
* @param anonymousUserId 匿名用户ID
* @param registeredUserId 注册用户ID
*/
export async function migrateAnonymousUserData(anonymousUserId: string, registeredUserId: string): Promise<boolean> {
export async function migrateAnonymousUserData(
anonymousUserId: string,
registeredUserId: string,
): Promise<boolean> {
try {
// 这里为了简化,我们只返回true
return true;
+2 -2
View File
@@ -1,8 +1,8 @@
import { createBrowserClient } from "@supabase/ssr";
import { createBrowserClient } from '@supabase/ssr';
export const createClient = () => {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
};
+3 -3
View File
@@ -1,5 +1,5 @@
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
@@ -14,7 +14,7 @@ export const createClient = async () => {
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
cookieStore.set(name, value, options),
);
} catch {
// The `setAll` method was called from a Server Component.
+1 -1
View File
@@ -1,4 +1,4 @@
import type { NextConfig } from "next";
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
/* config options here */
+158
View File
@@ -27,6 +27,8 @@
"eslint": "^9",
"eslint-config-next": "16.0.4",
"prettier": "^3.7.2",
"prettier-plugin-organize-imports": "^4.3.0",
"prettier-plugin-packagejson": "^2.5.20",
"prettier-plugin-tailwindcss": "^0.7.1",
"tailwindcss": "^4",
"typescript": "^5"
@@ -2120,6 +2122,19 @@
"node": ">=12.4.0"
}
},
"node_modules/@pkgr/core": {
"version": "0.2.9",
"resolved": "https://registry.npmmirror.com/@pkgr/core/-/core-0.2.9.tgz",
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -3788,6 +3803,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/detect-indent": {
"version": "7.0.2",
"resolved": "https://registry.npmmirror.com/detect-indent/-/detect-indent-7.0.2.tgz",
"integrity": "sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -3798,6 +3826,19 @@
"node": ">=8"
}
},
"node_modules/detect-newline": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/detect-newline/-/detect-newline-4.0.1.tgz",
"integrity": "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz",
@@ -4946,6 +4987,16 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/git-hooks-list": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/git-hooks-list/-/git-hooks-list-4.1.1.tgz",
"integrity": "sha512-cmP497iLq54AZnv4YRAEMnEyQ1eIn4tGKbmswqwmFV4GBnAqE8NLtWxxdXa++AalfgL5EBH4IxTPyquEuGY/jA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/fisker/git-hooks-list?sponsor=1"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -5458,6 +5509,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-plain-obj": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz",
@@ -6630,6 +6694,42 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.3.0.tgz",
"integrity": "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"prettier": ">=2.0",
"typescript": ">=2.9",
"vue-tsc": "^2.1.0 || 3"
},
"peerDependenciesMeta": {
"vue-tsc": {
"optional": true
}
}
},
"node_modules/prettier-plugin-packagejson": {
"version": "2.5.20",
"resolved": "https://registry.npmmirror.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.20.tgz",
"integrity": "sha512-G8cowPh+QmJJECTZlrPDKWkVVcwrFjF2rGcw546w3N8blLoc4szSs8UUPfFVxHUNLUjiru71Ah83g1lZkeK9Bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"sort-package-json": "3.5.0",
"synckit": "0.11.11"
},
"peerDependencies": {
"prettier": ">= 1.16.0"
},
"peerDependenciesMeta": {
"prettier": {
"optional": true
}
}
},
"node_modules/prettier-plugin-tailwindcss": {
"version": "0.7.1",
"resolved": "https://registry.npmmirror.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.7.1.tgz",
@@ -7177,6 +7277,48 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sort-object-keys": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/sort-object-keys/-/sort-object-keys-2.0.1.tgz",
"integrity": "sha512-R89fO+z3x7hiKPXX5P0qim+ge6Y60AjtlW+QQpRozrrNcR1lw9Pkpm5MLB56HoNvdcLHL4wbpq16OcvGpEDJIg==",
"dev": true,
"license": "MIT"
},
"node_modules/sort-package-json": {
"version": "3.5.0",
"resolved": "https://registry.npmmirror.com/sort-package-json/-/sort-package-json-3.5.0.tgz",
"integrity": "sha512-moY4UtptUuP5sPuu9H9dp8xHNel7eP5/Kz/7+90jTvC0IOiPH2LigtRM/aSFSxreaWoToHUVUpEV4a2tAs2oKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-indent": "^7.0.1",
"detect-newline": "^4.0.1",
"git-hooks-list": "^4.0.0",
"is-plain-obj": "^4.1.0",
"semver": "^7.7.1",
"sort-object-keys": "^2.0.0",
"tinyglobby": "^0.2.12"
},
"bin": {
"sort-package-json": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/sort-package-json/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
@@ -7413,6 +7555,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/synckit": {
"version": "0.11.11",
"resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.11.11.tgz",
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.2.9"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/synckit"
}
},
"node_modules/tailwindcss": {
"version": "4.1.17",
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.17.tgz",
+6 -3
View File
@@ -3,10 +3,11 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
"dev": "next dev",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
"lint": "eslint",
"start": "next start"
},
"dependencies": {
"@supabase/auth-helpers-nextjs": "^0.15.0",
@@ -28,6 +29,8 @@
"eslint": "^9",
"eslint-config-next": "16.0.4",
"prettier": "^3.7.2",
"prettier-plugin-organize-imports": "^4.3.0",
"prettier-plugin-packagejson": "^2.5.20",
"prettier-plugin-tailwindcss": "^0.7.1",
"tailwindcss": "^4",
"typescript": "^5"
+41 -41
View File
@@ -23,13 +23,13 @@
## 🎨 核心视觉元素(SVG 推荐)
| 元素 | 用途 | 设计建议 |
| -------------- | -------------------- | ------------------------------------------------------------- |
| **卷轴边框** | 包裹角色卡、战斗播报 | 两端木轴 + 中间纸纹,可用 `<svg>` 绘制,或 CSS `border-image` |
| **符箓底纹** | 按钮背景 / 卡片装饰 | 简化道教符咒线条,单色(墨或朱砂),半透明叠加 |
| **丹炉图标** | “生成”按钮图标 | 小型 SVG,炉中冒烟(CSS 动画) |
| **云纹分隔线** | 页面区块分隔 | 底部波浪形云纹 SVG,淡墨色 |
| **印章** | 胜利标识 / Logo | “道”“胜”“灵”等篆体红印,PNG/SVG |
| 元素 | 用途 | 设计建议 |
| --- | --- | --- |
| **卷轴边框** | 包裹角色卡、战斗播报 | 两端木轴 + 中间纸纹,可用 `<svg>` 绘制,或 CSS `border-image` |
| **符箓底纹** | 按钮背景 / 卡片装饰 | 简化道教符咒线条,单色(墨或朱砂),半透明叠加 |
| **丹炉图标** | “生成”按钮图标 | 小型 SVG,炉中冒烟(CSS 动画) |
| **云纹分隔线** | 页面区块分隔 | 底部波浪形云纹 SVG,淡墨色 |
| **印章** | 胜利标识 / Logo | “道”“胜”“灵”等篆体红印,PNG/SVG |
> ✅ 所有 SVG 均可内联到 HTML,避免额外请求,且支持动态上色。
@@ -45,34 +45,34 @@
<!-- 宣纸纹理背景 -->
<!-- 顶部标题 -->
<h1 class="font-ma-shan-zheng text-4xl text-ink mb-6 text-center">
<h1 class="font-ma-shan-zheng text-ink mb-6 text-center text-4xl">
万界道录
</h1>
<p class="text-ink/70 text-center mb-8">输入心念,凝练道身</p>
<p class="text-ink/70 mb-8 text-center">输入心念,凝练道身</p>
<!-- 主按钮:仿丹炉 -->
<button class="btn-primary flex items-center justify-center mx-auto mb-10">
<svg class="w-6 h-6 mr-2" viewBox="0 0 24 24">...</svg>
<button class="btn-primary mx-auto mb-10 flex items-center justify-center">
<svg class="mr-2 h-6 w-6" viewBox="0 0 24 24">...</svg>
<!-- 丹炉 SVG -->
觉醒灵根
</button>
<!-- 排行榜:仿古籍名录 -->
<div class="ranking-list max-w-md mx-auto">
<h2 class="font-ma-shan-zheng text-xl text-ink mb-4 flex items-center">
<div class="ranking-list mx-auto max-w-md">
<h2 class="font-ma-shan-zheng text-ink mb-4 flex items-center text-xl">
<span>天榜前十</span>
<svg class="ml-2 w-5 h-5" fill="#c1121f">...</svg>
<svg class="ml-2 h-5 w-5" fill="#c1121f">...</svg>
<!-- 小火焰或龙纹 -->
</h2>
<!-- 每个条目:仿竹简 or 名帖 -->
<div class="ranking-item border-b border-ink/10 py-3">
<div class="flex justify-between items-center">
<div class="ranking-item border-ink/10 border-b py-3">
<div class="flex items-center justify-between">
<span class="font-ma-shan-zheng">玄霄子</span>
<span class="text-sm text-ink/80">元婴初期 · 战力 892</span>
<span class="text-ink/80 text-sm">元婴初期 · 战力 892</span>
</div>
<div class="text-xs text-ink/60 mt-1">剑心通明|雷劫不灭</div>
<button class="text-xs text-crimson mt-2">挑战</button>
<div class="text-ink/60 mt-1 text-xs">剑心通明|雷劫不灭</div>
<button class="text-crimson mt-2 text-xs">挑战</button>
</div>
</div>
@@ -95,26 +95,26 @@
<div class="page-create bg-paper p-6">
<!-- 输入区:仿砚台 -->
<div class="input-area mb-8">
<label class="block font-ma-shan-zheng text-ink mb-2">以心念唤道:</label>
<label class="font-ma-shan-zheng text-ink mb-2 block">以心念唤道:</label>
<textarea
placeholder="例:我想成为一位靠炼丹逆袭的废柴少主..."
class="w-full h-32 p-4 bg-paper-light border border-ink/20 rounded-lg focus:ring-1 focus:ring-crimson"
class="bg-paper-light border-ink/20 focus:ring-crimson h-32 w-full rounded-lg border p-4 focus:ring-1"
></textarea>
</div>
<!-- 生成按钮 -->
<button class="btn-primary mx-auto block mb-10">
<svg class="w-5 h-5 inline mr-1">...</svg> 凝气成形
<button class="btn-primary mx-auto mb-10 block">
<svg class="mr-1 inline h-5 w-5">...</svg> 凝气成形
</button>
<!-- 角色卡:仿卷轴 -->
{generated && (
<div class="character-scroll max-w-lg mx-auto">
<div class="character-scroll mx-auto max-w-lg">
<div
class="scroll-content p-6 bg-white/80 backdrop-blur-sm border border-ink/10 rounded"
class="scroll-content border-ink/10 rounded border bg-white/80 p-6 backdrop-blur-sm"
>
<h3 class="font-ma-shan-zheng text-2xl text-ink mb-2">{name}</h3>
<div class="grid grid-cols-2 gap-2 text-sm mb-4">
<h3 class="font-ma-shan-zheng text-ink mb-2 text-2xl">{name}</h3>
<div class="mb-4 grid grid-cols-2 gap-2 text-sm">
<div><span class="text-ink/70">境界:</span>{cultivation_level}</div>
<div><span class="text-ink/70">灵根:</span>{spirit_root}</div>
</div>
@@ -129,7 +129,7 @@
)}
<!-- 底部操作 -->
<div class="flex justify-center gap-4 mt-6">
<div class="mt-6 flex justify-center gap-4">
<button class="btn-outline">重凝</button>
<button class="btn-primary">入世对战</button>
</div>
@@ -142,7 +142,7 @@
```css
.character-scroll::before,
.character-scroll::after {
content: "";
content: '';
display: block;
height: 20px;
background: #8b4513; /* 木色 */
@@ -158,46 +158,46 @@
```html
<div class="battle-page bg-paper p-4">
<!-- 对战双方(左右分列,仿对战图谱)-->
<div class="flex justify-between mb-8 px-4">
<div class="mb-8 flex justify-between px-4">
<div class="text-center">
<div class="font-ma-shan-zheng text-lg text-ink">{player.name}</div>
<div class="text-xs text-ink/70">{player.cultivation_level}</div>
<div class="font-ma-shan-zheng text-ink text-lg">{player.name}</div>
<div class="text-ink/70 text-xs">{player.cultivation_level}</div>
</div>
<div class="text-center">
<div class="font-ma-shan-zheng text-lg text-ink">{opponent.name}</div>
<div class="text-xs text-ink/70">{opponent.cultivation_level}</div>
<div class="font-ma-shan-zheng text-ink text-lg">{opponent.name}</div>
<div class="text-ink/70 text-xs">{opponent.cultivation_level}</div>
</div>
</div>
<!-- 战斗播报:仿古籍批注 -->
<div
class="narrative-box max-w-lg mx-auto p-6 bg-paper-light border border-ink/10 rounded relative"
class="narrative-box bg-paper-light border-ink/10 relative mx-auto max-w-lg rounded border p-6"
>
<!-- 左侧朱批竖线 -->
<div
class="absolute left-2 top-2 bottom-2 w-1 bg-crimson rounded-full opacity-30"
class="bg-crimson absolute top-2 bottom-2 left-2 w-1 rounded-full opacity-30"
></div>
<p class="text-ink leading-relaxed text-center whitespace-pre-line">
<p class="text-ink text-center leading-relaxed whitespace-pre-line">
{narrativeText}
</p>
<!-- 胜利印章(条件渲染)-->
{isWin && (
<div class="absolute -top-4 -right-4">
<svg class="w-16 h-16" fill="#c1121f" opacity="0.8">...</svg>
<svg class="h-16 w-16" fill="#c1121f" opacity="0.8">...</svg>
<!-- “胜”字篆印 -->
</div>
)}
</div>
<!-- 操作按钮 -->
<div class="flex justify-center gap-4 mt-8">
<div class="mt-8 flex justify-center gap-4">
<button class="btn-outline">再战</button>
<button class="btn-primary">载入道录</button>
<!-- 存入排行榜 -->
<button class="btn-outline flex items-center">
<svg class="w-4 h-4 mr-1">...</svg> 分享
<svg class="mr-1 h-4 w-4">...</svg> 分享
</button>
</div>
</div>
@@ -219,7 +219,7 @@
```css
.bg-paper {
background-color: #f8f3e6;
background-image: url("/textures/paper.png");
background-image: url('/textures/paper.png');
background-size: 300px;
}
```
+26 -14
View File
@@ -3,7 +3,7 @@ import OpenAI from 'openai';
/**
* AI 客户端工具
* 用于调用 OpenAI API 生成角色和战斗播报
*
*
* 环境变量配置:
* - OPENAI_API_KEY: OpenAI API Key(必需)
* - OPENAI_BASE_URL: 自定义 API 地址(可选,用于兼容其他 OpenAI 兼容的 API)
@@ -13,13 +13,15 @@ import OpenAI from 'openai';
// 初始化 OpenAI 客户端
function getOpenAIClient(): OpenAI {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error('OPENAI_API_KEY 环境变量未设置。请在 .env.local 文件中配置 OPENAI_API_KEY');
throw new Error(
'OPENAI_API_KEY 环境变量未设置。请在 .env.local 文件中配置 OPENAI_API_KEY',
);
}
const baseURL = process.env.OPENAI_BASE_URL;
return new OpenAI({
apiKey,
...(baseURL && { baseURL }), // 如果设置了自定义 baseURL,则使用它(用于兼容其他 API)
@@ -32,7 +34,10 @@ function getOpenAIClient(): OpenAI {
* @param userInput 用户输入
* @returns 生成的角色 JSON 字符串
*/
export async function generateCharacter(prompt: string, userInput: string): Promise<string> {
export async function generateCharacter(
prompt: string,
userInput: string,
): Promise<string> {
try {
const client = getOpenAIClient();
const model = process.env.OPENAI_MODEL || 'gpt-4o-mini';
@@ -54,7 +59,7 @@ export async function generateCharacter(prompt: string, userInput: string): Prom
});
const content = response.choices[0]?.message?.content;
if (!content) {
throw new Error('AI 返回的内容为空');
}
@@ -62,7 +67,7 @@ export async function generateCharacter(prompt: string, userInput: string): Prom
return content;
} catch (error) {
console.error('生成角色失败:', error);
if (error instanceof Error) {
// 处理 OpenAI API 错误
if (error.message.includes('API key')) {
@@ -75,8 +80,10 @@ export async function generateCharacter(prompt: string, userInput: string): Prom
throw new Error('API 配额不足,请检查账户余额');
}
}
throw new Error(`生成角色失败: ${error instanceof Error ? error.message : '未知错误'}`);
throw new Error(
`生成角色失败: ${error instanceof Error ? error.message : '未知错误'}`,
);
}
}
@@ -90,7 +97,7 @@ export async function generateCharacter(prompt: string, userInput: string): Prom
export async function generateBattleReportStream(
prompt: string,
userPrompt: string,
onChunk: (chunk: string) => void
onChunk: (chunk: string) => void,
): Promise<string> {
try {
const client = getOpenAIClient();
@@ -126,7 +133,7 @@ export async function generateBattleReportStream(
return fullContent.trim();
} catch (error) {
console.error('生成战斗播报失败:', error);
if (error instanceof Error) {
// 处理 OpenAI API 错误
if (error.message.includes('API key')) {
@@ -139,8 +146,10 @@ export async function generateBattleReportStream(
throw new Error('API 配额不足,请检查账户余额');
}
}
throw new Error(`生成战斗播报失败: ${error instanceof Error ? error.message : '未知错误'}`);
throw new Error(
`生成战斗播报失败: ${error instanceof Error ? error.message : '未知错误'}`,
);
}
}
@@ -149,7 +158,10 @@ export async function generateBattleReportStream(
* @param prompt 战斗播报 prompt
* @returns 生成的战斗播报文本
*/
export async function generateBattleReport(prompt: string, userPrompt: string): Promise<string> {
export async function generateBattleReport(
prompt: string,
userPrompt: string,
): Promise<string> {
let fullContent = '';
await generateBattleReportStream(prompt, userPrompt, (chunk) => {
fullContent += chunk;
+32 -27
View File
@@ -1,4 +1,10 @@
import { BattleProfile, BattleAttributes, Cultivator, ElementType, Skill } from '@/types/cultivator';
import {
BattleAttributes,
BattleProfile,
Cultivator,
ElementType,
Skill,
} from '@/types/cultivator';
const ELEMENT_KEYWORDS: Record<ElementType, string[]> = {
金: ['金', '剑', '锋', '铁', '金属'],
@@ -10,9 +16,18 @@ const ELEMENT_KEYWORDS: Record<ElementType, string[]> = {
无: [],
};
const ELEMENT_COUNTERPARTS: ElementType[] = ['金', '木', '水', '火', '土', '雷', '无'];
const ELEMENT_COUNTERPARTS: ElementType[] = [
'金',
'木',
'水',
'火',
'土',
'雷',
'无',
];
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
const clamp = (value: number, min: number, max: number) =>
Math.max(min, Math.min(max, value));
/**
* 根据灵根推断元素属性
@@ -44,7 +59,12 @@ function buildDefaultSkills(name: string, element: ElementType): Skill[] {
type: 'attack',
power: randomInt(70, 90),
element: primaryElement,
effects: primaryElement === '雷' ? ['stun'] : primaryElement === '火' ? ['burn'] : [],
effects:
primaryElement === '雷'
? ['stun']
: primaryElement === '火'
? ['burn']
: [],
};
const secondarySkill: Skill = {
@@ -83,37 +103,23 @@ const LEVEL_BASE_MAP: { keyword: string; base: number }[] = [
function deriveBaseFromLevel(level: string): number {
const normalized = level || '';
const entry = LEVEL_BASE_MAP.find(({ keyword }) => normalized.includes(keyword));
const entry = LEVEL_BASE_MAP.find(({ keyword }) =>
normalized.includes(keyword),
);
return entry?.base ?? 65;
}
export function generateDefaultBattleProfile(
cultivator: Cultivator,
seed?: Partial<BattleAttributes>
seed?: Partial<BattleAttributes>,
): BattleProfile {
const element = mapSpiritRootToElement(cultivator.spiritRoot);
const base = deriveBaseFromLevel(cultivator.cultivationLevel);
const vitality = clamp(
seed?.vitality ?? base + randomInt(-5, 5),
55,
100
);
const spirit = clamp(
seed?.spirit ?? base + randomInt(0, 10),
55,
100
);
const wisdom = clamp(
seed?.wisdom ?? base + randomInt(-10, 8),
55,
100
);
const speed = clamp(
seed?.speed ?? base + randomInt(-5, 5),
55,
100
);
const vitality = clamp(seed?.vitality ?? base + randomInt(-5, 5), 55, 100);
const spirit = clamp(seed?.spirit ?? base + randomInt(0, 10), 55, 100);
const wisdom = clamp(seed?.wisdom ?? base + randomInt(-10, 8), 55, 100);
const speed = clamp(seed?.speed ?? base + randomInt(-5, 5), 55, 100);
const attributes: BattleAttributes = {
vitality,
@@ -156,4 +162,3 @@ export function ensureBattleProfile(cultivator: Cultivator): BattleProfile {
export function cloneBattleProfile(profile: BattleProfile): BattleProfile {
return JSON.parse(JSON.stringify(profile));
}
+48 -48
View File
@@ -1,41 +1,41 @@
import type {
Cultivator,
BattleProfile,
BattleAttributes,
Skill,
BattleProfile,
Cultivator,
ElementType,
Equipment,
PreHeavenFate,
ElementType,
} from "../types/cultivator";
import { parseAIResponse } from "./aiClient";
Skill,
} from '../types/cultivator';
import { parseAIResponse } from './aiClient';
import {
generateDefaultBattleProfile,
mapSpiritRootToElement,
} from "./battleProfile";
} from './battleProfile';
export function createCultivatorFromAI(
aiResponse: string,
userPrompt: string
userPrompt: string,
): Cultivator {
const data = parseAIResponse(aiResponse);
const getString = (value: unknown, fallback: string) =>
typeof value === "string" && value.trim().length ? value : fallback;
typeof value === 'string' && value.trim().length ? value : fallback;
const name = getString(data.name, "未命名");
const name = getString(data.name, '未命名');
const cultivationLevel = getString(
data.level ?? data.cultivationLevel,
"炼气一层"
'炼气一层',
);
const spiritRoot = getString(data.spirit_root ?? data.spiritRoot, "无灵根");
const appearance = getString(data.appearance, "普通修士");
const backstory = getString(data.background ?? data.backstory, "来历不明");
const gender = getString(data.gender, "");
const origin = getString(data.origin, "");
const personality = getString(data.personality, "");
const spiritRoot = getString(data.spirit_root ?? data.spiritRoot, '无灵根');
const appearance = getString(data.appearance, '普通修士');
const backstory = getString(data.background ?? data.backstory, '来历不明');
const gender = getString(data.gender, '');
const origin = getString(data.origin, '');
const personality = getString(data.personality, '');
const preHeavenFates = parseFates(
data.pre_heaven_fates ?? data.preHeavenFates
data.pre_heaven_fates ?? data.preHeavenFates,
);
const cultivator: Cultivator = {
id: generateUUID(),
@@ -61,7 +61,7 @@ export function createCultivatorFromAI(
export function generateUUID(): string {
return (
"cultivator-" + Date.now() + "-" + Math.random().toString(36).slice(2, 11)
'cultivator-' + Date.now() + '-' + Math.random().toString(36).slice(2, 11)
);
}
@@ -80,21 +80,21 @@ function parseFates(raw: unknown): PreHeavenFate[] {
if (!Array.isArray(raw)) return [];
return raw
.map((item) => {
if (!item || typeof item !== "object") return null;
if (!item || typeof item !== 'object') return null;
const record = item as Record<string, unknown>;
const name = typeof record.name === "string" ? record.name : null;
const name = typeof record.name === 'string' ? record.name : null;
if (!name) return null;
const type = record.type === "凶" ? "凶" : "吉";
const effect = typeof record.effect === "string" ? record.effect : "";
const type = record.type === '凶' ? '凶' : '吉';
const effect = typeof record.effect === 'string' ? record.effect : '';
const description =
typeof record.description === "string" ? record.description : "";
typeof record.description === 'string' ? record.description : '';
return { name, type, effect, description };
})
.filter((fate): fate is PreHeavenFate => !!fate);
}
function parseAttributes(raw: unknown): BattleAttributes | null {
if (!raw || typeof raw !== "object") return null;
if (!raw || typeof raw !== 'object') return null;
const record = raw as Record<string, unknown>;
const vitality = Number(record.vitality);
const spirit = Number(record.spirit);
@@ -110,21 +110,21 @@ function parseSkills(raw: unknown): Skill[] {
if (!Array.isArray(raw)) return [];
const parsed = raw
.map((item) => {
if (!item || typeof item !== "object") return null;
if (!item || typeof item !== 'object') return null;
const record = item as Record<string, unknown>;
const name = typeof record.name === "string" ? record.name : null;
const typeValue = typeof record.type === "string" ? record.type : null;
const name = typeof record.name === 'string' ? record.name : null;
const typeValue = typeof record.type === 'string' ? record.type : null;
const power = Number(record.power);
const elementValue = (record.element as ElementType) ?? "无";
const elementValue = (record.element as ElementType) ?? '无';
if (!name || !typeValue || !Number.isFinite(power)) return null;
const effects = Array.isArray(record.effects)
? record.effects.filter(
(effect: unknown): effect is string => typeof effect === "string"
(effect: unknown): effect is string => typeof effect === 'string',
)
: undefined;
return {
name,
type: typeValue as Skill["type"],
type: typeValue as Skill['type'],
power,
element: elementValue,
effects,
@@ -138,12 +138,12 @@ function parseEquipment(raw: unknown): Equipment[] | undefined {
if (!Array.isArray(raw)) return undefined;
const equipments = raw
.map((item) => {
if (!item || typeof item !== "object") return null;
if (!item || typeof item !== 'object') return null;
const record = item as Record<string, unknown>;
const name = typeof record.name === "string" ? record.name : null;
const name = typeof record.name === 'string' ? record.name : null;
if (!name) return null;
const bonus =
record.bonus && typeof record.bonus === "object"
record.bonus && typeof record.bonus === 'object'
? { ...(record.bonus as Record<string, unknown>) }
: undefined;
const result: Equipment = { name, bonus };
@@ -156,7 +156,7 @@ function parseEquipment(raw: unknown): Equipment[] | undefined {
function buildBattleProfileFromData(
data: Record<string, unknown>,
name: string,
spiritRoot: string
spiritRoot: string,
): BattleProfile | null {
// 获取battle_profile对象
const battleProfile = data.battle_profile as Record<string, unknown>;
@@ -175,7 +175,7 @@ function buildBattleProfileFromData(
// 解析先天气运(从data中获取,而不是从battle_profile中)
const preHeavenFates = parseFates(
data.pre_heaven_fates ?? data.preHeavenFates
data.pre_heaven_fates ?? data.preHeavenFates,
);
// 应用先天气运效果
@@ -188,7 +188,7 @@ function buildBattleProfileFromData(
(data.max_hp as number);
let maxHp =
Number.isFinite(maxHpRaw) && typeof maxHpRaw === "number"
Number.isFinite(maxHpRaw) && typeof maxHpRaw === 'number'
? maxHpRaw
: Math.round(150 + finalAttributes.vitality * 0.8);
@@ -220,7 +220,7 @@ function buildBattleProfileFromData(
*/
function applyFateEffects(
attributes: BattleAttributes,
fates: PreHeavenFate[]
fates: PreHeavenFate[],
): BattleAttributes {
const finalAttributes = { ...attributes };
@@ -231,30 +231,30 @@ function applyFateEffects(
const vitalityMatch = effect.match(/vitality\s*([+-]\s*\d+)/i);
if (vitalityMatch) {
finalAttributes.vitality += parseInt(
vitalityMatch[1].replace(/\s/g, ""),
10
vitalityMatch[1].replace(/\s/g, ''),
10,
);
}
const spiritMatch = effect.match(/spirit\s*([+-]\s*\d+)/i);
if (spiritMatch) {
finalAttributes.spirit += parseInt(spiritMatch[1].replace(/\s/g, ""), 10);
finalAttributes.spirit += parseInt(spiritMatch[1].replace(/\s/g, ''), 10);
}
const wisdomMatch = effect.match(/wisdom\s*([+-]\s*\d+)/i);
if (wisdomMatch) {
finalAttributes.wisdom += parseInt(wisdomMatch[1].replace(/\s/g, ""), 10);
finalAttributes.wisdom += parseInt(wisdomMatch[1].replace(/\s/g, ''), 10);
}
const speedMatch = effect.match(/speed\s*([+-]\s*\d+)/i);
if (speedMatch) {
finalAttributes.speed += parseInt(speedMatch[1].replace(/\s/g, ""), 10);
finalAttributes.speed += parseInt(speedMatch[1].replace(/\s/g, ''), 10);
}
// 解析所有属性加成
const allAttrMatch = effect.match(/所有属性\s*([+-]\s*\d+)/i);
if (allAttrMatch) {
const bonus = parseInt(allAttrMatch[1].replace(/\s/g, ""), 10);
const bonus = parseInt(allAttrMatch[1].replace(/\s/g, ''), 10);
finalAttributes.vitality += bonus;
finalAttributes.spirit += bonus;
finalAttributes.wisdom += bonus;
@@ -265,7 +265,7 @@ function applyFateEffects(
// 确保属性在合理范围内
finalAttributes.vitality = Math.max(
50,
Math.min(100, finalAttributes.vitality)
Math.min(100, finalAttributes.vitality),
);
finalAttributes.spirit = Math.max(50, Math.min(100, finalAttributes.spirit));
finalAttributes.wisdom = Math.max(50, Math.min(100, finalAttributes.wisdom));
@@ -285,12 +285,12 @@ function applyFateMaxHpBonus(maxHp: number, fates: PreHeavenFate[]): number {
// 解析maxHp加成
const maxHpMatch = effect.match(
/max_hp\s*([+-]\s*\d+)|maxhp\s*([+-]\s*\d+)|生命值上限\s*([+-]\s*\d+)/i
/max_hp\s*([+-]\s*\d+)|maxhp\s*([+-]\s*\d+)|生命值上限\s*([+-]\s*\d+)/i,
);
if (maxHpMatch) {
const bonus = parseInt(
(maxHpMatch[1] || maxHpMatch[2] || maxHpMatch[3]).replace(/\s/g, ""),
10
(maxHpMatch[1] || maxHpMatch[2] || maxHpMatch[3]).replace(/\s/g, ''),
10,
);
finalMaxHp += bonus;
}
+44 -44
View File
@@ -1,6 +1,6 @@
import type { Cultivator } from "../types/cultivator";
import type { BattleEngineResult } from "@/engine/battleEngine";
import { mapSpiritRootToElement } from "./battleProfile";
import type { BattleEngineResult } from '@/engine/battleEngine';
import type { Cultivator } from '../types/cultivator';
import { mapSpiritRootToElement } from './battleProfile';
/**
* 角色生成 Prompt 模板(系统提示词)
@@ -69,7 +69,7 @@ interface BattlePromptPayload {
opponent: Cultivator;
battleResult: Pick<
BattleEngineResult,
"log" | "turns" | "playerHp" | "opponentHp" | "triggeredMiracle"
'log' | 'turns' | 'playerHp' | 'opponentHp' | 'triggeredMiracle'
> & { winnerId: string };
}
@@ -86,15 +86,15 @@ export function getBattleReportPrompt({
profile?.element ?? mapSpiritRootToElement(cultivator.spiritRoot);
const attributes = profile
? `体魄${profile.attributes.vitality} 灵力${profile.attributes.spirit} 悟性${profile.attributes.wisdom} 速度${profile.attributes.speed}`
: "属性:未知";
: '属性:未知';
const skills =
profile?.skills
?.map((skill) => `${skill.name}(${skill.element}/${skill.type})`)
.join(",") ?? "无";
.join(',') ?? '无';
const fates =
cultivator.preHeavenFates
?.map((fate) => `${fate.name}(${fate.type})`)
.join(",") ?? "无";
.join(',') ?? '无';
return `姓名:${cultivator.name}
境界:${cultivator.cultivationLevel}
灵根/属性:${cultivator.spiritRoot} · ${element}
@@ -103,7 +103,7 @@ export function getBattleReportPrompt({
先天气运:${fates}`;
};
const battleLog = (battleResult.log || []).join("\n");
const battleLog = (battleResult.log || []).join('\n');
const systemPrompt = `你是一位修仙题材连载小说作者,擅长写具有画面感的战斗场景。请根据设定与战斗日志,创作分回合的战斗播报,每回合描述控制在30-50字左右。
@@ -136,10 +136,10 @@ ${battleLog}
【战斗结论】
胜者:${winner.name}
回合数:${battleResult.turns ?? battleResult.log.length}
双方剩余气血:${player.name} ${battleResult.playerHp ?? "未知"} / ${
双方剩余气血:${player.name} ${battleResult.playerHp ?? '未知'} / ${
opponent.name
} ${battleResult.opponentHp ?? "未知"}
是否出现奇迹逆转:${battleResult.triggeredMiracle ? "是" : "否"}
} ${battleResult.opponentHp ?? '未知'}
是否出现奇迹逆转:${battleResult.triggeredMiracle ? '是' : '否'}
请写一段完整的战斗描写。`;
@@ -151,37 +151,37 @@ ${battleLog}
*/
export function getDefaultBoss(): Cultivator {
return {
id: "boss-001",
name: "血手人屠",
prompt: "魔道巨擘,嗜血成性",
cultivationLevel: "元婴后期",
spiritRoot: "血魔灵根",
appearance: "黑袍遮身,双目赤红,血雾缭绕,手持血色长刀",
backstory: "昔年正道第一,如今坠入魔渊,以血养道,屠戮无数。",
id: 'boss-001',
name: '血手人屠',
prompt: '魔道巨擘,嗜血成性',
cultivationLevel: '元婴后期',
spiritRoot: '血魔灵根',
appearance: '黑袍遮身,双目赤红,血雾缭绕,手持血色长刀',
backstory: '昔年正道第一,如今坠入魔渊,以血养道,屠戮无数。',
preHeavenFates: [
{
name: "血煞命格",
type: "凶",
effect: "vitality +15, wisdom -5",
description: "煞气入骨,杀孽滔天。",
name: '血煞命格',
type: '凶',
effect: 'vitality +15, wisdom -5',
description: '煞气入骨,杀孽滔天。',
},
{
name: "魔心不灭",
type: "吉",
effect: "max_hp +30",
description: "魔念护身,百劫不磨。",
name: '魔心不灭',
type: '吉',
effect: 'max_hp +30',
description: '魔念护身,百劫不磨。',
},
{
name: "尸山血海",
type: "凶",
effect: "火系技能威力 +15%",
description: "血海为源,冥焰滔天。",
name: '尸山血海',
type: '凶',
effect: '火系技能威力 +15%',
description: '血海为源,冥焰滔天。',
},
],
battleProfile: {
maxHp: 350,
hp: 350,
element: "火",
element: '火',
attributes: {
vitality: 110,
spirit: 80,
@@ -190,30 +190,30 @@ export function getDefaultBoss(): Cultivator {
},
skills: [
{
name: "血海滔天",
type: "attack",
name: '血海滔天',
type: 'attack',
power: 85,
element: "火",
effects: ["burn"],
element: '火',
effects: ['burn'],
},
{
name: "魔焰护身",
type: "buff",
name: '魔焰护身',
type: 'buff',
power: 55,
element: "火",
effects: ["speed_up"],
element: '火',
effects: ['speed_up'],
},
{
name: "血元回潮",
type: "heal",
name: '血元回潮',
type: 'heal',
power: 65,
element: "土",
effects: ["heal"],
element: '土',
effects: ['heal'],
},
],
equipment: [
{
name: "血炼魔刀",
name: '血炼魔刀',
bonus: { vitality: 8, elementBoost: { 火: 0.15 } },
},
],