fmt
This commit is contained in:
1 parent
e0254f4ef3
commit
b9f18fda39
37 files changed
+1611
-1020
No files matched your search
+6
-6
@@ -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"]
|
||||
}
|
||||
@@ -36,4 +36,3 @@ OPENAI_MODEL=gpt-4o-mini
|
||||
- `.env.local` 文件不会被提交到 Git(已在 .gitignore 中)
|
||||
- 修改环境变量后需要重启开发服务器
|
||||
- 生产环境需要在部署平台(如 Vercel)配置环境变量
|
||||
|
||||
@@ -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+** | 收集反馈 → 迭代(加装备?加用户系统?) | 进入正向循环 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}`}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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}`,
|
||||
},
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,4 @@
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+88
-88
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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(),
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!,
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -1,4 +1,4 @@
|
||||
import type { NextConfig } from "next";
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
|
||||
Generated
+158
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 } },
|
||||
},
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user