diff --git a/bun.lockb b/bun.lockb index 15d0c07..22c05e5 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/trpc-implementation.md b/docs/trpc-implementation.md deleted file mode 100644 index 201c955..0000000 --- a/docs/trpc-implementation.md +++ /dev/null @@ -1,258 +0,0 @@ -# tRPC Implementation Documentation - -## Overview - -This project implements a [tRPC](https://trpc.io/) API layer to provide type-safe communication between the frontend and backend. The implementation follows SolidStart's server-side rendering architecture with a clear separation of concerns. - -## Architecture - -The tRPC setup is organized in the following structure: - -``` -src/ -├── server/ -│ └── api/ -│ ├── root.ts # Main router that combines all sub-routers -│ ├── utils.ts # tRPC utility functions and initialization -│ └── routers/ # Individual API route groups -│ ├── auth.ts # Authentication procedures -│ ├── database.ts # Database operations -│ ├── example.ts # Example procedures -│ ├── lineage.ts # Lineage-related APIs -│ └── misc.ts # Miscellaneous endpoints -└── routes/ - └── api/ - └── trpc/ - └── [trpc].ts # API endpoint handler -``` - -## How to Use tRPC Procedures from the Frontend - -The `api` client is pre-configured and available for use in components: - -```typescript -import { api } from "~/lib/api"; - -// Example usage in a component -export function MyComponent() { - const [result, setResult] = useState(null); - - const handleClick = async () => { - try { - // Call a tRPC procedure - const data = await api.example.hello.query("World"); - setResult(data); - } catch (error) { - console.error("Error calling tRPC procedure:", error); - } - }; - - return ( -
-

{result}

- -
- ); -} -``` - -## API Route Structure - -### Root Router (`src/server/api/root.ts`) - -The main router combines all individual routers: - -```typescript -export const appRouter = createTRPCRouter({ - example: exampleRouter, - auth: authRouter, - database: databaseRouter, - lineage: lineageRouter, - misc: miscRouter -}); -``` - -### Procedure Types - -tRPC provides two main procedure types: -- **Query**: For read-only operations (GET requests) -- **Mutation**: For write operations (POST, PUT, DELETE requests) - -Example: - -```typescript -// Query procedure - read-only -publicProcedure - .input(z.string()) - .query(({ input }) => { - return `Hello ${input}!`; - }) - -// Mutation procedure - write operation -publicProcedure - .input(z.object({ name: z.string() })) - .mutation(({ input }) => { - // Logic for creating/updating data - return { success: true, name: input.name }; - }) -``` - -## Adding New Endpoints - -### 1. Create a new router file - -Create a new file in `src/server/api/routers/`: - -```typescript -import { createTRPCRouter, publicProcedure } from "../utils"; -import { z } from "zod"; - -export const myRouter = createTRPCRouter({ - // Add your procedures here - hello: publicProcedure - .input(z.string()) - .query(({ input }) => { - return `Hello ${input}!`; - }), -}); -``` - -### 2. Register the router in the root - -Add your new router to `src/server/api/root.ts`: - -```typescript -import { exampleRouter } from "./routers/example"; -import { authRouter } from "./routers/auth"; -import { databaseRouter } from "./routers/database"; -import { lineageRouter } from "./routers/lineage"; -import { miscRouter } from "./routers/misc"; -import { myRouter } from "./routers/myRouter"; // Add this import -import { createTRPCRouter } from "./utils"; - -export const appRouter = createTRPCRouter({ - example: exampleRouter, - auth: authRouter, - database: databaseRouter, - lineage: lineageRouter, - misc: miscRouter, - myRouter: myRouter, // Add this line -}); -``` - -### 3. Use in frontend - -```typescript -// In your frontend component -const data = await api.myRouter.hello.query("World"); -``` - -## Best Practices - -1. **Type Safety**: Always use Zod schemas to validate input data and return types. - -2. **Error Handling**: Implement proper error handling with try/catch blocks in async procedures. - -3. **Procedure Organization**: Group related procedures into logical routers. - -4. **Consistent Naming**: Use clear, descriptive names for your procedures and routers. - -5. **Documentation**: Document each procedure with clear descriptions of what it does. - -## Example Usage Patterns - -### Query Procedure (GET) -```typescript -// In your router file -getPosts: publicProcedure - .input(z.object({ - limit: z.number().optional(), - offset: z.number().optional() - })) - .query(({ input }) => { - // Return data from database or external service - return { posts: [], total: 0 }; - }) -``` - -```typescript -// In frontend component -const { data, isLoading } = api.database.getPosts.useQuery({ limit: 10 }); -``` - -### Mutation Procedure (POST/PUT/DELETE) -```typescript -// In your router file -createPost: publicProcedure - .input(z.object({ - title: z.string(), - content: z.string() - })) - .mutation(({ input }) => { - // Create post in database - return { success: true, post: { id: "1", ...input } }; - }) -``` - -```typescript -// In frontend component -const { mutateAsync } = api.database.createPost.useMutation(); - -const handleClick = async () => { - try { - const result = await mutateAsync({ - title: "New Post", - content: "Post content" - }); - console.log("Created post:", result); - } catch (error) { - console.error("Error creating post:", error); - } -}; -``` - -## Available Endpoints - -### Auth -- `auth.githubCallback` - GitHub OAuth callback -- `auth.googleCallback` - Google OAuth callback -- `auth.emailLogin` - Email login -- `auth.emailVerification` - Email verification - -### Database -- `database.getCommentReactions` - Get comment reactions -- `database.postCommentReaction` - Add comment reaction -- `database.deleteCommentReaction` - Remove comment reaction -- `database.getComments` - Get comments for a post -- `database.getPosts` - Get posts with pagination -- `database.createPost` - Create new post -- `database.updatePost` - Update existing post -- `database.deletePost` - Delete post -- `database.getPostLikes` - Get likes for a post -- `database.likePost` - Like a post -- `database.unlikePost` - Unlike a post - -### Lineage -- `lineage.databaseManagement` - Database management operations -- `lineage.analytics` - Analytics endpoints -- `lineage.appleAuth` - Apple authentication -- `lineage.emailLogin` - Email login -- `lineage.emailRegister` - Email registration -- `lineage.emailVerify` - Email verification -- `lineage.googleRegister` - Google registration -- `lineage.attacks` - Attack data -- `lineage.conditions` - Condition data -- `lineage.dungeons` - Dungeon data -- `lineage.enemies` - Enemy data -- `lineage.items` - Item data -- `lineage.misc` - Miscellaneous data -- `lineage.offlineSecret` - Offline secret endpoint -- `lineage.pvpGet` - PvP GET operations -- `lineage.pvpPost` - PvP POST operations -- `lineage.tokens` - Token operations - -### Misc -- `misc.downloads` - Downloads endpoint -- `misc.s3Delete` - Delete S3 object -- `misc.s3Get` - Get S3 object -- `misc.hashPassword` - Hash password \ No newline at end of file diff --git a/package.json b/package.json index ce178dd..d8dd2d6 100644 --- a/package.json +++ b/package.json @@ -7,18 +7,31 @@ "start": "vinxi start" }, "dependencies": { + "@aws-sdk/client-s3": "^3.953.0", + "@aws-sdk/s3-request-presigner": "^3.953.0", + "@libsql/client": "^0.15.15", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", "@solidjs/start": "^1.1.0", "@tailwindcss/vite": "^4.0.7", "@trpc/client": "^10.45.2", "@trpc/server": "^10.45.2", + "@tursodatabase/api": "^1.9.2", "@typeschema/valibot": "^0.13.4", + "bcrypt": "^6.0.0", + "google-auth-library": "^10.5.0", + "jose": "^6.1.3", "solid-js": "^1.9.5", + "uuid": "^13.0.0", "valibot": "^0.29.0", - "vinxi": "^0.5.7" + "vinxi": "^0.5.7", + "zod": "^4.2.1" }, "engines": { "node": ">=22" + }, + "devDependencies": { + "@types/bcrypt": "^6.0.0", + "trpc-panel": "^1.3.4" } } diff --git a/src/api/auth/callback/github/route.ts b/src/api/auth/callback/github/route.ts deleted file mode 100644 index 5b0bf83..0000000 --- a/src/api/auth/callback/github/route.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { v4 as uuidV4 } from "uuid"; -import { env } from "@/env.mjs"; -import { cookies } from "next/headers"; -import { User } from "@/types/model-types"; -import jwt from "jsonwebtoken"; -import { ConnectionFactory } from "@/app/utils"; - -export async function GET(request: NextRequest) { - const params = request.nextUrl.searchParams; - const code = params.get("code"); - if (code) { - const tokenResponse = await fetch( - "https://github.com/login/oauth/access_token", - { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - client_id: env.NEXT_PUBLIC_GITHUB_CLIENT_ID, - client_secret: env.GITHUB_CLIENT_SECRET, - code, - }), - }, - ); - const { access_token } = await tokenResponse.json(); - - const userResponse = await fetch("https://api.github.com/user", { - headers: { - Authorization: `token ${access_token}`, - }, - }); - - const user = await userResponse.json(); - const login = user.login; - const conn = ConnectionFactory(); - - const query = `SELECT * FROM User WHERE provider = ? AND display_name = ?`; - const params = ["github", login]; - const res = await conn.execute({ sql: query, args: params }); - if (res.rows[0]) { - const token = jwt.sign( - { id: (res.rows[0] as unknown as User).id }, - env.JWT_SECRET_KEY, - { - expiresIn: 60 * 60 * 24 * 14, // expires in 14 days - }, - ); - (await cookies()).set({ - name: "userIDToken", - value: token, - maxAge: 60 * 60 * 24 * 14, - }); - } else { - const icon = user.avatar_url; - const email = user.email; - const userId = uuidV4(); - - const insertQuery = `INSERT INTO User (id, email, display_name, provider, image) VALUES (?, ?, ?, ?, ?)`; - const insertParams = [userId, email, login, "github", icon]; - await conn.execute({ sql: insertQuery, args: insertParams }); - const token = jwt.sign({ id: userId }, env.JWT_SECRET_KEY, { - expiresIn: 60 * 60 * 24 * 14, // expires in 14 days - }); - (await cookies()).set({ - name: "userIDToken", - value: token, - maxAge: 60 * 60 * 24 * 14, - }); - } - - return NextResponse.redirect(`${env.NEXT_PUBLIC_DOMAIN}/account`); - } else { - return NextResponse.json( - JSON.stringify({ - success: false, - message: `authentication failed: no code on callback`, - }), - { status: 401, headers: { "content-type": "application/json" } }, - ); - } -} diff --git a/src/api/auth/callback/google/route.ts b/src/api/auth/callback/google/route.ts deleted file mode 100644 index b116ebf..0000000 --- a/src/api/auth/callback/google/route.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { v4 as uuidV4 } from "uuid"; -import { env } from "@/env.mjs"; -import { cookies } from "next/headers"; -import { User } from "@/types/model-types"; -import jwt from "jsonwebtoken"; -import { ConnectionFactory } from "@/app/utils"; - -export async function GET(request: NextRequest) { - const params = request.nextUrl.searchParams; - const code = params.get("code"); - if (code) { - const tokenResponse = await fetch("https://oauth2.googleapis.com/token", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - code: code, - client_id: env.NEXT_PUBLIC_GOOGLE_CLIENT_ID, - client_secret: env.GOOGLE_CLIENT_SECRET, - redirect_uri: "https://www.freno.me/api/auth/callback/google", - grant_type: "authorization_code", - }), - }); - - const { access_token } = await tokenResponse.json(); - - const userResponse = await fetch( - "https://www.googleapis.com/oauth2/v3/userinfo", - { - headers: { - Authorization: `Bearer ${access_token}`, - }, - }, - ); - - const userData = await userResponse.json(); - console.log(userData); - const name = userData.name; - const image = userData.picture; - const email = userData.email; - const email_verified = userData.email_verified; - - const conn = ConnectionFactory(); - - const query = `SELECT * FROM User WHERE provider = ? AND email = ?`; - const params = ["google", email]; - const res = await conn.execute({ sql: query, args: params }); - if (res.rows[0]) { - const token = jwt.sign( - { id: (res.rows[0] as unknown as User).id }, - env.JWT_SECRET_KEY, - { - expiresIn: 60 * 60 * 24 * 14, // expires in 14 days - }, - ); - (await cookies()).set({ - name: "userIDToken", - value: token, - maxAge: 60 * 60 * 24 * 14, - }); - } else { - const userId = uuidV4(); - - const insertQuery = `INSERT INTO User (id, email, email_verified, display_name, provider, image) VALUES (?, ?, ?, ?, ?, ?)`; - const insertParams = [ - userId, - email, - email_verified, - name, - "google", - image, - ]; - await conn.execute({ - sql: insertQuery, - args: insertParams, - }); - const token = jwt.sign({ id: userId }, env.JWT_SECRET_KEY, { - expiresIn: 60 * 60 * 24 * 14, // expires in 14 days - }); - (await cookies()).set({ - name: "userIDToken", - value: token, - maxAge: 60 * 60 * 24 * 14, - }); - } - - return NextResponse.redirect(`${env.NEXT_PUBLIC_DOMAIN}/account`); - } else { - return NextResponse.json( - JSON.stringify({ - success: false, - message: `authentication failed: no code on callback`, - }), - { status: 401, headers: { "content-type": "application/json" } }, - ); - } -} diff --git a/src/api/auth/email-login/[email]/route.ts b/src/api/auth/email-login/[email]/route.ts deleted file mode 100644 index ab157e9..0000000 --- a/src/api/auth/email-login/[email]/route.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { JwtPayload } from "jsonwebtoken"; -import { NextRequest, NextResponse } from "next/server"; -import jwt from "jsonwebtoken"; -import { env } from "@/env.mjs"; -import { cookies } from "next/headers"; -import { User } from "@/types/model-types"; -import { ConnectionFactory } from "@/app/utils"; - -export async function GET( - request: NextRequest, - context: { params: Promise<{ email: string }> }, -) { - const readyParams = await context.params; - const secretKey = env.JWT_SECRET_KEY; - const params = request.nextUrl.searchParams; - const token = params.get("token"); - const userEmail = readyParams.email; - try { - if (token) { - const decoded = jwt.verify(token, secretKey) as JwtPayload; - if (decoded.email == userEmail) { - const conn = ConnectionFactory(); - const query = `SELECT * FROM User WHERE email = ?`; - const params = [decoded.email]; - const res = await conn.execute({ sql: query, args: params }); - const token = jwt.sign( - { id: (res.rows[0] as unknown as User).id }, - env.JWT_SECRET_KEY, - { - expiresIn: 60 * 60 * 24 * 14, // expires in 14 days - }, - ); - if (decoded.rememberMe) { - (await cookies()).set({ - name: "userIDToken", - value: token, - maxAge: 60 * 60 * 24 * 14, - }); - } else { - (await cookies()).set({ - name: "userIDToken", - value: token, - }); - } - return NextResponse.redirect(`${env.NEXT_PUBLIC_DOMAIN}/account`); - } - } - return NextResponse.json( - JSON.stringify({ - success: false, - message: `authentication failed: no token`, - }), - { status: 401, headers: { "content-type": "application/json" } }, - ); - } catch (err) { - return NextResponse.json( - JSON.stringify({ - success: false, - message: `authentication failed: ${err}`, - }), - { status: 401, headers: { "content-type": "application/json" } }, - ); - } -} diff --git a/src/api/auth/email-verification/[email]/route.ts b/src/api/auth/email-verification/[email]/route.ts deleted file mode 100644 index d9749cf..0000000 --- a/src/api/auth/email-verification/[email]/route.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import jwt, { JwtPayload } from "jsonwebtoken"; -import { env } from "@/env.mjs"; -import { ConnectionFactory } from "@/app/utils"; - -export async function GET( - request: NextRequest, - context: { params: Promise<{ email: string }> }, -) { - const readyParams = await context.params; - const secretKey = env.JWT_SECRET_KEY; - const params = request.nextUrl.searchParams; - const token = params.get("token"); - const userEmail = readyParams.email; - try { - if (token) { - const decoded = jwt.verify(token, secretKey) as JwtPayload; - if (decoded.email == userEmail) { - const conn = ConnectionFactory(); - const query = `UPDATE User SET email_verified = ? WHERE email = ?`; - const params = [true, userEmail]; - await conn.execute({ sql: query, args: params }); - return new NextResponse( - JSON.stringify({ - success: true, - message: "email verification success, you may close this window", - }), - { status: 202, headers: { "content-type": "application/json" } }, - ); - } - } - return NextResponse.json( - JSON.stringify({ - success: false, - message: `authentication failed: no token`, - }), - { status: 401, headers: { "content-type": "application/json" } }, - ); - } catch (err) { - console.error("Invalid token:", err); - return new NextResponse( - JSON.stringify({ - success: false, - message: "authentication failed: Invalid token", - }), - { status: 401, headers: { "content-type": "application/json" } }, - ); - } -} diff --git a/src/api/database/comment-reactions/[commentID]/route.ts b/src/api/database/comment-reactions/[commentID]/route.ts deleted file mode 100644 index 5eb2520..0000000 --- a/src/api/database/comment-reactions/[commentID]/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NextResponse } from "next/server"; -import { ConnectionFactory } from "@/app/utils"; - -export async function GET( - _: Request, - context: { params: Promise<{ commentID: string }> }, -) { - const readyParams = await context.params; - const commentID = readyParams.commentID; - const conn = ConnectionFactory(); - const commentQuery = "SELECT * FROM CommentReaction WHERE comment_id = ?"; - const commentParams = [commentID]; - const commentResults = await conn.execute({ - sql: commentQuery, - args: commentParams, - }); - return NextResponse.json( - { commentReactions: commentResults.rows }, - { status: 202 }, - ); -} diff --git a/src/api/database/comment-reactions/add/[type]/route.ts b/src/api/database/comment-reactions/add/[type]/route.ts deleted file mode 100644 index 778a336..0000000 --- a/src/api/database/comment-reactions/add/[type]/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { ConnectionFactory } from "@/app/utils"; -import { CommentReactionInput } from "@/types/input-types"; -import { CommentReaction } from "@/types/model-types"; - -export async function POST( - input: NextRequest, - context: { params: Promise<{ type: string }> }, -) { - const readyParams = await context.params; - const inputData = (await input.json()) as CommentReactionInput; - const { comment_id, user_id } = inputData; - const conn = ConnectionFactory(); - const query = ` - INSERT INTO CommentReaction (type, comment_id, user_id) - VALUES (?, ?, ?) - `; - const params = [readyParams.type, comment_id, user_id]; - await conn.execute({ sql: query, args: params }); - const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`; - const followUpParams = [comment_id]; - const res = await conn.execute({ sql: followUpQuery, args: followUpParams }); - const data = (res.rows as unknown as CommentReaction[]).filter( - (commentReaction) => commentReaction.comment_id == comment_id, - ); - return NextResponse.json({ commentReactions: data || [] }); -} diff --git a/src/api/database/comment-reactions/remove/[type]/route.ts b/src/api/database/comment-reactions/remove/[type]/route.ts deleted file mode 100644 index 246f448..0000000 --- a/src/api/database/comment-reactions/remove/[type]/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { ConnectionFactory } from "@/app/utils"; -import { CommentReactionInput } from "@/types/input-types"; -import { CommentReaction } from "@/types/model-types"; - -export async function POST( - input: NextRequest, - context: { params: Promise<{ type: string }> }, -) { - const readyParams = await context.params; - const inputData = (await input.json()) as CommentReactionInput; - const { comment_id, user_id } = inputData; - const conn = ConnectionFactory(); - const query = ` - DELETE FROM CommentReaction - WHERE type = ? AND comment_id = ? AND user_id = ? - `; - const params = [readyParams.type, comment_id, user_id]; - await conn.execute({ sql: query, args: params }); - - const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`; - const followUpParams = [comment_id]; - const res = await conn.execute({ sql: followUpQuery, args: followUpParams }); - const data = (res.rows as unknown as CommentReaction[]).filter( - (commentReaction) => commentReaction.comment_id == comment_id, - ); - return NextResponse.json({ commentReactions: data || [] }); -} diff --git a/src/api/database/comments/get-all/[post_id]/route.ts b/src/api/database/comments/get-all/[post_id]/route.ts deleted file mode 100644 index 7aff6c7..0000000 --- a/src/api/database/comments/get-all/[post_id]/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ConnectionFactory } from "@/app/utils"; -import { NextResponse } from "next/server"; - -export async function GET( - _: Request, - context: { - params: Promise<{ post_id: string }>; - }, -) { - const readyParams = await context.params; - const conn = ConnectionFactory(); - const query = `SELECT * FROM Comment WHERE post_id = ?`; - const params = [readyParams.post_id]; - const res = await conn.execute({ sql: query, args: params }); - return NextResponse.json({ comments: res.rows }, { status: 302 }); -} diff --git a/src/api/database/comments/get-all/route.ts b/src/api/database/comments/get-all/route.ts deleted file mode 100644 index 6d80b34..0000000 --- a/src/api/database/comments/get-all/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NextResponse } from "next/server"; -import { ConnectionFactory } from "@/app/utils"; - -export async function GET() { - const conn = ConnectionFactory(); - const query = `SELECT * FROM Comment`; - const res = await conn.execute(query); - return NextResponse.json({ comments: res.rows }); -} diff --git a/src/api/database/post-like/add/route.ts b/src/api/database/post-like/add/route.ts deleted file mode 100644 index 72b824c..0000000 --- a/src/api/database/post-like/add/route.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ConnectionFactory } from "@/app/utils"; -import { PostLikeInput } from "@/types/input-types"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(input: NextRequest) { - const inputData = (await input.json()) as PostLikeInput; - const { user_id, post_id } = inputData; - const conn = ConnectionFactory(); - const query = `INSERT INTO PostLike (user_id, post_id) - VALUES (?, ?)`; - const params = [user_id, post_id]; - await conn.execute({ sql: query, args: params }); - const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`; - const followUpParams = [post_id]; - const res = await conn.execute({ sql: followUpQuery, args: followUpParams }); - return NextResponse.json({ newLikes: res.rows }); -} diff --git a/src/api/database/post-like/remove/route.ts b/src/api/database/post-like/remove/route.ts deleted file mode 100644 index 2aae7d7..0000000 --- a/src/api/database/post-like/remove/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ConnectionFactory } from "@/app/utils"; -import { PostLikeInput } from "@/types/input-types"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(input: NextRequest) { - const inputData = (await input.json()) as PostLikeInput; - const { user_id, post_id } = inputData; - const conn = ConnectionFactory(); - const query = ` - DELETE FROM PostLike - WHERE user_id = ? AND post_id = ? - `; - const params = [user_id, post_id]; - await conn.execute({ sql: query, args: params }); - const followUpQuery = `SELECT * FROM PostLike WHERE post_id=?`; - const followUpParams = [post_id]; - const res = await conn.execute({ sql: followUpQuery, args: followUpParams }); - return NextResponse.json({ newLikes: res.rows }); -} diff --git a/src/api/database/post/[category]/by-id/[id]/route.ts b/src/api/database/post/[category]/by-id/[id]/route.ts deleted file mode 100644 index 97f756c..0000000 --- a/src/api/database/post/[category]/by-id/[id]/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { ConnectionFactory } from "@/app/utils"; - -export async function GET( - _: NextRequest, - context: { params: Promise<{ category: string; id: string }> }, -) { - const readyParams = await context.params; - if (readyParams.category !== "blog" && readyParams.category !== "project") { - return NextResponse.json( - { error: "invalid category value" }, - { status: 400 }, - ); - } else { - try { - const conn = ConnectionFactory(); - const query = `SELECT * FROM Post WHERE id = ?`; - const params = [parseInt(readyParams.id)]; - const results = await conn.execute({ sql: query, args: params }); - const tagQuery = `SELECT * FROM Tag WHERE post_id = ?`; - const tagRes = await conn.execute({ sql: tagQuery, args: params }); - if (results.rows[0]) { - return NextResponse.json( - { - post: results.rows[0], - tags: tagRes.rows, - }, - { status: 200 }, - ); - } else { - return NextResponse.json( - { - post: [], - }, - { status: 204 }, - ); - } - } catch (e) { - console.error(e); - return NextResponse.json({ error: e }, { status: 400 }); - } - } -} diff --git a/src/api/database/post/[category]/by-title/[title]/route.ts b/src/api/database/post/[category]/by-title/[title]/route.ts deleted file mode 100644 index a3ea110..0000000 --- a/src/api/database/post/[category]/by-title/[title]/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { env } from "@/env.mjs"; -import { ConnectionFactory } from "@/app/utils"; -import { Post } from "@/types/model-types"; - -export async function GET( - request: NextRequest, - context: { params: Promise<{ category: string; title: string }> }, -) { - const readyParams = await context.params; - if (readyParams.category !== "blog" && readyParams.category !== "project") { - return NextResponse.json( - { error: "invalid category value" }, - { status: 400 }, - ); - } else { - try { - let privilegeLevel = "anonymous"; - const token = request.cookies.get("userIDToken"); - if (token) { - if (token.value == env.ADMIN_ID) { - privilegeLevel = "admin"; - } else { - privilegeLevel = "user"; - } - } - const conn = ConnectionFactory(); - - const projectQuery = - "SELECT p.*, c.*, l.*,t.* FROM Post p JOIN Comment c ON p.id = c.post_id JOIN PostLike l ON p.id = l.post_idJOIN Tag t ON p.id = t.post_id WHERE p.title = ? AND p.category = ? AND p.published = ?;"; - const projectParams = [readyParams.title, readyParams.category, true]; - const projectResults = await conn.execute({ - sql: projectQuery, - args: projectParams, - }); - if (projectResults.rows[0]) { - const post_id = (projectResults.rows[0] as unknown as Post).id; - - const commentQuery = "SELECT * FROM Comment WHERE post_id = ?"; - const commentResults = await conn.execute({ - sql: commentQuery, - args: [post_id], - }); - - const likeQuery = "SELECT * FROM PostLike WHERE post_id = ?"; - const likeQueryResults = await conn.execute({ - sql: likeQuery, - args: [post_id], - }); - - const tagsQuery = "SELECT * FROM Tag WHERE post_id = ?"; - const tagResults = await conn.execute({ - sql: tagsQuery, - args: [post_id], - }); - - return NextResponse.json( - { - project: projectResults.rows[0], - comments: commentResults.rows, - likes: likeQueryResults.rows, - tagResults: tagResults.rows, - privilegeLevel: privilegeLevel, - }, - { status: 200 }, - ); - } - return NextResponse.json({ status: 200 }); - } catch (e) { - return NextResponse.json({ error: e }, { status: 400 }); - } - } -} diff --git a/src/api/database/post/[category]/manipulation/route.ts b/src/api/database/post/[category]/manipulation/route.ts deleted file mode 100644 index 9e6d306..0000000 --- a/src/api/database/post/[category]/manipulation/route.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import { env } from "@/env.mjs"; -import { ConnectionFactory } from "@/app/utils"; - -interface POSTInputData { - title: string; - subtitle: string | null; - body: string | null; - banner_photo: string | null; - published: boolean; - tags: string[] | null; -} - -interface PATCHInputData { - id: number; - title: string | null; - subtitle: string | null; - body: string | null; - banner_photo: string | null; - published: boolean | null; - tags: string[] | null; -} - -export async function POST( - input: NextRequest, - context: { params: Promise<{ category: string }> }, -) { - const readyParams = await context.params; - if (readyParams.category !== "blog" && readyParams.category !== "project") { - return NextResponse.json( - { error: "invalid category value" }, - { status: 400 }, - ); - } else { - try { - const inputData = (await input.json()) as POSTInputData; - const { title, subtitle, body, banner_photo, published, tags } = - inputData; - const userIDCookie = (await cookies()).get("userIDToken"); - const fullURL = env.NEXT_PUBLIC_AWS_BUCKET_STRING + banner_photo; - - if (userIDCookie) { - const author_id = userIDCookie.value; - const conn = ConnectionFactory(); - const query = ` - INSERT INTO Post (title, category, subtitle, body, banner_photo, published, author_id) - VALUES (?, ?, ?, ?, ?, ?, ?) - `; - const params = [ - title, - readyParams.category, - subtitle, - body, - banner_photo ? fullURL : null, - published, - author_id, - ]; - const results = await conn.execute({ sql: query, args: params }); - if (tags) { - let query = "INSERT INTO Tag (value, post_id) VALUES "; - let values = tags.map( - (tag) => `("${tag}", ${results.lastInsertRowid})`, - ); - query += values.join(", "); - await conn.execute(query); - } - return NextResponse.json( - { data: results.lastInsertRowid }, - { status: 201 }, - ); - } - return NextResponse.json({ error: "no cookie" }, { status: 401 }); - } catch (e) { - console.error(e); - return NextResponse.json({ error: e }, { status: 400 }); - } - } -} -export async function PATCH(input: NextRequest) { - try { - const inputData = (await input.json()) as PATCHInputData; - - const conn = ConnectionFactory(); - const { query, params } = await createUpdateQuery(inputData); - const results = await conn.execute({ - sql: query, - args: params as string[], - }); - const { tags, id } = inputData; - const deleteTagsQuery = `DELETE FROM Tag WHERE post_id = ?`; - await conn.execute({ sql: deleteTagsQuery, args: [id.toString()] }); - if (tags) { - let query = "INSERT INTO Tag (value, post_id) VALUES "; - let values = tags.map((tag) => `("${tag}", ${id})`); - query += values.join(", "); - await conn.execute(query); - } - return NextResponse.json( - { data: results.lastInsertRowid }, - { status: 201 }, - ); - } catch (e) { - console.error(e); - return NextResponse.json({ error: e }, { status: 400 }); - } -} - -async function createUpdateQuery(data: PATCHInputData) { - const { id, title, subtitle, body, banner_photo, published } = data; - - let query = "UPDATE Post SET "; - let params = []; - let first = true; - - if (title !== null) { - query += first ? "title = ?" : ", title = ?"; - params.push(title); - first = false; - } - - if (subtitle !== null) { - query += first ? "subtitle = ?" : ", subtitle = ?"; - params.push(subtitle); - first = false; - } - - if (body !== null) { - query += first ? "body = ?" : ", body = ?"; - params.push(body); - first = false; - } - - if (banner_photo !== null) { - query += first ? "banner_photo = ?" : ", banner_photo = ?"; - if (banner_photo == "_DELETE_IMAGE_") { - params.push(undefined); - } else { - params.push(env.NEXT_PUBLIC_AWS_BUCKET_STRING + banner_photo); - } - first = false; - } - - if (published !== null) { - query += first ? "published = ?" : ", published = ?"; - params.push(published); - first = false; - } - - query += first ? "author_id = ?" : ", author_id = ?"; - params.push((await cookies()).get("userIDToken")?.value); - - query += " WHERE id = ?"; - params.push(id); - - return { query, params }; -} diff --git a/src/api/database/user/email/route.ts b/src/api/database/user/email/route.ts deleted file mode 100644 index 835e29a..0000000 --- a/src/api/database/user/email/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import { newEmailInput } from "@/types/input-types"; -import { ConnectionFactory } from "@/app/utils"; - -export async function POST(input: NextRequest) { - const inputData = (await input.json()) as newEmailInput; - const { id, newEmail } = inputData; - const oldEmail = (await cookies()).get("emailToken"); - const conn = ConnectionFactory(); - const query = `UPDATE User SET email = ? WHERE id = ? AND email = ?`; - const params = [newEmail, id, oldEmail]; - try { - const res = await conn.execute({ sql: query, args: params as string[] }); - return NextResponse.json({ res: res }, { status: 202 }); - } catch (e) { - console.log(e); - return NextResponse.json({ status: 400 }); - } -} diff --git a/src/api/database/user/from-id/[id]/route.ts b/src/api/database/user/from-id/[id]/route.ts deleted file mode 100644 index 7684365..0000000 --- a/src/api/database/user/from-id/[id]/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { User } from "@/types/model-types"; -import { ConnectionFactory } from "@/app/utils"; -import { NextResponse } from "next/server"; -export async function GET( - _: Request, - context: { params: Promise<{ id: string }> }, -) { - try { - const conn = ConnectionFactory(); - const userQuery = "SELECT * FROM User WHERE id =?"; - const params = await context.params; - const userParams = [params.id]; - const res = await conn.execute({ sql: userQuery, args: userParams }); - if (res.rows[0]) { - const user = res.rows[0] as unknown as User; - if (user && user.display_name !== "user deleted") - return NextResponse.json( - { - id: user.id, - email: user.email, - emailVerified: user.email_verified, - image: user.image, - displayName: user.display_name, - provider: user.provider, - hasPassword: !!user.password_hash, - }, - { status: 202 }, - ); - } - return NextResponse.json({}, { status: 200 }); - } catch (err) { - console.error(err); - return NextResponse.json({}, { status: 200 }); - } -} diff --git a/src/api/database/user/image/[id]/route.ts b/src/api/database/user/image/[id]/route.ts deleted file mode 100644 index 89eebce..0000000 --- a/src/api/database/user/image/[id]/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ConnectionFactory } from "@/app/utils"; -import { env } from "@/env.mjs"; -import { changeImageInput } from "@/types/input-types"; -import { NextRequest, NextResponse } from "next/server"; - -export async function GET( - _: Request, - context: { params: Promise<{ id: string }> }, -) { - const conn = ConnectionFactory(); - const query = "SELECT * FROM User WHERE id = ?"; - const params = await context.params; - const idArr = [params.id]; - const results = await conn.execute({ sql: query, args: idArr }); - return NextResponse.json({ user: results.rows[0] }, { status: 200 }); -} -export async function POST( - request: NextRequest, - context: { params: Promise<{ id: string }> }, -) { - const inputData = (await request.json()) as changeImageInput; - const { imageURL } = inputData; - try { - const conn = ConnectionFactory(); - const query = `UPDATE User SET image = ? WHERE id = ?`; - const fullURL = env.NEXT_PUBLIC_AWS_BUCKET_STRING + imageURL; - const params = [imageURL ? fullURL : null, (await context.params).id]; - await conn.execute({ sql: query, args: params }); - return NextResponse.json({ res: "success" }, { status: 200 }); - } catch (err) { - return NextResponse.json({ res: err }, { status: 500 }); - } -} diff --git a/src/api/database/user/public-data/[id]/route.ts b/src/api/database/user/public-data/[id]/route.ts deleted file mode 100644 index 7075add..0000000 --- a/src/api/database/user/public-data/[id]/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { User } from "@/types/model-types"; -import { ConnectionFactory } from "@/app/utils"; -import { NextResponse } from "next/server"; -export async function GET( - _: Request, - context: { params: Promise<{ id: string }> }, -) { - try { - const conn = ConnectionFactory(); - const userQuery = "SELECT email, display_name, image FROM User WHERE id =?"; - const params = await context.params; - const userParams = [params.id]; - const res = await conn.execute({ sql: userQuery, args: userParams }); - if (res.rows[0]) { - const user = res.rows[0] as unknown as User; - if (user && user.display_name !== "user deleted") - return NextResponse.json( - { - email: user.email, - image: user.image, - display_name: user.display_name, - }, - { status: 202 }, - ); - } - return NextResponse.json({}, { status: 200 }); - } catch (err) { - console.error(err); - return NextResponse.json({}, { status: 200 }); - } -} diff --git a/src/api/downloads/public/[asset_name]/route.ts b/src/api/downloads/public/[asset_name]/route.ts deleted file mode 100644 index 0e1b180..0000000 --- a/src/api/downloads/public/[asset_name]/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextResponse } from "next/server"; -import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { env } from "@/env.mjs"; - -const assets: Record = { - "shapes-with-abigail": "shapes-with-abigail.apk", - "magic-delve": "magic-delve.apk", - cork: "Cork.zip", -}; - -const bucket = "frenomedownloads"; - -export async function GET( - _: Request, - context: { - params: Promise<{ asset_name: string }>; - }, -) { - const readyParams = await context.params; - const params = { - Bucket: bucket, - Key: assets[readyParams.asset_name], - Expires: 60 * 60, - }; - const credentials = { - accessKeyId: env._AWS_ACCESS_KEY, - secretAccessKey: env._AWS_SECRET_KEY, - }; - try { - const client = new S3Client({ - region: env.AWS_REGION, - credentials: credentials, - }); - - const command = new GetObjectCommand(params); - const signedUrl = await getSignedUrl(client, command, { expiresIn: 120 }); - return NextResponse.json({ downloadURL: signedUrl }); - } catch (e) { - console.log(e); - return NextResponse.json({ error: e }, { status: 400 }); - } -} diff --git a/src/api/lineage/_database_mgmt/loose/route.ts b/src/api/lineage/_database_mgmt/loose/route.ts deleted file mode 100644 index 0d3fe8f..0000000 --- a/src/api/lineage/_database_mgmt/loose/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { LineageConnectionFactory } from "@/app/utils"; -import { env } from "@/env.mjs"; -import { createClient as createAPIClient } from "@tursodatabase/api"; -import { NextResponse } from "next/server"; - -const IGNORE = ["frenome", "magic-delve-conductor"]; - -export async function GET() { - const conn = LineageConnectionFactory(); - const query = "SELECT database_url FROM User WHERE database_url IS NOT NULL"; - try { - const res = await conn.execute(query); - const turso = createAPIClient({ - org: "mikefreno", - token: env.TURSO_DB_API_TOKEN, - }); - const linkedDatabaseUrls = res.rows.map((row) => row.database_url); - - const all_dbs = await turso.databases.list(); - console.log(all_dbs); - const dbs_to_delete = all_dbs.filter((db) => { - return !IGNORE.includes(db.name) && !linkedDatabaseUrls.includes(db.name); - }); - //console.log("will delete:", dbs_to_delete); - } catch (e) { - return new NextResponse( - JSON.stringify({ - success: false, - message: e, - }), - { status: 400, headers: { "content-type": "application/json" } }, - ); - } - return new NextResponse( - JSON.stringify({ - success: true, - }), - { status: 200, headers: { "content-type": "application/json" } }, - ); -} diff --git a/src/api/lineage/_database_mgmt/old/route.ts b/src/api/lineage/_database_mgmt/old/route.ts deleted file mode 100644 index 6db0479..0000000 --- a/src/api/lineage/_database_mgmt/old/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { LineageConnectionFactory } from "@/app/utils"; -import { env } from "@/env.mjs"; - -import { createClient as createAPIClient } from "@tursodatabase/api"; -import { NextResponse } from "next/server"; - -export async function GET() { - const conn = LineageConnectionFactory(); - const query = - "SELECT * FROM User WHERE datetime(db_destroy_date) < datetime('now');"; - try { - const res = await conn.execute(query); - const turso = createAPIClient({ - org: "mikefreno", - token: env.TURSO_DB_API_TOKEN, - }); - - res.rows.forEach(async (row) => { - const db_url = row.database_url; - - await turso.databases.delete(db_url as string); - const query = - "UPDATE User SET database_url = ?, database_token = ?, db_destroy_date = ? WHERE id = ?"; - const params = [null, null, null, row.id]; - conn.execute({ sql: query, args: params }); - }); - } catch (e) { - return new NextResponse( - JSON.stringify({ - success: false, - message: e, - }), - { status: 400, headers: { "content-type": "application/json" } }, - ); - } - return new NextResponse( - JSON.stringify({ - success: true, - }), - { status: 200, headers: { "content-type": "application/json" } }, - ); -} diff --git a/src/api/lineage/analytics/route.ts b/src/api/lineage/analytics/route.ts deleted file mode 100644 index fe7be7f..0000000 --- a/src/api/lineage/analytics/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { LineageConnectionFactory } from "@/app/utils"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(req: NextRequest) { - const { - playerID, - dungeonProgression, - playerClass, - spellCount, - proficiencies, - jobs, - resistanceTable, - damageTable, - } = await req.json(); - const conn = LineageConnectionFactory(); - try { - const res = await conn.execute({ - sql: ` - INSERT OR REPLACE INTO Analytics - (playerID, dungeonProgression, playerClass, spellCount, proficiencies, jobs, resistanceTable, damageTable) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `, - args: [ - playerID, - JSON.stringify(dungeonProgression), - playerClass, - spellCount, - JSON.stringify(proficiencies), - JSON.stringify(jobs), - JSON.stringify(resistanceTable), - JSON.stringify(damageTable), - ], - }); - console.log(res); - - return NextResponse.json({ status: 200 }); - } catch (e) { - console.error(e); - - return NextResponse.json({ status: 500 }); - } -} diff --git a/src/api/lineage/apple/email/route.ts b/src/api/lineage/apple/email/route.ts deleted file mode 100644 index 065c0af..0000000 --- a/src/api/lineage/apple/email/route.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { LineageConnectionFactory } from "@/app/utils"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(req: NextRequest) { - const { userString } = await req.json(); - if (!userString) { - return new NextResponse( - JSON.stringify({ - success: false, - message: "Missing required fields", - }), - { status: 400, headers: { "content-type": "application/json" } }, - ); - } - const conn = LineageConnectionFactory(); - const query = "SELECT * FROM User WHERE apple_user_string = ?"; - const res = await conn.execute({ sql: query, args: [userString] }); - if (res.rows.length > 0) { - return NextResponse.json( - { success: true, email: res.rows[0].email }, - { status: 200 }, - ); - } else { - return NextResponse.json({ success: false }, { status: 404 }); - } -} diff --git a/src/api/lineage/apple/registration/route.ts b/src/api/lineage/apple/registration/route.ts deleted file mode 100644 index 3cd5fd9..0000000 --- a/src/api/lineage/apple/registration/route.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { LineageConnectionFactory, LineageDBInit } from "@/app/utils"; -import { NextRequest, NextResponse } from "next/server"; - -import { createClient as createAPIClient } from "@tursodatabase/api"; -import { env } from "@/env.mjs"; - -export async function POST(request: NextRequest) { - const { email, userString } = await request.json(); - if (!userString) { - return new NextResponse( - JSON.stringify({ - success: false, - message: "Missing required fields", - }), - { status: 400, headers: { "content-type": "application/json" } }, - ); - } - - let dbName; - let dbToken; - const conn = LineageConnectionFactory(); - - try { - let checkUserQuery = "SELECT * FROM User WHERE apple_user_string = ?"; - - let args = [userString]; - if (email) { - args.push(email); - checkUserQuery += " OR email = ?"; - } - const checkUserResult = await conn.execute({ - sql: checkUserQuery, - args: args, - }); - - if (checkUserResult.rows.length > 0) { - const setClauses = []; - const values = []; - - if (email) { - setClauses.push("email = ?"); - values.push(email); - } - setClauses.push("provider = ?", "apple_user_string = ?"); - values.push("apple", userString); - const whereClause = `WHERE apple_user_string = ?${ - email && " OR email = ?" - }`; - values.push(userString); - if (email) { - values.push(email); - } - - const updateQuery = `UPDATE User SET ${setClauses.join( - ", ", - )} ${whereClause}`; - const updateRes = await conn.execute({ - sql: updateQuery, - args: values, - }); - if (updateRes.rowsAffected != 0) { - return new NextResponse( - JSON.stringify({ - success: true, - message: "User information updated", - email: checkUserResult.rows[0].email, - }), - { status: 200, headers: { "content-type": "application/json" } }, - ); - } else { - return new NextResponse( - JSON.stringify({ - success: false, - message: "User update failed!", - }), - { status: 418, headers: { "content-type": "application/json" } }, - ); - } - } else { - // User doesn't exist, insert new user and init database - const dbInit = await LineageDBInit(); - dbToken = dbInit.token; - dbName = dbInit.dbName; - - try { - const insertQuery = ` - INSERT INTO User (email, email_verified, apple_user_string, provider, database_name, database_token) - VALUES (?, ?, ?, ?, ?, ?) - `; - await conn.execute({ - sql: insertQuery, - args: [email, true, userString, "apple", dbName, dbToken], - }); - - return new NextResponse( - JSON.stringify({ - success: true, - message: "New user created", - dbName, - dbToken, - }), - { status: 201, headers: { "content-type": "application/json" } }, - ); - } catch (error) { - const turso = createAPIClient({ - org: "mikefreno", - token: env.TURSO_DB_API_TOKEN, - }); - await turso.databases.delete(dbName); - console.error(error); - } - } - } catch (error) { - if (dbName) { - try { - const turso = createAPIClient({ - org: "mikefreno", - token: env.TURSO_DB_API_TOKEN, - }); - await turso.databases.delete(dbName); - } catch (deleteErr) { - console.error("Error deleting database:", deleteErr); - } - } - console.error("Error in Apple Sign-Up handler:", error); - return new NextResponse( - JSON.stringify({ - success: false, - message: "An error occurred while processing the request", - }), - { status: 500, headers: { "content-type": "application/json" } }, - ); - } -} diff --git a/src/api/lineage/database/creds/route.ts b/src/api/lineage/database/creds/route.ts deleted file mode 100644 index d230198..0000000 --- a/src/api/lineage/database/creds/route.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { env } from "@/env.mjs"; -import { NextRequest, NextResponse } from "next/server"; -import jwt from "jsonwebtoken"; -import { LineageConnectionFactory } from "@/app/utils"; -import { OAuth2Client } from "google-auth-library"; -const CLIENT_ID = env.NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE; - -const client = new OAuth2Client(CLIENT_ID); - -export async function POST(req: NextRequest) { - const authHeader = req.headers.get("authorization"); - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return new NextResponse(JSON.stringify({ valid: false }), { status: 401 }); - } - const { email, provider } = await req.json(); - if (!email) { - return new NextResponse( - JSON.stringify({ success: false, message: "missing email in body" }), - { - status: 401, - }, - ); - } - - const token = authHeader.split(" ")[1]; - - try { - let valid_request = false; - if (provider == "email") { - const decoded = jwt.verify(token, env.JWT_SECRET_KEY) as jwt.JwtPayload; - if (decoded.email == email) { - valid_request = true; - } - } else if (provider == "google") { - const ticket = await client.verifyIdToken({ - idToken: token, - audience: CLIENT_ID, - }); - if (ticket.getPayload()?.email == email) { - valid_request = true; - } - } else { - const conn = LineageConnectionFactory(); - const query = "SELECT * FROM User WHERE apple_user_string = ?"; - const res = await conn.execute({ sql: query, args: [token] }); - if (res.rows.length > 0 && res.rows[0].email == email) { - valid_request = true; - } - } - - if (valid_request) { - const conn = LineageConnectionFactory(); - const query = "SELECT * FROM User WHERE email = ? LIMIT 1"; - const params = [email]; - const res = await conn.execute({ sql: query, args: params }); - if (res.rows.length === 1) { - const user = res.rows[0]; - return new NextResponse( - JSON.stringify({ - success: true, - db_name: user.database_name, - db_token: user.database_token, - }), - { status: 200 }, - ); - } - return new NextResponse( - JSON.stringify({ success: false, message: "no user found" }), - { - status: 404, - }, - ); - } else { - return new NextResponse( - JSON.stringify({ success: false, message: "destroy token" }), - { - status: 401, - }, - ); - } - } catch (error) { - return new NextResponse( - JSON.stringify({ success: false, message: error }), - { - status: 401, - }, - ); - } -} diff --git a/src/api/lineage/database/deletion/cancel/route.ts b/src/api/lineage/database/deletion/cancel/route.ts deleted file mode 100644 index 731cac8..0000000 --- a/src/api/lineage/database/deletion/cancel/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { LineageConnectionFactory, validateLineageRequest } from "@/app/utils"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(req: NextRequest) { - const authHeader = req.headers.get("authorization"); - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return NextResponse.json({ - status: 401, - ok: false, - message: "Missing or invalid authorization header.", - }); - } - const auth_token = authHeader.split(" ")[1]; - - const { email } = await req.json(); - if (!email) { - return NextResponse.json({ - status: 400, - ok: false, - message: "Email is required to cancel the cron job.", - }); - } - - const conn = LineageConnectionFactory(); - - const resUser = await conn.execute({ - sql: `SELECT * FROM User WHERE email = ?;`, - args: [email], - }); - if (resUser.rows.length === 0) { - return NextResponse.json({ - status: 404, - ok: false, - message: "User not found.", - }); - } - const userRow = resUser.rows[0]; - if (!userRow) { - return NextResponse.json({ status: 404, ok: false }); - } - - const valid = await validateLineageRequest({ auth_token, userRow }); - if (!valid) { - return NextResponse.json({ - status: 401, - ok: false, - message: "Invalid credentials for cancelation.", - }); - } - - const result = await conn.execute({ - sql: `DELETE FROM cron WHERE email = ?;`, - args: [email], - }); - - if (result.rowsAffected > 0) { - return NextResponse.json({ - status: 200, - ok: true, - message: "Cron job(s) canceled successfully.", - }); - } else { - return NextResponse.json({ - status: 404, - ok: false, - message: "No cron job found for the given email.", - }); - } -} diff --git a/src/api/lineage/database/deletion/check/route.ts b/src/api/lineage/database/deletion/check/route.ts deleted file mode 100644 index 53581b9..0000000 --- a/src/api/lineage/database/deletion/check/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { LineageConnectionFactory } from "@/app/utils"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(req: NextRequest) { - const { email } = await req.json(); - const conn = LineageConnectionFactory(); - try { - const res = await conn.execute({ - sql: `SELECT * FROM cron WHERE email = ?`, - args: [email], - }); - const cronRow = res.rows[0]; - if (!cronRow) { - return NextResponse.json({ status: 204, ok: true }); - } - return NextResponse.json({ - ok: true, - status: 200, - created_at: cronRow.created_at, - }); - } catch (e) { - return NextResponse.json({ status: 500, ok: false }); - } -} diff --git a/src/api/lineage/database/deletion/cron/route.ts b/src/api/lineage/database/deletion/cron/route.ts deleted file mode 100644 index 6ad2198..0000000 --- a/src/api/lineage/database/deletion/cron/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { dumpAndSendDB, LineageConnectionFactory } from "@/app/utils"; -import { NextResponse } from "next/server"; -import { createClient as createAPIClient } from "@tursodatabase/api"; -import { env } from "@/env.mjs"; - -export async function GET() { - const conn = LineageConnectionFactory(); - const res = await conn.execute( - `SELECT * FROM cron WHERE created_at <= datetime('now', '-1 day');`, - ); - - if (res.rows.length > 0) { - const executed_ids = []; - for (const row of res.rows) { - const { id, db_name, db_token, send_dump_target, email } = row; - - if (send_dump_target) { - const res = await dumpAndSendDB({ - dbName: db_name as string, - dbToken: db_token as string, - sendTarget: send_dump_target as string, - }); - if (res.success) { - //const res = await turso.databases.delete(db_name as string); - // - const res = await fetch( - `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, - }, - }, - ); - if (res.ok) { - executed_ids.push(id); - // Shouldn't fail. No idea what the response from turso would be at this point - not documented - } - } - } else { - const res = await fetch( - `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, - }, - }, - ); - if (res.ok) { - conn.execute({ - sql: `DELETE FROM User WHERE email = ?`, - args: [email], - }); - executed_ids.push(id); - // Shouldn't fail. No idea what the response from turso would be at this point - not documented - } - } - } - if (executed_ids.length > 0) { - const placeholders = executed_ids.map(() => "?").join(", "); - const deleteQuery = `DELETE FROM cron WHERE id IN (${placeholders});`; - await conn.execute({ sql: deleteQuery, args: executed_ids }); - - return NextResponse.json({ - status: 200, - message: - "Processed databases deleted and corresponding cron rows removed.", - }); - } - } - return NextResponse.json({ status: 200, ok: true }); -} diff --git a/src/api/lineage/database/deletion/init/route.ts b/src/api/lineage/database/deletion/init/route.ts deleted file mode 100644 index b9b3b02..0000000 --- a/src/api/lineage/database/deletion/init/route.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { - dumpAndSendDB, - LineageConnectionFactory, - validateLineageRequest, -} from "@/app/utils"; -import { env } from "@/env.mjs"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(req: NextRequest) { - const authHeader = req.headers.get("authorization"); - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return NextResponse.json({ status: 401, ok: false }); - } - - const auth_token = authHeader.split(" ")[1]; - const { email, db_name, db_token, skip_cron, send_dump_target } = - await req.json(); - if (!email || !db_name || !db_token || !auth_token) { - return NextResponse.json({ - status: 401, - message: "Missing required fields", - }); - } - - const conn = LineageConnectionFactory(); - const res = await conn.execute({ - sql: `SELECT * FROM User WHERE email = ?`, - args: [email], - }); - const userRow = res.rows[0]; - if (!userRow) { - return NextResponse.json({ status: 404, ok: false }); - } - - const valid = await validateLineageRequest({ auth_token, userRow }); - if (!valid) { - return NextResponse.json({ - ok: false, - status: 401, - message: "Invalid Verification", - }); - } - - const { database_token, database_name } = userRow; - - if (database_token !== db_token || database_name !== db_name) { - return NextResponse.json({ - ok: false, - status: 401, - message: "Incorrect Verification", - }); - } - - if (skip_cron) { - if (send_dump_target) { - const res = await dumpAndSendDB({ - dbName: db_name, - dbToken: db_token, - sendTarget: send_dump_target, - }); - if (res.success) { - //const turso = createAPIClient({ - //org: "mikefreno", - //token: env.TURSO_DB_API_TOKEN, - //}); - //const res = await turso.databases.delete(db_name); // seems unreliable, using rest api instead - const res = await fetch( - `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, - }, - }, - ); - if (res.ok) { - conn.execute({ - sql: `DELETE FROM User WHERE email = ?`, - args: [email], - }); - return NextResponse.json({ - ok: true, - status: 200, - message: `Account and Database deleted, db dump sent to email: ${send_dump_target}`, - }); - } else { - // Shouldn't fail. No idea what the response from turso would be at this point - not documented - return NextResponse.json({ - status: 500, - message: "Unknown", - ok: false, - }); - } - } else { - return NextResponse.json({ - ok: false, - status: 500, - message: res.reason, - }); - } - } else { - //const turso = createAPIClient({ - //org: "mikefreno", - //token: env.TURSO_DB_API_TOKEN, - //}); - //const res = await turso.databases.delete(db_name); - const res = await fetch( - `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, - }, - }, - ); - if (res.ok) { - conn.execute({ - sql: `DELETE FROM User WHERE email = ?`, - args: [email], - }); - return NextResponse.json({ - ok: true, - status: 200, - message: `Account and Database deleted`, - }); - } else { - // Shouldn't fail. No idea what the response from turso would be at this point - not documented - return NextResponse.json({ - ok: false, - status: 500, - message: "Unknown", - }); - } - } - } else { - const insertRes = await conn.execute({ - sql: `INSERT INTO cron (email, db_name, db_token, send_dump_target) VALUES (?, ?, ?, ?)`, - args: [email, db_name, db_token, send_dump_target], - }); - if (insertRes.rowsAffected > 0) { - return NextResponse.json({ - ok: true, - status: 200, - message: `Deletion scheduled.`, - }); - } else { - return NextResponse.json({ - ok: false, - status: 500, - message: `Deletion not scheduled, due to server failure`, - }); - } - } -} diff --git a/src/api/lineage/email/login/route.ts b/src/api/lineage/email/login/route.ts deleted file mode 100644 index 1a12fbd..0000000 --- a/src/api/lineage/email/login/route.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { LINEAGE_JWT_EXPIRY, LineageConnectionFactory } from "@/app/utils"; -import { NextRequest, NextResponse } from "next/server"; -import { checkPassword } from "../../../passwordHashing"; -import jwt from "jsonwebtoken"; -import { env } from "@/env.mjs"; - -interface InputData { - email: string; - password: string; -} - -export async function POST(input: NextRequest) { - const inputData = (await input.json()) as InputData; - const { email, password } = inputData; - if (email && password) { - if (password.length < 8) { - return new NextResponse( - JSON.stringify({ - success: false, - message: "Invalid Credentials", - }), - { status: 401, headers: { "content-type": "application/json" } }, - ); - } - const conn = LineageConnectionFactory(); - const query = `SELECT * FROM User WHERE email = ? AND provider = ? LIMIT 1`; - const params = [email, "email"]; - const res = await conn.execute({ sql: query, args: params }); - if (res.rows.length == 0) { - return new NextResponse( - JSON.stringify({ - success: false, - message: "Invalid Credentials", - }), - { status: 401, headers: { "content-type": "application/json" } }, - ); - } - const user = res.rows[0]; - if (user.email_verified === 0) { - return new NextResponse( - JSON.stringify({ - success: false, - message: "Email not yet verified!", - }), - { status: 401, headers: { "content-type": "application/json" } }, - ); - } - const valid = await checkPassword(password, user.password_hash as string); - if (!valid) { - return new NextResponse( - JSON.stringify({ - success: false, - message: "Invalid Credentials", - }), - { status: 401, headers: { "content-type": "application/json" } }, - ); - } - - // create token - const token = jwt.sign( - { userId: user.id, email: user.email }, - env.JWT_SECRET_KEY, - { expiresIn: LINEAGE_JWT_EXPIRY }, - ); - - return NextResponse.json({ - success: true, - message: "Login successful", - token: token, - email: email, - }); - } else { - return new NextResponse( - JSON.stringify({ - success: false, - message: "Missing required fields", - }), - { status: 400, headers: { "content-type": "application/json" } }, - ); - } -} diff --git a/src/api/lineage/email/refresh/token/route.ts b/src/api/lineage/email/refresh/token/route.ts deleted file mode 100644 index 52aeb0e..0000000 --- a/src/api/lineage/email/refresh/token/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import jwt from "jsonwebtoken"; -import { env } from "@/env.mjs"; -import { LINEAGE_JWT_EXPIRY } from "@/app/utils"; - -export async function GET(req: NextRequest) { - const authHeader = req.headers.get("authorization"); - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return new NextResponse(JSON.stringify({ valid: false }), { status: 401 }); - } - - const token = authHeader.split(" ")[1]; - - try { - const decoded = jwt.verify(token, env.JWT_SECRET_KEY) as jwt.JwtPayload; - - const newToken = jwt.sign( - { userId: decoded.userId, email: decoded.email }, - env.JWT_SECRET_KEY, - { expiresIn: LINEAGE_JWT_EXPIRY }, - ); - - return NextResponse.json({ - status: 200, - ok: true, - valid: true, - token: newToken, - email: decoded.email, - }); - } catch (error) { - return NextResponse.json({ status: 401, ok: false }); - } -} diff --git a/src/api/lineage/email/refresh/verification/route.ts b/src/api/lineage/email/refresh/verification/route.ts deleted file mode 100644 index 6cb43fc..0000000 --- a/src/api/lineage/email/refresh/verification/route.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { LineageConnectionFactory } from "@/app/utils"; -import { env } from "@/env.mjs"; -import jwt from "jsonwebtoken"; -import { NextRequest, NextResponse } from "next/server"; - -interface InputData { - email: string; -} -export async function POST(input: NextRequest) { - const inputData = (await input.json()) as InputData; - const { email } = inputData; - const conn = LineageConnectionFactory(); - const query = "SELECT * FROM User WHERE email = ?"; - const params = [email]; - - const res = await conn.execute({ sql: query, args: params }); - - if (res.rows.length == 0 || res.rows[0].email_verified) { - return new NextResponse( - JSON.stringify({ - success: false, - message: "Invalid Request", - }), - { status: 409, headers: { "content-type": "application/json" } }, - ); - } - - const email_res = await sendEmailVerification(email); - const json = await email_res.json(); - if (json.messageId) { - return new NextResponse( - JSON.stringify({ - success: true, - message: "Email verification sent!", - }), - { status: 201, headers: { "content-type": "application/json" } }, - ); - } else { - return NextResponse.json(json); - } -} - -async function sendEmailVerification(userEmail: string) { - const apiKey = env.SENDINBLUE_KEY as string; - const apiUrl = "https://api.sendinblue.com/v3/smtp/email"; - - const secretKey = env.JWT_SECRET_KEY; - const payload = { email: userEmail }; - const token = jwt.sign(payload, secretKey, { expiresIn: "15m" }); - - const sendinblueData = { - sender: { - name: "MikeFreno", - email: "lifeandlineage_no_reply@freno.me", - }, - to: [ - { - email: userEmail, - }, - ], - htmlContent: ` - - - - -
-

Click the button below to verify email

-
-
- - - -`, - subject: `Life and Lineage email verification`, - }; - return await fetch(apiUrl, { - method: "POST", - headers: { - accept: "application/json", - "api-key": apiKey, - "content-type": "application/json", - }, - body: JSON.stringify(sendinblueData), - }); -} diff --git a/src/api/lineage/email/registration/route.ts b/src/api/lineage/email/registration/route.ts deleted file mode 100644 index 253ec2f..0000000 --- a/src/api/lineage/email/registration/route.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { hashPassword } from "../../../passwordHashing"; -import { LineageConnectionFactory } from "@/app/utils"; -import { env } from "@/env.mjs"; -import jwt from "jsonwebtoken"; -import { LibsqlError } from "@libsql/client/web"; - -interface InputData { - email: string; - password: string; - password_conf: string; -} - -export async function POST(input: NextRequest) { - const inputData = (await input.json()) as InputData; - const { email, password, password_conf } = inputData; - - if (email && password && password_conf) { - if (password == password_conf) { - const passwordHash = await hashPassword(password); - const conn = LineageConnectionFactory(); - const userCreationQuery = ` - INSERT INTO User (email, provider, password_hash) - VALUES (?, ?, ?) - `; - const params = [email, "email", passwordHash]; - try { - await conn.execute({ sql: userCreationQuery, args: params }); - - const res = await sendEmailVerification(email); - const json = await res.json(); - if (json.messageId) { - return new NextResponse( - JSON.stringify({ - success: true, - message: "Email verification sent!", - }), - { status: 201, headers: { "content-type": "application/json" } }, - ); - } else { - return NextResponse.json(json); - } - } catch (e) { - console.error(e); - if (e instanceof LibsqlError && e.code === "SQLITE_CONSTRAINT") { - return new NextResponse( - JSON.stringify({ - success: false, - message: "User already exists", - }), - { status: 400, headers: { "content-type": "application/json" } }, - ); - } - return new NextResponse( - JSON.stringify({ - success: false, - message: "An error occurred while creating the user", - }), - { status: 500, headers: { "content-type": "application/json" } }, - ); - } - } - return new NextResponse( - JSON.stringify({ - success: false, - message: "Password mismatch", - }), - { status: 400, headers: { "content-type": "application/json" } }, - ); - } - return new NextResponse( - JSON.stringify({ - success: false, - message: "Missing required fields", - }), - { status: 400, headers: { "content-type": "application/json" } }, - ); -} - -async function sendEmailVerification(userEmail: string) { - const apiKey = env.SENDINBLUE_KEY as string; - const apiUrl = "https://api.sendinblue.com/v3/smtp/email"; - - const secretKey = env.JWT_SECRET_KEY; - const payload = { email: userEmail }; - const token = jwt.sign(payload, secretKey, { expiresIn: "15m" }); - - const sendinblueData = { - sender: { - name: "MikeFreno", - email: "lifeandlineage_no_reply@freno.me", - }, - to: [ - { - email: userEmail, - }, - ], - htmlContent: ` - - - - -
-

Click the button below to verify email

-
-
- - - -`, - subject: `Life and Lineage email verification`, - }; - return await fetch(apiUrl, { - method: "POST", - headers: { - accept: "application/json", - "api-key": apiKey, - "content-type": "application/json", - }, - body: JSON.stringify(sendinblueData), - }); -} diff --git a/src/api/lineage/email/verification/[email]/route.ts b/src/api/lineage/email/verification/[email]/route.ts deleted file mode 100644 index 6928abd..0000000 --- a/src/api/lineage/email/verification/[email]/route.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { env } from "@/env.mjs"; -import jwt, { JwtPayload } from "jsonwebtoken"; -import { LineageConnectionFactory, LineageDBInit } from "@/app/utils"; -import { createClient as createAPIClient } from "@tursodatabase/api"; - -export async function GET( - request: NextRequest, - context: { params: Promise<{ email: string }> }, -) { - const secretKey = env.JWT_SECRET_KEY; - const params = request.nextUrl.searchParams; - const token = params.get("token"); - const userEmail = (await context.params).email; - - let conn; - let dbName; - let dbToken; - - try { - if (!token) { - return NextResponse.json( - { success: false, message: "Authentication failed: no token" }, - { status: 401, headers: { "content-type": "application/json" } }, - ); - } - - const decoded = jwt.verify(token, secretKey) as JwtPayload; - if (decoded.email !== userEmail) { - return NextResponse.json( - { success: false, message: "Authentication failed: email mismatch" }, - { status: 401, headers: { "content-type": "application/json" } }, - ); - } - - conn = LineageConnectionFactory(); - const dbInit = await LineageDBInit(); - dbName = dbInit.dbName; - dbToken = dbInit.token; - - const query = `UPDATE User SET email_verified = ?, database_name = ?, database_token = ? WHERE email = ?`; - const queryParams = [true, dbName, dbToken, userEmail]; - const res = await conn.execute({ sql: query, args: queryParams }); - - if (res.rowsAffected === 0) { - throw new Error("User not found or update failed"); - } - - return new NextResponse( - JSON.stringify({ - success: true, - message: - "Email verification success. You may close this window and sign in within the app.", - }), - { status: 202, headers: { "content-type": "application/json" } }, - ); - } catch (err) { - console.error("Error in email verification:", err); - - // Delete the database if it was created - if (dbName) { - try { - const turso = createAPIClient({ - org: "mikefreno", - token: env.TURSO_DB_API_TOKEN, - }); - await turso.databases.delete(dbName); - console.log(`Database ${dbName} deleted due to error`); - } catch (deleteErr) { - console.error("Error deleting database:", deleteErr); - } - } - - // Attempt to revert the User table update if conn is available - if (conn) { - try { - await conn.execute({ - sql: `UPDATE User SET email_verified = ?, database_name = ?, database_token = ? WHERE email = ?`, - args: [false, null, null, userEmail], - }); - console.log("User table update reverted"); - } catch (revertErr) { - console.error("Error reverting User table update:", revertErr); - } - } - - return new NextResponse( - JSON.stringify({ - success: false, - message: - "Authentication failed: An error occurred during email verification. Please try again.", - }), - { status: 500, headers: { "content-type": "application/json" } }, - ); - } -} diff --git a/src/api/lineage/google/registration/route.ts b/src/api/lineage/google/registration/route.ts deleted file mode 100644 index fe0a9cc..0000000 --- a/src/api/lineage/google/registration/route.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { LineageConnectionFactory, LineageDBInit } from "@/app/utils"; -import { NextRequest, NextResponse } from "next/server"; - -import { createClient as createAPIClient } from "@tursodatabase/api"; -import { env } from "@/env.mjs"; - -export async function POST(request: NextRequest) { - const { email } = await request.json(); - if (!email) { - return new NextResponse( - JSON.stringify({ - success: false, - message: "Missing required fields", - }), - { status: 400, headers: { "content-type": "application/json" } }, - ); - } - - const conn = LineageConnectionFactory(); - - try { - // Check if the user exists - const checkUserQuery = "SELECT * FROM User WHERE email = ?"; - const checkUserResult = await conn.execute({ - sql: checkUserQuery, - args: [email], - }); - - if (checkUserResult.rows.length > 0) { - const updateQuery = ` - UPDATE User - SET provider = ? - WHERE email = ? - `; - const updateRes = await conn.execute({ - sql: updateQuery, - args: ["google", email], - }); - - if (updateRes.rowsAffected != 0) { - return new NextResponse( - JSON.stringify({ - success: true, - message: "User information updated", - }), - { status: 200, headers: { "content-type": "application/json" } }, - ); - } else { - return new NextResponse( - JSON.stringify({ - success: false, - message: "User update failed!", - }), - { status: 418, headers: { "content-type": "application/json" } }, - ); - } - } else { - // User doesn't exist, insert new user and init database - let db_name; - try { - const { token, dbName } = await LineageDBInit(); - db_name = dbName; - console.log("init success"); - const insertQuery = ` - INSERT INTO User (email, email_verified, provider, database_name, database_token) - VALUES (?, ?, ?, ?, ?) - `; - await conn.execute({ - sql: insertQuery, - args: [email, true, "google", dbName, token], - }); - - console.log("insert success"); - - return new NextResponse( - JSON.stringify({ - success: true, - message: "New user created", - }), - { status: 201, headers: { "content-type": "application/json" } }, - ); - } catch (error) { - const turso = createAPIClient({ - org: "mikefreno", - token: env.TURSO_DB_API_TOKEN, - }); - await turso.databases.delete(db_name!); - console.error(error); - } - } - } catch (error) { - console.error("Error in Google Sign-Up handler:", error); - return new NextResponse( - JSON.stringify({ - success: false, - message: "An error occurred while processing the request", - }), - { status: 500, headers: { "content-type": "application/json" } }, - ); - } -} diff --git a/src/api/lineage/json_service/attacks/route.ts b/src/api/lineage/json_service/attacks/route.ts deleted file mode 100644 index a471070..0000000 --- a/src/api/lineage/json_service/attacks/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import playerAttacks from "@/lineage-json/attack-route/playerAttacks.json"; -import mageBooks from "@/lineage-json/attack-route/mageBooks.json"; -import mageSpells from "@/lineage-json/attack-route/mageSpells.json"; -import necroBooks from "@/lineage-json/attack-route/necroBooks.json"; -import necroSpells from "@/lineage-json/attack-route/necroSpells.json"; -import rangerBooks from "@/lineage-json/attack-route/rangerBooks.json"; -import rangerSpells from "@/lineage-json/attack-route/rangerSpells.json"; -import paladinBooks from "@/lineage-json/attack-route/paladinBooks.json"; -import paladinSpells from "@/lineage-json/attack-route/paladinSpells.json"; -import summons from "@/lineage-json/attack-route/summons.json"; -import { NextResponse } from "next/server"; - -export async function GET() { - return NextResponse.json({ - ok: true, - playerAttacks, - mageBooks, - mageSpells, - necroBooks, - necroSpells, - rangerBooks, - rangerSpells, - paladinBooks, - paladinSpells, - summons, - }); -} diff --git a/src/api/lineage/json_service/conditions/route.ts b/src/api/lineage/json_service/conditions/route.ts deleted file mode 100644 index 3e27497..0000000 --- a/src/api/lineage/json_service/conditions/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import conditions from "@/lineage-json/conditions-route/conditions.json"; -import debilitations from "@/lineage-json/conditions-route/debilitations.json"; -import sanityDebuffs from "@/lineage-json/conditions-route/sanityDebuffs.json"; -import { NextResponse } from "next/server"; - -export async function GET() { - return NextResponse.json({ - ok: true, - conditions, - debilitations, - sanityDebuffs, - }); -} diff --git a/src/api/lineage/json_service/dungeons/route.ts b/src/api/lineage/json_service/dungeons/route.ts deleted file mode 100644 index 503858a..0000000 --- a/src/api/lineage/json_service/dungeons/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import dungeons from "@/lineage-json/dungeon-route/dungeons.json"; -import specialEncounters from "@/lineage-json/dungeon-route/specialEncounters.json"; -import { NextResponse } from "next/server"; - -export async function GET() { - return NextResponse.json({ ok: true, dungeons, specialEncounters }); -} diff --git a/src/api/lineage/json_service/enemies/route.ts b/src/api/lineage/json_service/enemies/route.ts deleted file mode 100644 index 15c3a59..0000000 --- a/src/api/lineage/json_service/enemies/route.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NextResponse } from "next/server"; -import bosses from "@/lineage-json/enemy-route/bosses.json"; -import enemies from "@/lineage-json/enemy-route/enemy.json"; -import enemyAttacks from "@/lineage-json/enemy-route/enemyAttacks.json"; - -export async function GET() { - return NextResponse.json({ ok: true, bosses, enemies, enemyAttacks }); -} diff --git a/src/api/lineage/json_service/items/route.ts b/src/api/lineage/json_service/items/route.ts deleted file mode 100644 index 636428b..0000000 --- a/src/api/lineage/json_service/items/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NextResponse } from "next/server"; -import arrows from "@/lineage-json/item-route/arrows.json"; -import bows from "@/lineage-json/item-route/bows.json"; -import foci from "@/lineage-json/item-route/foci.json"; -import hats from "@/lineage-json/item-route/hats.json"; -import junk from "@/lineage-json/item-route/junk.json"; -import melee from "@/lineage-json/item-route/melee.json"; -import robes from "@/lineage-json/item-route/robes.json"; -import wands from "@/lineage-json/item-route/wands.json"; -import ingredients from "@/lineage-json/item-route/ingredients.json"; -import storyItems from "@/lineage-json/item-route/storyItems.json"; -import artifacts from "@/lineage-json/item-route/artifacts.json"; -import shields from "@/lineage-json/item-route/shields.json"; -import bodyArmor from "@/lineage-json/item-route/bodyArmor.json"; -import helmets from "@/lineage-json/item-route/helmets.json"; -import suffix from "@/lineage-json/item-route/suffix.json"; -import prefix from "@/lineage-json/item-route/prefix.json"; -import potions from "@/lineage-json/item-route/potions.json"; -import poison from "@/lineage-json/item-route/poison.json"; -import staves from "@/lineage-json/item-route/staves.json"; - -export async function GET() { - return NextResponse.json({ - ok: true, - arrows, - bows, - foci, - hats, - junk, - melee, - robes, - wands, - ingredients, - storyItems, - artifacts, - shields, - bodyArmor, - helmets, - suffix, - prefix, - potions, - poison, - staves, - }); -} diff --git a/src/api/lineage/json_service/misc/route.ts b/src/api/lineage/json_service/misc/route.ts deleted file mode 100644 index 80248d7..0000000 --- a/src/api/lineage/json_service/misc/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextResponse } from "next/server"; -import activities from "@/lineage-json/misc-route/activities.json"; -import investments from "@/lineage-json/misc-route/investments.json"; -import jobs from "@/lineage-json/misc-route/jobs.json"; -import manaOptions from "@/lineage-json/misc-route/manaOptions.json"; -import otherOptions from "@/lineage-json/misc-route/otherOptions.json"; -import healthOptions from "@/lineage-json/misc-route/healthOptions.json"; -import sanityOptions from "@/lineage-json/misc-route/sanityOptions.json"; -import pvpRewards from "@/lineage-json/misc-route/pvpRewards.json"; - -export async function GET() { - return NextResponse.json({ - ok: true, - activities, - investments, - jobs, - manaOptions, - otherOptions, - healthOptions, - sanityOptions, - pvpRewards, - }); -} diff --git a/src/api/lineage/offline_secret/route.ts b/src/api/lineage/offline_secret/route.ts deleted file mode 100644 index cf1e0ba..0000000 --- a/src/api/lineage/offline_secret/route.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NextResponse } from "next/server"; - -export async function GET() { - return new NextResponse(process.env.LINEAGE_OFFLINE_SERIALIZATION_SECRET); -} diff --git a/src/api/lineage/pvp/battle_result/route.ts b/src/api/lineage/pvp/battle_result/route.ts deleted file mode 100644 index 33f502c..0000000 --- a/src/api/lineage/pvp/battle_result/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { LineageConnectionFactory } from "@/app/utils"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(req: NextRequest) { - const { winnerLinkID, loserLinkID } = await req.json(); - - const conn = LineageConnectionFactory(); - - try { - await conn.execute({ - sql: ` - UPDATE PvP_Characters - SET - winCount = winCount + CASE WHEN linkID = ? THEN 1 ELSE 0 END, - lossCount = lossCount + CASE WHEN linkID = ? THEN 1 ELSE 0 END - WHERE linkID IN (?, ?) - `, - args: [winnerLinkID, loserLinkID, winnerLinkID, loserLinkID], - }); - return NextResponse.json({ - ok: true, - status: 200, - }); - } catch (e) { - console.error(e); - return NextResponse.json({ ok: false, status: 500 }); - } -} diff --git a/src/api/lineage/pvp/route.ts b/src/api/lineage/pvp/route.ts deleted file mode 100644 index bf8cd2f..0000000 --- a/src/api/lineage/pvp/route.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { LineageConnectionFactory } from "@/app/utils"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(req: NextRequest) { - const { character, linkID, pushToken, pushCurrentlyEnabled } = - await req.json(); - - try { - const conn = LineageConnectionFactory(); - const res = await conn.execute({ - sql: `SELECT * FROM PvP_Characters WHERE linkID = ?`, - args: [linkID], - }); - if (res.rows.length == 0) { - //create - await conn.execute({ - sql: `INSERT INTO PvP_Characters ( - linkID, - blessing, - playerClass, - name, - maxHealth, - maxSanity, - maxMana, - baseManaRegen, - strength, - intelligence, - dexterity, - resistanceTable, - damageTable, - attackStrings, - knownSpells, - pushToken, - pushCurrentlyEnabled - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - args: [ - linkID, - character.playerClass, - character.name, - character.maxHealth, - character.maxSanity, - character.maxMana, - character.baseManaRegen, - character.strength, - character.intelligence, - character.dexterity, - character.resistanceTable, - character.damageTable, - character.attackStrings, - character.knownSpells, - pushToken, - pushCurrentlyEnabled, - ], - }); - return NextResponse.json({ - ok: true, - winCount: 0, - lossCount: 0, - tokenRedemptionCount: 0, - status: 201, - }); - } else { - //update - await conn.execute({ - sql: `UPDATE PvP_Characters SET - playerClass = ?, - blessing = ?, - name = ?, - maxHealth = ?, - maxSanity = ?, - maxMana = ?, - baseManaRegen = ?, - strength = ?, - intelligence = ?, - dexterity = ?, - resistanceTable = ?, - damageTable = ?, - attackStrings = ?, - knownSpells = ?, - pushToken = ?, - pushCurrentlyEnabled = ? - WHERE linkID = ?`, - args: [ - character.playerClass, - character.blessing, - character.name, - character.maxHealth, - character.maxSanity, - character.maxMana, - character.baseManaRegen, - character.strength, - character.intelligence, - character.dexterity, - character.resistanceTable, - character.damageTable, - character.attackStrings, - character.knownSpells, - pushToken, - pushCurrentlyEnabled, - linkID, - ], - }); - return NextResponse.json({ - ok: true, - winCount: res.rows[0].winCount, - lossCount: res.rows[0].lossCount, - tokenRedemptionCount: res.rows[0].tokenRedemptionCount, - status: 200, - }); - } - } catch (e) { - console.error(e); - return NextResponse.json({ ok: false, status: 500 }); - } -} - -export async function GET() { - // Get three opponents, high, med, low, based on win/loss ratio - const conn = LineageConnectionFactory(); - try { - const res = await conn.execute( - ` - SELECT playerClass, - blessing, - name, - maxHealth, - maxSanity, - maxMana, - baseManaRegen, - strength, - intelligence, - dexterity, - resistanceTable, - damageTable, - attackStrings, - knownSpells, - linkID, - winCount, - lossCount - FROM PvP_Characters - ORDER BY RANDOM() - LIMIT 3 - `, - ); - return NextResponse.json({ - ok: true, - characters: res.rows, - status: 200, - }); - } catch (e) { - console.error(e); - return NextResponse.json({ ok: false, status: 500 }); - } -} diff --git a/src/api/lineage/tokens/route.ts b/src/api/lineage/tokens/route.ts deleted file mode 100644 index 5059814..0000000 --- a/src/api/lineage/tokens/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { LineageConnectionFactory } from "@/app/utils"; -import { NextRequest, NextResponse } from "next/server"; - -export async function POST(req: NextRequest) { - const { token } = await req.json(); - if (!token) { - return new NextResponse( - JSON.stringify({ success: false, message: "missing token in body" }), - { - status: 401, - }, - ); - } - const conn = LineageConnectionFactory(); - const query = "SELECT * FROM Token WHERE token = ?"; - const res = await conn.execute({ sql: query, args: [token] }); - if (res.rows.length > 0) { - const queryUpdate = - "UPDATE Token SET last_updated_at = datetime('now') WHERE token = ?"; - const resUpdate = await conn.execute({ sql: queryUpdate, args: [token] }); - return NextResponse.json(JSON.stringify(resUpdate)); - } else { - const queryInsert = "INSERT INTO Token (token) VALUES (?)"; - const resInsert = await conn.execute({ sql: queryInsert, args: [token] }); - return NextResponse.json(JSON.stringify(resInsert)); - } -} diff --git a/src/api/passwordHashing.ts b/src/api/passwordHashing.ts deleted file mode 100644 index 28f1f4f..0000000 --- a/src/api/passwordHashing.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as bcrypt from "bcrypt"; - -// Asynchronous function to hash a password -export async function hashPassword(password: string): Promise { - // 10 here is the number of rounds of hashing to apply - // The higher the number, the more secure but also the slower - const saltRounds = 10; - const salt = await bcrypt.genSalt(saltRounds); - const hashedPassword = await bcrypt.hash(password, salt); - return hashedPassword; -} - -// Asynchronous function to check a password against a hash -export async function checkPassword( - password: string, - hash: string -): Promise { - const match = await bcrypt.compare(password, hash); - return match; -} diff --git a/src/api/s3/deleteImage/route.ts b/src/api/s3/deleteImage/route.ts deleted file mode 100644 index 902a4cd..0000000 --- a/src/api/s3/deleteImage/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3"; -import { NextRequest } from "next/dist/server/web/spec-extension/request"; -import { NextResponse } from "next/server"; -import { ConnectionFactory } from "@/app/utils"; -import { env } from "@/env.mjs"; - -interface InputData { - key: string; - newAttachmentString: string; - type: string; - id: number; -} - -export async function POST(input: NextRequest) { - const inputData = (await input.json()) as InputData; - const { key, newAttachmentString, type, id } = inputData; - // Parse the url to get the bucket and key - - const s3params = { - Bucket: env.AWS_S3_BUCKET_NAME, - Key: key, - }; - - const client = new S3Client({ - region: env.AWS_REGION, - }); - - const command = new DeleteObjectCommand(s3params); - const res = await client.send(command); - const conn = ConnectionFactory(); - const query = `UPDATE ${type} SET attachments = ? WHERE id = ?`; - const dbparams = [newAttachmentString, id]; - await conn.execute({ sql: query, args: dbparams }); - return NextResponse.json(res); -} diff --git a/src/api/s3/getPreSignedURL/route.ts b/src/api/s3/getPreSignedURL/route.ts deleted file mode 100644 index 3fedb3d..0000000 --- a/src/api/s3/getPreSignedURL/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; -import { NextRequest, NextResponse } from "next/server"; -import { env } from "@/env.mjs"; - -interface InputData { - type: string; - title: string; - filename: string; -} - -export async function POST(input: NextRequest) { - const inputData = (await input.json()) as InputData; - const { type, title, filename } = inputData; - const credentials = { - accessKeyId: env._AWS_ACCESS_KEY, - secretAccessKey: env._AWS_SECRET_KEY, - }; - try { - const client = new S3Client({ - region: env.AWS_REGION, - credentials: credentials, - }); - const Key = `${type}/${title}/${filename}`; - const ext = /^.+\.([^.]+)$/.exec(filename); - - const s3params = { - Bucket: env.AWS_S3_BUCKET_NAME, - Key, - ContentType: `image/${ext![1]}`, - }; - 3; - const command = new PutObjectCommand(s3params); - - const signedUrl = await getSignedUrl(client, command, { expiresIn: 120 }); - return NextResponse.json({ uploadURL: signedUrl, key: Key }); - } catch (e) { - console.log(e); - return NextResponse.json({ error: e }, { status: 400 }); - } -} diff --git a/src/api/s3/simpleDeleteImage/route.ts b/src/api/s3/simpleDeleteImage/route.ts deleted file mode 100644 index 0dbb5a3..0000000 --- a/src/api/s3/simpleDeleteImage/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3"; -import { NextRequest } from "next/dist/server/web/spec-extension/request"; -import { NextResponse } from "next/server"; - -import { env } from "@/env.mjs"; - -interface InputData { - key: string; - newAttachmentString: string; - type: string; - id: number; -} - -export async function POST(input: NextRequest) { - const inputData = (await input.json()) as InputData; - const { key } = inputData; - // Parse the url to get the bucket and key - - const s3params = { - Bucket: env.AWS_S3_BUCKET_NAME, - Key: key, - }; - - const client = new S3Client({ - region: env.AWS_REGION, - }); - - const command = new DeleteObjectCommand(s3params); - const res = await client.send(command); - return NextResponse.json(res); -} diff --git a/src/entry-server.tsx b/src/entry-server.tsx index fbf7abe..fb9b651 100644 --- a/src/entry-server.tsx +++ b/src/entry-server.tsx @@ -3,7 +3,7 @@ import { createHandler, StartServer } from "@solidjs/start/server"; import { validateServerEnv } from "./env/server"; try { - const validatedEnv = validateServerEnv(import.meta.env); + const validatedEnv = validateServerEnv(process.env); console.log("Environment validation successful"); } catch (error) { console.error("Environment validation failed:", error); diff --git a/src/env/server.ts b/src/env/server.ts index 2cc81bc..ff8bd51 100644 --- a/src/env/server.ts +++ b/src/env/server.ts @@ -22,6 +22,19 @@ const serverEnvSchema = z.object({ TURSO_LINEAGE_TOKEN: z.string().min(1), TURSO_DB_API_TOKEN: z.string().min(1), LINEAGE_OFFLINE_SERIALIZATION_SECRET: z.string().min(1), + // Client-side variables accessible on server + VITE_DOMAIN: z.string().min(1).optional(), + VITE_AWS_BUCKET_STRING: z.string().min(1).optional(), + VITE_GOOGLE_CLIENT_ID: z.string().min(1).optional(), + VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1).optional(), + VITE_GITHUB_CLIENT_ID: z.string().min(1).optional(), + VITE_WEBSOCKET: z.string().min(1).optional(), + // Aliases for backward compatibility + NEXT_PUBLIC_DOMAIN: z.string().min(1).optional(), + NEXT_PUBLIC_AWS_BUCKET_STRING: z.string().min(1).optional(), + NEXT_PUBLIC_GITHUB_CLIENT_ID: z.string().min(1).optional(), + NEXT_PUBLIC_GOOGLE_CLIENT_ID: z.string().min(1).optional(), + NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE: z.string().min(1).optional(), }); const clientEnvSchema = z.object({ @@ -66,15 +79,21 @@ export const validateServerEnv = ( const formattedErrors = error.format(); const missingVars = Object.entries(formattedErrors) .filter( - ([_, value]) => - value._errors.length > 0 && value._errors[0] === "Required", + ([key, value]) => + key !== "_errors" && + typeof value === "object" && + value._errors?.length > 0 && + value._errors[0] === "Required", ) .map(([key, _]) => key); const invalidVars = Object.entries(formattedErrors) .filter( - ([_, value]) => - value._errors.length > 0 && value._errors[0] !== "Required", + ([key, value]) => + key !== "_errors" && + typeof value === "object" && + value._errors?.length > 0 && + value._errors[0] !== "Required", ) .map(([key, value]) => ({ key, @@ -116,15 +135,21 @@ export const validateClientEnv = ( const formattedErrors = error.format(); const missingVars = Object.entries(formattedErrors) .filter( - ([_, value]) => - value._errors.length > 0 && value._errors[0] === "Required", + ([key, value]) => + key !== "_errors" && + typeof value === "object" && + value._errors?.length > 0 && + value._errors[0] === "Required", ) .map(([key, _]) => key); const invalidVars = Object.entries(formattedErrors) .filter( - ([_, value]) => - value._errors.length > 0 && value._errors[0] !== "Required", + ([key, value]) => + key !== "_errors" && + typeof value === "object" && + value._errors?.length > 0 && + value._errors[0] !== "Required", ) .map(([key, value]) => ({ key, @@ -158,8 +183,8 @@ export const validateClientEnv = ( // Environment validation for server startup with better error reporting export const env = (() => { try { - // Validate server environment variables - const validatedServerEnv = validateServerEnv(import.meta.env); + // Validate server environment variables using process.env + const validatedServerEnv = validateServerEnv(process.env); console.log("✅ Environment validation successful"); return validatedServerEnv; @@ -194,12 +219,20 @@ export const getClientEnvValidation = () => { // Helper function to check if a variable is missing export const isMissingEnvVar = (varName: string): boolean => { + return !process.env[varName] || process.env[varName]?.trim() === ""; +}; + +// Helper function to check if a client variable is missing +export const isMissingClientEnvVar = (varName: string): boolean => { return !import.meta.env[varName] || import.meta.env[varName]?.trim() === ""; }; // Helper function to get all missing environment variables -export const getMissingEnvVars = (): string[] => { - const requiredVars = [ +export const getMissingEnvVars = (): { + server: string[]; + client: string[]; +} => { + const requiredServerVars = [ "NODE_ENV", "ADMIN_EMAIL", "ADMIN_ID", @@ -222,5 +255,19 @@ export const getMissingEnvVars = (): string[] => { "LINEAGE_OFFLINE_SERIALIZATION_SECRET", ]; - return requiredVars.filter((varName) => isMissingEnvVar(varName)); + const requiredClientVars = [ + "VITE_DOMAIN", + "VITE_AWS_BUCKET_STRING", + "VITE_GOOGLE_CLIENT_ID", + "VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE", + "VITE_GITHUB_CLIENT_ID", + "VITE_WEBSOCKET", + ]; + + return { + server: requiredServerVars.filter((varName) => isMissingEnvVar(varName)), + client: requiredClientVars.filter((varName) => + isMissingClientEnvVar(varName), + ), + }; }; diff --git a/src/lineage-json/README.md b/src/lineage-json/README.md new file mode 100644 index 0000000..5524611 --- /dev/null +++ b/src/lineage-json/README.md @@ -0,0 +1 @@ +Do not directly modify this directory, it is synced from lineage using the script `json-sync.ts` diff --git a/src/lineage-json/attack-route/mageBooks.json b/src/lineage-json/attack-route/mageBooks.json new file mode 100644 index 0000000..498a40b --- /dev/null +++ b/src/lineage-json/attack-route/mageBooks.json @@ -0,0 +1,248 @@ +[ + { + "name": "book of fire bolt", + "type": "spell", + "teaches": "fire bolt", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of catch flame", + "type": "spell", + "teaches": "catch flame", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of scorch", + "type": "spell", + "teaches": "scorch", + "icon": "Book", + "baseValue": 20000 + }, + { + "name": "book of fireballs", + "type": "spell", + "teaches": "fireball", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of fire wall", + "type": "spell", + "teaches": "fire wall", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "book of rain fire", + "type": "spell", + "teaches": "rain fire", + "icon": "Book_2", + "baseValue": 200000 + }, + { + "name": "forbidden fire techniques vol. 1", + "type": "spell", + "teaches": "dragons breath", + "icon": "Book_3", + "baseValue": 500000 + }, + { + "name": "forbidden fire techniques vol. 2", + "type": "spell", + "teaches": "sunbeam", + "icon": "Book_3", + "baseValue": 1000000 + }, + { + "name": "book of frost", + "type": "spell", + "teaches": "frost", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of water whip", + "type": "spell", + "teaches": "water whip", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of soothing waters", + "type": "spell", + "teaches": "soothing waters", + "icon": "Book", + "baseValue": 10000 + }, + { + "name": "book of cone of cold", + "type": "spell", + "teaches": "cone of cold", + "icon": "Book", + "baseValue": 20000 + }, + { + "name": "book of steam blast", + "type": "spell", + "teaches": "steam blast", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of ice spike", + "type": "spell", + "teaches": "ice spike", + "icon": "Book_2", + "baseValue": 100000 + }, + { + "name": "book of healing rain", + "type": "spell", + "teaches": "healing rain", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "book of orb of cold", + "type": "spell", + "teaches": "orb of cold", + "icon": "Book_2", + "baseValue": 200000 + }, + { + "name": "forbidden water techniques vol. 1", + "type": "spell", + "teaches": "torrent", + "icon": "Book_3", + "baseValue": 500000 + }, + { + "name": "forbidden water techniques vol. 2", + "type": "spell", + "teaches": "spike field", + "icon": "Book_3", + "baseValue": 1000000 + }, + { + "name": "book of air burst", + "type": "spell", + "teaches": "air burst", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of sparks", + "type": "spell", + "teaches": "sparks", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of lightning", + "type": "spell", + "teaches": "lightning", + "icon": "Book", + "baseValue": 20000 + }, + { + "name": "book of thunder clap", + "type": "spell", + "teaches": "thunder clap", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of chain lightning", + "type": "spell", + "teaches": "chain lightning", + "icon": "Book_2", + "baseValue": 100000 + }, + { + "name": "book of wind blades", + "type": "spell", + "teaches": "wind blades", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "book of gale", + "type": "spell", + "teaches": "surrounding gale", + "icon": "Book_2", + "baseValue": 200000 + }, + { + "name": "forbidden air techniques vol. 1", + "type": "spell", + "teaches": "tornado", + "icon": "Book_3", + "baseValue": 500000 + }, + { + "name": "forbidden air techniques vol. 2", + "type": "spell", + "teaches": "suffocate", + "icon": "Book_3", + "baseValue": 1000000 + }, + { + "name": "book of rock toss", + "type": "spell", + "teaches": "rock toss", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of stone hands", + "type": "spell", + "teaches": "stone hands", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of stone salvo", + "type": "spell", + "teaches": "stone salvo", + "icon": "Book", + "baseValue": 20000 + }, + { + "name": "book of rock armor", + "type": "spell", + "teaches": "rock armor", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of stalactite storm", + "type": "spell", + "teaches": "stalactite storm", + "icon": "Book_2", + "baseValue": 100000 + }, + { + "name": "book of rock wall", + "type": "spell", + "teaches": "rock wall", + "icon": "Book_2", + "baseValue": 200000 + }, + { + "name": "forbidden earth techniques vol. 1", + "type": "spell", + "teaches": "earthquake", + "icon": "Book_3", + "baseValue": 500000 + }, + { + "name": "forbidden earth techniques vol. 2", + "type": "spell", + "teaches": "collider", + "icon": "Book_3", + "baseValue": 1000000 + } +] + diff --git a/src/lineage-json/attack-route/mageSpells.json b/src/lineage-json/attack-route/mageSpells.json new file mode 100644 index 0000000..e3df075 --- /dev/null +++ b/src/lineage-json/attack-route/mageSpells.json @@ -0,0 +1,794 @@ +[ + { + "name": "fire bolt", + "element": "fire", + "proficiencyNeeded": "novice", + "targets": "single", + "type": "offense", + "manaCost": 10, + "damageTable": { + "fire": 15 + }, + "debuffNames": [ + { + "name": "burn", + "chance": 0.25 + } + ], + "animation": { + "sprite": "fireMissile", + "style": "missile", + "position": "enemy", + "scale": 0.3, + "reachTargetAtFrame": 5 + } + }, + { + "name": "catch flame", + "element": "fire", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "apprentice", + "manaCost": 20, + "damageTable": { + "fire": 25 + }, + "debuffNames": [ + { + "name": "burn", + "chance": 0.25 + } + ], + "animation": { + "sprite": "fireSlash", + "style": "static", + "position": "enemy", + "scale": 0.5 + } + }, + { + "name": "scorch", + "element": "fire", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 50, + "damageTable": { + "fire": 25 + }, + "debuffNames": [ + { + "name": "burn", + "chance": 1.0 + } + ], + "animation": { + "sprite": "flameDust", + "style": "static", + "position": "enemy", + "scale": 0.6 + } + }, + { + "name": "fireball", + "element": "fire", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 75, + "damageTable": { + "fire": 60 + }, + "debuffNames": [ + { + "name": "burn", + "chance": 0.25 + } + ], + "animation": { + "sprite": "fireMissile", + "style": "missile", + "position": "enemy", + "scale": 0.7, + "reachTargetAtFrame": 5 + } + }, + { + "name": "fire wall", + "element": "fire", + "targets": "area", + "type": "defense", + "proficiencyNeeded": "expert", + "manaCost": 100, + "maxTurnsActive": 3, + "buffNames": [ + "projectile suppression" + ], + "debuffNames": [ + { + "name": "burn", + "chance": 0.5 + } + ], + "animation": { + "sprite": "flameWall", + "style": "static", + "position": "field", + "scale": 0.8 + } + }, + { + "name": "rain fire", + "element": "fire", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "master", + "manaCost": 250, + "maxTurnsActive": 5, + "damageTable": { + "fire": 50 + }, + "debuffNames": [ + { + "name": "burn", + "chance": 0.25 + } + ], + "animation": { + "sprite": "fireRain", + "style": "static", + "position": "enemy" + } + }, + { + "name": "dragons breath", + "element": "fire", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 600, + "damageTable": { + "fire": 275 + }, + "debuffNames": [ + { + "name": "severe burn", + "chance": 0.75 + } + ], + "animation": { + "sprite": "dragonBreath", + "style": "static", + "position": "enemy", + "topOffset": 30 + } + }, + { + "name": "sunbeam", + "element": "fire", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 800, + "damageTable": { + "fire": 400 + }, + "debuffNames": [ + { + "name": "severe burn", + "chance": 1.0 + } + ], + "animation": { + "sprite": "fireBeam", + "style": "span", + "position": "self", + "scale": 1.2 + } + }, + { + "name": "frost", + "element": "water", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "novice", + "manaCost": 10, + "damageTable": { + "cold": 10 + }, + "debuffNames": [ + { + "name": "chill", + "chance": 0.5 + } + ], + "animation": { + "sprite": "iceBlock", + "style": "static", + "position": "enemy", + "scale": 0.3 + } + }, + { + "name": "water whip", + "element": "water", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "apprentice", + "manaCost": 20, + "damageTable": { + "physical": 30 + }, + "animation": { + "sprite": "ambiguousSparks", + "style": "static", + "position": "enemy", + "scale": 0.4 + } + }, + { + "name": "soothing waters", + "element": "water", + "targets": "single", + "type": "defense", + "proficiencyNeeded": "apprentice", + "manaCost": 50, + "selfDamageTable": { + "raw": -35 + }, + "animation": { + "sprite": "splash", + "style": "static", + "position": "self" + } + }, + { + "name": "cone of cold", + "element": "water", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 100, + "damageTable": { + "cold": 40 + }, + "debuffNames": [ + { + "name": "chill", + "chance": 1.0 + }, + { + "name": "chill", + "chance": 1.0 + } + ], + "animation": { + "sprite": "coldSmoke", + "style": "span", + "position": "self" + } + }, + { + "name": "steam blast", + "element": "water", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 125, + "damageTable": { + "fire": 60 + }, + "buffNames": null, + "debuffNames": [ + { + "name": "burn", + "chance": 0.5 + } + ], + "animation": { + "sprite": "steam", + "style": "span", + "position": "enemy", + "scale": 0.5, + "leftOffset": 10 + } + }, + { + "name": "ice spike", + "element": "water", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "expert", + "manaCost": 200, + "damageTable": { + "cold": 100 + }, + "buffNames": null, + "debuffNames": [ + { + "name": "chill", + "chance": 0.75 + }, + { + "name": "chill", + "chance": 0.75 + }, + { + "name": "chill", + "chance": 0.75 + } + ], + "animation": { + "sprite": "iceSpike", + "style": "static", + "position": "enemy" + } + }, + { + "name": "healing rain", + "element": "water", + "proficiencyNeeded": "master", + "targets": "area", + "type": "defense", + "manaCost": 200, + "buffNames": [ + "lasting heal" + ], + "debuffNames": null, + "animation": { + "sprite": "rainCall", + "style": "static", + "position": "self" + } + }, + { + "name": "orb of cold", + "element": "water", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "master", + "manaCost": 300, + "maxTurnsActive": 5, + "damageTable": { + "cold": 50 + }, + "debuffNames": [ + { + "name": "chill", + "chance": 0.75 + } + ], + "animation": { + "sprite": "iceOrb", + "style": "static", + "position": "enemy", + "scale": 0.8, + "repeat": 3 + } + }, + { + "name": "torrent", + "element": "water", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 600, + "damageTable": { + "physical": 400 + }, + "animation": { + "sprite": "torrent", + "style": "static", + "position": "enemy" + } + }, + { + "name": "spike field", + "element": "water", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 700, + "maxTurnsActive": 2, + "damageTable": { + "cold": 150 + }, + "debuffNames": [ + { + "name": "chill", + "chance": 0.75 + }, + { + "name": "chill", + "chance": 0.75 + }, + { + "name": "bleed", + "chance": 0.75 + }, + { + "name": "bleed", + "chance": 0.75 + } + ], + "animation": { + "sprite": "massSpikes", + "style": "static", + "position": "enemy", + "retrigger": false, + "scale": 1.4 + } + }, + { + "name": "air burst", + "element": "air", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "novice", + "manaCost": 15, + "damageTable": { + "physical": 18 + }, + "animation": { + "sprite": "puft", + "style": "static", + "position": "enemy", + "scale": 0.4 + } + }, + { + "name": "sparks", + "element": "air", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "apprentice", + "manaCost": 25, + "damageTable": { + "lightning": 25 + }, + "debuffNames": [ + { + "name": "shocked", + "chance": 0.5 + } + ], + "animation": { + "sprite": "sparks", + "style": "static", + "position": "enemy", + "scale": 0.4 + } + }, + { + "name": "lightning", + "element": "air", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 75, + "damageTable": { + "lightning": 50 + }, + "debuffNames": [ + { + "name": "shocked", + "chance": 0.5 + }, + { + "name": "shocked", + "chance": 0.5 + } + ], + "animation": { + "sprite": "lightning", + "style": "static", + "position": "enemy" + } + }, + { + "name": "thunder clap", + "element": "air", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "expert", + "manaCost": 75, + "damageTable": { + "lightning": 50 + }, + "debuffNames": [ + { + "name": "shocked", + "chance": 0.75 + }, + { + "name": "stun", + "chance": 0.5 + } + ], + "animation": { + "sprite": "thunderClap", + "style": "static", + "position": "enemy", + "scale": 0.8 + } + }, + { + "name": "chain lightning", + "element": "air", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "expert", + "manaCost": 105, + "damageTable": { + "lightning": 70 + }, + "debuffNames": [ + { + "name": "shocked", + "chance": 0.75 + }, + { + "name": "stun", + "chance": 0.5 + } + ], + "animation": { + "sprite": "lightningRay", + "style": "span", + "position": "self", + "repeat": 3 + } + }, + { + "name": "wind blades", + "element": "air", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "master", + "manaCost": 100, + "damageTable": { + "physical": 100 + }, + "debuffNames": [ + { + "name": "bleed", + "chance": 0.75 + }, + { + "name": "bleed", + "chance": 0.75 + } + ], + "animation": { + "sprite": "windBlades", + "style": "static", + "position": "enemy", + "scale": 0.8 + } + }, + { + "name": "surrounding gale", + "element": "air", + "targets": "single", + "type": "defense", + "proficiencyNeeded": "master", + "manaCost": 125, + "buffNames": [ + "hard to see", + "damaging to hit" + ], + "animation": { + "sprite": "groundSlash", + "style": "static", + "position": "self", + "repeat": 2 + } + }, + { + "name": "tornado", + "element": "air", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "maxTurnsActive": 3, + "manaCost": 500, + "damageTable": { + "physical": 100 + }, + "animation": { + "sprite": "tornado", + "style": "static", + "position": "enemy", + "scale": 1.4 + } + }, + { + "name": "suffocate", + "element": "air", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "legend", + "maxTurnsActive": 5, + "manaCost": 1250, + "damageTable": { + "physical": 125 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.75 + } + ], + "animation": { + "sprite": "suffocate", + "style": "static", + "position": "enemy", + "scale": 0.8 + } + }, + { + "name": "rock toss", + "element": "earth", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "novice", + "manaCost": 10, + "damageTable": { + "physical": 5, + "raw": 5 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.10 + } + ], + "animation": { + "sprite": "rockDrop", + "style": "static", + "position": "enemy" + } + }, + { + "name": "stone hands", + "element": "earth", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "apprentice", + "manaCost": 20, + "buffNames": [ + "stone hands" + ], + "debuffNames": null, + "animation": { + "glow": "#77484C20", + "position": "self" + } + }, + { + "name": "stone salvo", + "element": "earth", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "adept", + "maxTurnsActive": 3, + "manaCost": 40, + "damageTable": { + "physical": 5, + "raw": 5 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.25 + } + ], + "animation": { + "sprite": "rocksDropper", + "style": "static", + "position": "enemy" + } + }, + { + "name": "rock armor", + "element": "earth", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "adept", + "manaCost": 60, + "buffNames": [ + "rock armor" + ], + "animation": { + "glow": "#77484C20", + "position": "self" + } + }, + { + "name": "stalactite storm", + "element": "earth", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "expert", + "manaCost": 120, + "damageTable": { + "physical": 90, + "raw": 40 + }, + "animation": { + "sprite": "fallingSpikes", + "style": "static", + "position": "enemy", + "scale": 4 + } + }, + { + "name": "rock wall", + "element": "earth", + "targets": "area", + "type": "defense", + "proficiencyNeeded": "master", + "maxTurnsActive": 5, + "manaCost": 120, + "buffNames": [ + "projectile negation" + ], + "animation": { + "sprite": "rockWall", + "style": "static", + "position": "field" + } + }, + { + "name": "earthquake", + "element": "earth", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 650, + "damageTable": { + "physical": 120, + "raw": 75 + }, + "debuffNames": [ + { + "name": "heavy stun", + "chance": 1.0 + } + ], + "animation": { + "glow": "#77484895", + "position": "field", + "triggersScreenShake": { + "when": "start", + "duration": 1000 + }, + "duration": 1000 + } + }, + { + "name": "collider", + "element": "earth", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 1200, + "damageTable": { + "physical": 330, + "raw": 100 + }, + "debuffNames": [ + { + "name": "heavy stun", + "chance": 1.0 + } + ], + "animation": { + "sprite": "rockCollider", + "style": "static", + "position": "enemy", + "scale": 2 + } + } +] + diff --git a/src/lineage-json/attack-route/necroBooks.json b/src/lineage-json/attack-route/necroBooks.json new file mode 100644 index 0000000..415d101 --- /dev/null +++ b/src/lineage-json/attack-route/necroBooks.json @@ -0,0 +1,220 @@ +[ + { + "name": "book of pull blood", + "type": "spell", + "teaches": "pull blood", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of orb burst (mana)", + "type": "spell", + "teaches": "orb burst(mana)", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of orb burst (health)", + "type": "spell", + "teaches": "orb burst(health)", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of vampiric touch", + "type": "spell", + "teaches": "vampiric touch", + "icon": "Book", + "baseValue": 20000 + }, + { + "name": "book of blood spear", + "type": "spell", + "teaches": "blood spear", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of blood lance", + "type": "spell", + "teaches": "blood lance", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "book of blood storm", + "type": "spell", + "teaches": "blood storm", + "icon": "Book_2", + "baseValue": 200000 + }, + { + "name": "forbidden blood techniques", + "type": "spell", + "teaches": "blood spike", + "icon": "Book_3", + "baseValue": 1000000 + }, + { + "name": "book of the flying skull", + "type": "spell", + "teaches": "summon flying skull", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of skeleton raising", + "type": "spell", + "teaches": "raise skeleton", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of zombie raising", + "type": "spell", + "teaches": "raise zombie", + "icon": "Book", + "baseValue": 15000 + }, + { + "name": "book of luch summoning", + "type": "spell", + "teaches": "summon lich", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of wraith summoning", + "type": "spell", + "teaches": "summon wraith", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "forbidden summoning techniques vol. 1", + "type": "spell", + "teaches": "summon death knight", + "icon": "Book_3", + "baseValue": 500000 + }, + { + "name": "forbidden summoning techniques vol. 2", + "type": "spell", + "teaches": "mass raise dead", + "icon": "Book_3", + "baseValue": 1000000 + }, + { + "name": "book of teeth", + "type": "spell", + "teaches": "teeth", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of bone shield", + "type": "spell", + "teaches": "bone shield", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of bone wall", + "type": "spell", + "teaches": "bone wall", + "icon": "Book", + "baseValue": 20000 + }, + { + "name": "book of bone spear", + "type": "spell", + "teaches": "bone spear", + "icon": "Book", + "baseValue": 50000 + }, + { + "name": "book of bone armor", + "type": "spell", + "teaches": "bone armor", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of bone armor", + "type": "spell", + "teaches": "bone armor", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "book of bone prison", + "type": "spell", + "teaches": "bone prison", + "icon": "Book_2", + "baseValue": 200000 + }, + { + "name": "forbidden bone techniques", + "type": "spell", + "teaches": "bone blade", + "icon": "Book_3", + "baseValue": 1000000 + }, + { + "name": "book of poison dart", + "type": "spell", + "teaches": "poison dart", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of plague cloud", + "type": "spell", + "teaches": "plague cloud", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of poison stream", + "type": "spell", + "teaches": "poison stream", + "icon": "Book", + "baseValue": 20000 + }, + { + "name": "book of disease ward", + "type": "spell", + "teaches": "disease ward", + "icon": "Book", + "baseValue": 20000 + }, + { + "name": "book of miasma", + "type": "spell", + "teaches": "miasma", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of plague bearer", + "type": "spell", + "teaches": "plague bearer", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "book of death cloud", + "type": "spell", + "teaches": "death cloud", + "icon": "Book_2", + "baseValue": 200000 + }, + { + "name": "forbidden pestilence techniques", + "type": "spell", + "teaches": "virulent explosion", + "icon": "Book_3", + "baseValue": 1000000 + } +] + diff --git a/src/lineage-json/attack-route/necroSpells.json b/src/lineage-json/attack-route/necroSpells.json new file mode 100644 index 0000000..ea88744 --- /dev/null +++ b/src/lineage-json/attack-route/necroSpells.json @@ -0,0 +1,580 @@ +[ + { + "name": "pull blood", + "element": "blood", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "novice", + "manaCost": 20, + "damageTable": { + "physical": 5 + }, + "buffNames": [ + "blood orb" + ], + "animation": { + "sprite": "bloodCone", + "style": "static", + "position": "enemy", + "leftOffset": 5, + "scale": 0.3 + } + }, + { + "name": "orb burst(mana)", + "element": "blood", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "apprentice", + "manaCost": -35, + "buffNames": [ + "consume blood orb" + ], + "animation": { + "sprite": "bloodBurst", + "style": "static", + "position": "self" + } + }, + { + "name": "orb burst(health)", + "element": "blood", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "apprentice", + "manaCost": 5, + "selfDamageTable": { + "raw": -30 + }, + "buffNames": [ + "consume blood orb" + ], + "animation": { + "sprite": "bloodBurst", + "style": "static", + "position": "self" + } + }, + { + "name": "vampiric touch", + "element": "blood", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 65, + "damageTable": { + "physical": 25 + }, + "selfDamageTable": { + "raw": -20 + }, + "buffNames": [ + "blood orb" + ], + "animation": { + "glow": "#7f1d1d80", + "position": "enemy" + } + }, + { + "name": "blood spear", + "element": "blood", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "expert", + "damageTable": { + "physical": 90 + }, + "manaCost": 75, + "buffNames": [ + "consume blood orb" + ], + "animation": { + "sprite": "bloodLongBolts", + "style": "span", + "position": "enemy" + } + }, + { + "name": "blood lance", + "element": "blood", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "master", + "manaCost": 150, + "damageTable": { + "physical": 125 + }, + "selfDamageTable": { + "raw": 25 + }, + "buffNames": [ + "consume blood orb", + "consume blood orb" + ], + "debuffNames": [ + { + "name": "hemmorage", + "chance": 0.75 + } + ], + "animation": { + "sprite": "bloodSimpleBolts", + "style": "span", + "position": "enemy" + } + }, + { + "name": "blood storm", + "element": "blood", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 300, + "damageTable": { + "physical": 200 + }, + "selfDamageTable": { + "raw": 50 + }, + "buffNames": [ + "blood orb", + "blood orb" + ], + "debuffNames": [ + { + "name": "hemmorage", + "chance": 1.0 + } + ], + "animation": { + "sprite": "bloodRain", + "style": "static", + "position": "enemy" + } + }, + { + "name": "blood spike", + "element": "blood", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 750, + "damageTable": { + "physical": 350 + }, + "selfDamageTable": { + "raw": 75 + }, + "buffNames": [ + "blood orb", + "blood orb", + "blood orb" + ], + "debuffNames": [ + { + "name": "severe hemmorage", + "chance": 1.0 + } + ], + "animation": { + "sprite": "bloodSpikes", + "style": "static", + "position": "enemy" + } + }, + { + "name": "summon flying skull", + "element": "summoning", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "novice", + "manaCost": 50, + "summonNames": [ + "flying skull" + ] + }, + { + "name": "raise skeleton", + "element": "summoning", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "apprentice", + "manaCost": 50, + "summonNames": [ + "skeleton" + ] + }, + { + "name": "raise zombie", + "element": "summoning", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "adept", + "manaCost": 50, + "summonNames": [ + "skeleton" + ] + }, + { + "name": "summon lich", + "element": "summoning", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "expert", + "manaCost": 150, + "summonNames": [ + "lich" + ] + }, + { + "name": "summon wraith", + "element": "summoning", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "master", + "manaCost": 250, + "summonNames": [ + "wraith" + ] + }, + { + "name": "summon death knight", + "element": "summoning", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "legend", + "manaCost": 500, + "summonNames": [ + "death knight" + ] + }, + { + "name": "mass raise dead", + "element": "summoning", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 750, + "summonNames": [ + "skeleton", + "skeleton", + "skeleton" + ] + }, + { + "name": "teeth", + "element": "bone", + "targets": "dual", + "type": "offense", + "proficiencyNeeded": "novice", + "manaCost": 15, + "damageTable": { + "physical": 7 + }, + "animation": { + "sprite": "teeth", + "style": "span", + "position": "field" + } + }, + { + "name": "bone shield", + "element": "bone", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "apprentice", + "manaCost": 15, + "buffNames": [ + "bone shield" + ], + "animation": { + "sprite": "boneShield", + "style": "static", + "position": "self" + } + }, + { + "name": "bone wall", + "element": "bone", + "targets": "area", + "type": "defense", + "proficiencyNeeded": "adept", + "manaCost": 60, + "buffNames": [ + "projectile suppression" + ], + "animation": { + "sprite": "boneWall", + "style": "static", + "position": "field" + } + }, + { + "name": "bone spear", + "element": "bone", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "expert", + "manaCost": 45, + "damageTable": { + "physical": 40 + }, + "debuffNames": [ + { + "name": "bleed", + "chance": 0.5 + } + ], + "animation": { + "sprite": "boneLance", + "style": "span", + "position": "field" + } + }, + { + "name": "bone armor", + "element": "bone", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "master", + "manaCost": 75, + "buffNames": [ + "guard" + ], + "animation": { + "sprite": "boneShield", + "style": "static", + "position": "self" + } + }, + { + "name": "bone prison", + "element": "bone", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "master", + "manaCost": 125, + "damageTable": { + "physical": 100 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.75 + } + ], + "animation": { + "sprite": "boneOrb", + "style": "static", + "position": "enemy" + } + }, + { + "name": "bone blade", + "element": "bone", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 200, + "damageTable": { + "physical": 300 + }, + "animation": { + "sprite": "boneBlade", + "style": "static", + "position": "enemy" + } + }, + { + "name": "poison dart", + "element": "pestilence", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "novice", + "manaCost": 20, + "damageTable": { + "poison": 5 + }, + "debuffNames": [ + { + "name": "poison", + "chance": 0.75 + } + ], + "animation": { + "sprite": "poisonDart", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 10, + "scale": 0.3 + } + }, + { + "name": "plague cloud", + "element": "pestilence", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "apprentice", + "manaCost": 35, + "damageTable": { + "poison": 15 + }, + "debuffNames": [ + { + "name": "diseased", + "chance": 0.5 + } + ], + "animation": { + "sprite": "poisonPuft", + "style": "static", + "position": "enemy" + } + }, + { + "name": "poison stream", + "element": "pestilence", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 60, + "damageTable": { + "poison": 5 + }, + "debuffNames": [ + { + "name": "poison", + "chance": 0.75 + }, + { + "name": "poison", + "chance": 0.75 + } + ], + "animation": { + "sprite": "poisonStream", + "style": "span", + "position": "field" + } + }, + { + "name": "disease ward", + "element": "pestilence", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "adept", + "manaCost": 50, + "buffNames": [ + "disease immunity" + ], + "animation": { + "sprite": "poisonShield", + "style": "span", + "position": "field" + } + }, + { + "name": "miasma", + "element": "pestilence", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "expert", + "manaCost": 100, + "damageTable": { + "poison": 45 + }, + "debuffNames": [ + { + "name": "poison", + "chance": 1.0 + }, + { + "name": "diseased", + "chance": 0.75 + } + ], + "animation": { + "sprite": "poisonOrbBurst", + "style": "static", + "position": "enemy" + } + }, + { + "name": "plague bearer", + "element": "pestilence", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "master", + "manaCost": 175, + "damageTable": { + "poison": 75 + }, + "buffNames": [ + "siphon poison" + ], + "debuffNames": [ + { + "name": "diseased", + "chance": 1.0 + } + ], + "animation": { + "glow": "#53DF2E", + "position": "field" + } + }, + { + "name": "death cloud", + "element": "pestilence", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 300, + "damageTable": { + "poison": 150 + }, + "debuffNames": [ + { + "name": "poison", + "chance": 1.0 + }, + { + "name": "diseased", + "chance": 1.0 + } + ], + "animation": { + "sprite": "poisonSmallBurst", + "style": "static", + "position": "enemy" + } + }, + { + "name": "virulent explosion", + "element": "pestilence", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 500, + "damageTable": { + "poison": 250 + }, + "debuffNames": [ + { + "name": "severe poison", + "chance": 1.0 + }, + { + "name": "severe disease", + "chance": 1.0 + } + ], + "animation": { + "sprite": "poisonLargeBurst", + "style": "static", + "position": "enemy" + } + } +] + diff --git a/src/lineage-json/attack-route/paladinBooks.json b/src/lineage-json/attack-route/paladinBooks.json new file mode 100644 index 0000000..ec9f545 --- /dev/null +++ b/src/lineage-json/attack-route/paladinBooks.json @@ -0,0 +1,262 @@ +[ + { + "name": "book of flash heal", + "type": "spell", + "teaches": "flash heal", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of minor blessing", + "type": "spell", + "teaches": "minor blessing", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of holy light", + "type": "spell", + "teaches": "holy light", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of rejuvenating light", + "type": "spell", + "teaches": "rejuvenating light", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of turn undead", + "type": "spell", + "teaches": "turn undead", + "icon": "Book", + "baseValue": 20000 + }, + { + "name": "book of cleansing light", + "type": "spell", + "teaches": "cleansing light", + "icon": "Book", + "baseValue": 10000 + }, + { + "name": "book of delayed heal", + "type": "spell", + "teaches": "delayed heal", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of moderate heal", + "type": "spell", + "teaches": "moderate heal", + "icon": "Book_2", + "baseValue": 75000 + }, + { + "name": "book of holy nova", + "type": "spell", + "teaches": "holy nova", + "icon": "Book_2", + "baseValue": 100000 + }, + { + "name": "book of revoke undead", + "type": "spell", + "teaches": "revoke undead", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "book of great rejuvenating light", + "type": "spell", + "teaches": "great rejuvenating light", + "icon": "Book_2", + "baseValue": 200000 + }, + { + "name": "sacred healing techniques vol. 1", + "type": "spell", + "teaches": "overwhelming glow", + "icon": "Book_3", + "baseValue": 500000 + }, + { + "name": "sacred healing techniques vol. 2", + "type": "spell", + "teaches": "unending cure", + "icon": "Book_3", + "baseValue": 1000000 + }, + { + "name": "book of judgment", + "type": "spell", + "teaches": "judgment", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of righteous fury", + "type": "spell", + "teaches": "righteous fury", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of holy bolt", + "type": "spell", + "teaches": "holy bolt", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of holy strike", + "type": "spell", + "teaches": "holy strike", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of righteous blow", + "type": "spell", + "teaches": "righteous blow", + "icon": "Book", + "baseValue": 10000 + }, + { + "name": "book of consecrated blade", + "type": "spell", + "teaches": "consecrated blade", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of holy wrath", + "type": "spell", + "teaches": "holy wrath", + "icon": "Book_2", + "baseValue": 75000 + }, + { + "name": "book of smite", + "type": "spell", + "teaches": "smite", + "icon": "Book_2", + "baseValue": 100000 + }, + { + "name": "book of holy nova", + "type": "spell", + "teaches": "holy nova", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "book of righteous condemnation", + "type": "spell", + "teaches": "righteous condemnation", + "icon": "Book_2", + "baseValue": 200000 + }, + { + "name": "sacred vengeance techniques vol. 1", + "type": "spell", + "teaches": "divine judgment", + "icon": "Book_3", + "baseValue": 500000 + }, + { + "name": "sacred vengeance techniques vol. 2", + "type": "spell", + "teaches": "holy cataclysm", + "icon": "Book_3", + "baseValue": 1000000 + }, + { + "name": "book of blessed guard", + "type": "spell", + "teaches": "blessed guard", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of protection aura", + "type": "spell", + "teaches": "protection aura", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of divine fortitude", + "type": "spell", + "teaches": "divine fortitude", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of reflective bulwark", + "type": "spell", + "teaches": "reflective bulwark", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of readied guard", + "type": "spell", + "teaches": "readied guard", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of holy ward", + "type": "spell", + "teaches": "holy ward", + "icon": "Book", + "baseValue": 10000 + }, + { + "name": "book of blessed shield", + "type": "spell", + "teaches": "blessed shield", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of divine resilience", + "type": "spell", + "teaches": "divine resilience", + "icon": "Book_2", + "baseValue": 75000 + }, + { + "name": "book of divine intervention", + "type": "spell", + "teaches": "divine intervention", + "icon": "Book_2", + "baseValue": 100000 + }, + { + "name": "book of holy sanctuary", + "type": "spell", + "teaches": "holy sanctuary", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "book of holy barrier", + "type": "spell", + "teaches": "holy barrier", + "icon": "Book_2", + "baseValue": 200000 + }, + { + "name": "sacred protection techniques", + "type": "spell", + "teaches": "aegis of light", + "icon": "Book_3", + "baseValue": 1000000 + } +] + diff --git a/src/lineage-json/attack-route/paladinSpells.json b/src/lineage-json/attack-route/paladinSpells.json new file mode 100644 index 0000000..634b138 --- /dev/null +++ b/src/lineage-json/attack-route/paladinSpells.json @@ -0,0 +1,708 @@ +[ + { + "name": "flash heal", + "element": "holy", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "novice", + "manaCost": 25, + "selfDamageTable": { + "raw": -35 + }, + "animation": { + "sprite": "goldenHeal", + "style": "static", + "position": "self" + } + }, + { + "name": "minor blessing", + "element": "holy", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "novice", + "manaCost": 15, + "buffNames": [ + "minor fortitude" + ], + "animation": { + "sprite": "holyShield", + "style": "static", + "position": "self" + } + }, + { + "name": "holy light", + "element": "holy", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "novice", + "manaCost": 20, + "damageTable": { + "holy": 15 + }, + "debuffNames": [ + { + "name": "blind", + "chance": 0.25 + } + ], + "animation": { + "glow": "#FBD44F", + "position": "field" + } + }, + { + "name": "rejuvenating light", + "element": "holy", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "apprentice", + "manaCost": 20, + "buffNames": [ + "quickened mind" + ], + "animation": { + "glow": "#FBD44F", + "position": "field" + } + }, + { + "name": "turn undead", + "element": "holy", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "apprentice", + "manaCost": 50, + "damageTable": { + "holy": 20 + }, + "debuffNames": [ + { + "name": "undead cower", + "chance": 1.0 + } + ], + "animation": { + "sprite": "corruptSword", + "style": "static", + "position": "enemy" + } + }, + { + "name": "cleansing light", + "element": "holy", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "apprentice", + "manaCost": 40, + "selfDamageTable": { + "raw": -25 + }, + "buffNames": [ + "purify" + ], + "animation": { + "glow": "#FBD44F", + "position": "field" + } + }, + { + "name": "delayed heal", + "element": "holy", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "adept", + "manaCost": 75, + "buffNames": [ + "delayed heal" + ], + "animation": { + "sprite": "goldenHeal", + "style": "static", + "position": "self" + } + }, + { + "name": "moderate heal", + "element": "holy", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "adept", + "manaCost": 75, + "selfDamageTable": { + "raw": -85 + }, + "animation": { + "sprite": "goldenHeal", + "style": "static", + "position": "self" + } + }, + { + "name": "holy nova", + "element": "holy", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 85, + "damageTable": { + "holy": 65 + }, + "selfDamageTable": { + "raw": -30 + }, + "debuffNames": [ + { + "name": "blind", + "chance": 0.5 + } + ], + "animation": { + "sprite": "holyOrb", + "style": "static", + "position": "enemy" + } + }, + { + "name": "revoke undead", + "element": "holy", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "expert", + "manaCost": 100, + "damageTable": { + "holy": 75 + }, + "debuffNames": [ + { + "name": "revoke undead", + "chance": 1.0 + } + ], + "animation": { + "sprite": "corruptSword", + "style": "static", + "position": "enemy" + } + }, + { + "name": "great rejuvenating light", + "element": "holy", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "expert", + "manaCost": 200, + "buffNames": [ + "greater quickened mind" + ], + "animation": { + "glow": "#FBD44F", + "position": "field" + } + }, + { + "name": "overwhelming glow", + "element": "holy", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "master", + "manaCost": 200, + "selfDamageTable": { + "raw": -120 + }, + "buffNames": [ + "overwhelming glow" + ], + "animation": { + "sprite": "holyBeam", + "style": "static", + "position": "enemy" + } + }, + { + "name": "unending cure", + "element": "holy", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "legend", + "manaCost": 250, + "duration": 3, + "selfDamageTable": { + "raw": -100 + }, + "buffNames": [ + "overwhelming glow" + ], + "animation": { + "sprite": "goldenHeal", + "style": "static", + "position": "self" + } + }, + { + "name": "judgment", + "element": "vengeance", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "proficiencyNeeded": "novice", + "manaCost": 25, + "damageTable": { + "holy": 5 + }, + "animation": { + "sprite": "holyArc", + "style": "static", + "position": "enemy", + "scale": 0.5 + } + }, + { + "name": "righteous fury", + "element": "vengeance", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "novice", + "manaCost": 15, + "buffNames": [ + "minor fury" + ], + "animation": { + "sprite": "crossedSwords", + "style": "static", + "position": "self" + } + }, + { + "name": "holy bolt", + "element": "vengeance", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "novice", + "manaCost": 20, + "damageTable": { + "holy": 20 + }, + "animation": { + "sprite": "holyDart", + "style": "missile", + "position": "enemy", + "scale": 0.3, + "reachTargetAtFrame": 10 + } + }, + { + "name": "holy strike", + "element": "vengeance", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "proficiencyNeeded": "apprentice", + "manaCost": 35, + "damageTable": { + "holy": 35 + }, + "debuffNames": [ + { + "name": "sunder", + "chance": 0.25 + } + ], + "animation": { + "sprite": "glowingBlade", + "style": "static", + "position": "enemy" + } + }, + { + "name": "righteous blow", + "element": "vengeance", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "proficiencyNeeded": "apprentice", + "manaCost": 40, + "damageTable": { + "holy": 45 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.25 + } + ], + "animation": { + "sprite": "holyFist", + "style": "static", + "position": "enemy" + } + }, + { + "name": "consecrated blade", + "element": "vengeance", + "targets": "self", + "type": "defense", + "usesWeapon": "melee", + "proficiencyNeeded": "adept", + "manaCost": 50, + "damageTable": { + "holy": 45 + }, + "buffNames": [ + "blessed defense" + ], + "animation": { + "sprite": "glowingBlade", + "style": "static", + "position": "self" + } + }, + { + "name": "holy wrath", + "element": "vengeance", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 65, + "damageTable": { + "holy": 55 + }, + "debuffNames": [ + { + "name": "weakened", + "chance": 0.5 + } + ], + "animation": { + "sprite": "holyShred", + "style": "static", + "position": "enemy" + } + }, + { + "name": "smite", + "element": "vengeance", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "proficiencyNeeded": "expert", + "manaCost": 75, + "damageTable": { + "holy": 85 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.5 + } + ], + "animation": { + "sprite": "holySword", + "style": "static", + "position": "enemy" + } + }, + { + "name": "holy nova", + "element": "vengeance", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "master", + "manaCost": 150, + "damageTable": { + "holy": 125 + }, + "buffNames": [ + "protection aura" + ], + "animation": { + "sprite": "holyTrails", + "style": "static", + "position": "field" + } + }, + { + "name": "righteous condemnation", + "element": "vengeance", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "proficiencyNeeded": "master", + "manaCost": 175, + "damageTable": { + "holy": 150 + }, + "debuffNames": [ + { + "name": "sunder", + "chance": 0.75 + }, + { + "name": "weakened", + "chance": 0.75 + } + ], + "animation": { + "sprite": "glowingBlade", + "style": "static", + "position": "enemy" + } + }, + { + "name": "divine judgment", + "element": "vengeance", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "proficiencyNeeded": "legend", + "manaCost": 200, + "damageTable": { + "holy": 250 + }, + "debuffNames": [ + { + "name": "execute", + "chance": 0.2 + } + ], + "animation": { + "sprite": "corruptSword", + "style": "static", + "position": "field" + } + }, + { + "name": "holy cataclysm", + "element": "vengeance", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 300, + "damageTable": { + "holy": 200 + }, + "buffNames": [ + "righteous fury" + ], + "debuffNames": [ + { + "name": "severe sunder", + "chance": 1.0 + } + ], + "animation": { + "sprite": "holySword", + "style": "static", + "position": "field" + } + }, + { + "name": "blessed guard", + "element": "protection", + "targets": "self", + "type": "defense", + "usesWeapon": "shield", + "proficiencyNeeded": "novice", + "manaCost": 25, + "buffNames": [ + "guard" + ], + "animation": { + "sprite": "holyShield", + "style": "static", + "position": "self" + } + }, + { + "name": "protection aura", + "element": "protection", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "novice", + "manaCost": 25, + "buffNames": [ + "protection aura" + ], + "animation": { + "glow": "#FBD44F", + "position": "field" + } + }, + { + "name": "divine fortitude", + "element": "protection", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "novice", + "manaCost": 20, + "buffNames": [ + "minor fortitude" + ], + "animation": { + "sprite": "holyFist", + "style": "static", + "position": "field" + } + }, + { + "name": "reflective bulwark", + "element": "protection", + "targets": "self", + "type": "defense", + "usesWeapon": "shield", + "proficiencyNeeded": "apprentice", + "manaCost": 50, + "buffNames": [ + "reflective bulwark" + ], + "animation": { + "sprite": "holyShield", + "style": "static", + "position": "self" + } + }, + { + "name": "readied guard", + "element": "protection", + "targets": "self", + "type": "defense", + "usesWeapon": "shield", + "proficiencyNeeded": "apprentice", + "manaCost": 10, + "buffNames": [ + "empowered guarding" + ], + "animation": { + "sprite": "holyShield", + "style": "static", + "position": "self" + } + }, + { + "name": "holy ward", + "element": "protection", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "apprentice", + "manaCost": 45, + "buffNames": [ + "spell resistance" + ], + "animation": { + "sprite": "holyShield", + "style": "static", + "position": "self" + } + }, + { + "name": "blessed shield", + "element": "protection", + "targets": "self", + "type": "defense", + "usesWeapon": "shield", + "proficiencyNeeded": "adept", + "manaCost": 65, + "buffNames": [ + "blessed defense" + ], + "animation": { + "sprite": "holyShield", + "style": "static", + "position": "self" + } + }, + { + "name": "divine resilience", + "element": "protection", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "adept", + "manaCost": 70, + "buffNames": [ + "fortitude", + "spell resistance" + ], + "animation": { + "sprite": "holyShield", + "style": "static", + "position": "self" + } + }, + { + "name": "divine intervention", + "element": "protection", + "targets": "self", + "type": "defense", + "usesWeapon": "shield", + "proficiencyNeeded": "expert", + "manaCost": 100, + "buffNames": [ + "guard", + "blessed defense" + ], + "animation": { + "sprite": "holyShield", + "style": "static", + "position": "self" + } + }, + { + "name": "holy sanctuary", + "element": "protection", + "targets": "area", + "type": "defense", + "proficiencyNeeded": "expert", + "manaCost": 125, + "selfDamageTable": { + "raw": -40 + }, + "buffNames": [ + "protection aura", + "minor fortitude" + ], + "animation": { + "sprite": "holyShield", + "style": "static", + "position": "self" + } + }, + { + "name": "holy barrier", + "element": "protection", + "targets": "self", + "type": "defense", + "usesWeapon": "shield", + "proficiencyNeeded": "master", + "manaCost": 150, + "buffNames": [ + "protection aura", + "reflective bulwark" + ], + "animation": { + "sprite": "holyShield", + "style": "static", + "position": "self" + } + }, + { + "name": "aegis of light", + "element": "protection", + "targets": "self", + "type": "defense", + "usesWeapon": "shield", + "proficiencyNeeded": "legend", + "manaCost": 250, + "buffNames": [ + "protection aura", + "blessed defense", + "reflective bulwark" + ], + "animation": { + "sprite": "holyShield", + "style": "static", + "position": "self" + } + } +] + diff --git a/src/lineage-json/attack-route/playerAttacks.json b/src/lineage-json/attack-route/playerAttacks.json new file mode 100644 index 0000000..fc35cc6 --- /dev/null +++ b/src/lineage-json/attack-route/playerAttacks.json @@ -0,0 +1,265 @@ +[ + { + "name": "punch", + "targets": "single", + "baseHitChance": 1.0, + "damageTable": {}, + "animation": { + "sprite": "puft", + "style": "static", + "position": "enemy", + "scale": 0.3 + } + }, + { + "name": "hit", + "targets": "single", + "baseHitChance": 1.0, + "damageTable": {}, + "animation": { + "sprite": "puft", + "style": "static", + "position": "enemy", + "scale": 0.3 + } + }, + { + "name": "stab", + "targets": "single", + "baseHitChance": 0.95, + "damageTable": {}, + "debuffNames": [ + { + "name": "bleed", + "chance": 0.2 + } + ], + "animation": { + "sprite": "smallCross", + "style": "static", + "position": "enemy" + } + }, + { + "name": "spark", + "targets": "single", + "baseHitChance": 0.85, + "damageTable": { + "lightning": 2 + }, + "debuffNames": [ + { + "name": "shocked", + "chance": 0.15 + } + ] + }, + { + "name": "torch stab", + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "fire": 2 + }, + "debuffNames": [ + { + "name": "burn", + "chance": 0.85 + } + ], + "animation": { + "sprite": "puft", + "style": "static", + "position": "enemy" + } + }, + { + "name": "slash", + "targets": "single", + "baseHitChance": 1.0, + "damageTable": {}, + "animation": { + "sprite": "slashHorizontal", + "style": "static", + "position": "enemy" + } + }, + { + "name": "cleave", + "targets": "dual", + "baseHitChance": 0.85, + "damageTable": {}, + "animation": { + "sprite": "slashHorizontal", + "style": "static", + "position": "enemy" + } + }, + { + "name": "crushing blow", + "targets": "single", + "baseHitChance": 0.90, + "damageTable": {}, + "debuffNames": [ + { + "name": "stun", + "chance": 0.25 + } + ], + "animation": { + "sprite": "crowning", + "style": "static", + "position": "enemy" + } + }, + { + "name": "", + "targets": "single", + "baseHitChance": 0.95, + "damageTable": {}, + "debuffNames": [ + { + "name": "execute", + "chance": 0.05 + } + ] + }, + { + "name": "cast", + "targets": "single", + "baseHitChance": 1.0, + "damageTable": {}, + "animation": { + "sprite": "puft", + "style": "static", + "position": "enemy", + "scale": 0.3 + } + }, + { + "name": "bonk", + "targets": "single", + "baseHitChance": 0.95, + "damageTable": {}, + "debuffNames": [ + { + "name": "stun", + "chance": 0.05 + } + ], + "animation": { + "sprite": "crowning", + "style": "static", + "position": "enemy" + } + }, + { + "name": "shoot", + "targets": "single", + "baseHitChance": 0.95, + "damageTable": {}, + "animation": { + "sprite": "chainedArrowHit", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 7, + "scale": 0.3 + } + }, + { + "name": "rooting shot", + "targets": "single", + "baseHitChance": 0.90, + "damageTable": {}, + "debuffNames": [ + { + "name": "stun", + "chance": 0.5 + } + ], + "animation": { + "sprite": "chainedArrowRooting", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 7, + "scale": 0.3 + } + }, + { + "name": "hack", + "targets": "single", + "baseHitChance": 0.85, + "damageTable": {}, + "debuffNames": [ + { + "name": "execute", + "chance": 0.1 + } + ], + "animation": { + "sprite": "crowning", + "style": "static", + "position": "enemy" + } + }, + { + "name": "rapid shot", + "targets": "single", + "baseHitChance": 0.55, + "damageTable": {}, + "hits": 3, + "animation": { + "sprite": "chainedArrowHit", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 7 + } + }, + { + "name": "careful shot", + "targets": "single", + "baseHitChance": 1.0, + "damageTable": {}, + "animation": { + "sprite": "chainedArrowHit", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 7 + } + }, + { + "name": "poison shot", + "targets": "single", + "baseHitChance": 0.9, + "damageTable": { + "poison": 3 + }, + "debuffNames": [ + { + "name": "poison", + "chance": 0.65 + } + ], + "animation": { + "sprite": "chainedPoisonArrowHit", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 7 + } + }, + { + "name": "overdraw", + "targets": "single", + "baseHitChance": 0.80, + "damageTable": { + "physical": 4 + }, + "animation": { + "sprite": "chainedArrowHit", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 7 + } + } +] + diff --git a/src/lineage-json/attack-route/rangerBooks.json b/src/lineage-json/attack-route/rangerBooks.json new file mode 100644 index 0000000..0b37f48 --- /dev/null +++ b/src/lineage-json/attack-route/rangerBooks.json @@ -0,0 +1,241 @@ +[ + { + "name": "book of throw shuriken", + "type": "spell", + "teaches": "throw shuriken", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of poison blade", + "type": "spell", + "teaches": "poison blade", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of quick strike", + "type": "spell", + "teaches": "quick strike", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of below the belt", + "type": "spell", + "teaches": "below the belt", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of dual slice", + "type": "spell", + "teaches": "dual slice", + "icon": "Book", + "baseValue": 10000 + }, + { + "name": "book of shadow cover", + "type": "spell", + "teaches": "shadow cover", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of backstab", + "type": "spell", + "teaches": "backstab", + "icon": "Book_2", + "baseValue": 75000 + }, + { + "name": "book of venomous strike", + "type": "spell", + "teaches": "venomous strike", + "icon": "Book_2", + "baseValue": 75000 + }, + { + "name": "book of dance of daggers", + "type": "spell", + "teaches": "dance of daggers", + "icon": "Book_2", + "baseValue": 100000 + }, + { + "name": "book of garrote", + "type": "spell", + "teaches": "garrote", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "book of shadow step", + "type": "spell", + "teaches": "shadow step", + "icon": "Book_3", + "baseValue": 500000 + }, + { + "name": "forbidden assassination techniques vol. 1", + "type": "spell", + "teaches": "blade fan", + "icon": "Book_3", + "baseValue": 500000 + }, + { + "name": "forbidden assassination techniques vol. 2", + "type": "spell", + "teaches": "finalé", + "icon": "Book_3", + "baseValue": 1000000 + }, + { + "name": "book of the raven", + "type": "spell", + "teaches": "call raven", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of the wolf", + "type": "spell", + "teaches": "call wolf", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of animal instinct", + "type": "spell", + "teaches": "animal instinct", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of rat trap", + "type": "spell", + "teaches": "rat trap", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of the bear", + "type": "spell", + "teaches": "call bear", + "icon": "Book", + "baseValue": 10000 + }, + { + "name": "book of beast bond", + "type": "spell", + "teaches": "beast bond", + "icon": "Book", + "baseValue": 10000 + }, + { + "name": "book of bear trap", + "type": "spell", + "teaches": "bear trap", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of the dragon welp", + "type": "spell", + "teaches": "call dragon welp", + "icon": "Book_2", + "baseValue": 75000 + }, + { + "name": "book of beast fury", + "type": "spell", + "teaches": "beastial fury", + "icon": "Book_2", + "baseValue": 75000 + }, + { + "name": "book of dragon trap", + "type": "spell", + "teaches": "dragon trap", + "icon": "Book_2", + "baseValue": 150000 + }, + { + "name": "book of the griffon", + "type": "spell", + "teaches": "call griffon", + "icon": "Book_3", + "baseValue": 200000 + }, + { + "name": "book of entangle", + "type": "spell", + "teaches": "entangle", + "icon": "Book_3", + "baseValue": 300000 + }, + { + "name": "forbidden beast mastery techniques vol. 1", + "type": "spell", + "teaches": "call dragon", + "icon": "Book_3", + "baseValue": 500000 + }, + { + "name": "book of arcane shot", + "type": "spell", + "teaches": "arcane shot", + "icon": "Book", + "baseValue": 2500 + }, + { + "name": "book of enchanted quiver", + "type": "spell", + "teaches": "enchanted quiver", + "icon": "Book", + "baseValue": 5000 + }, + { + "name": "book of arcane arrow", + "type": "spell", + "teaches": "arcane arrow", + "icon": "Book", + "baseValue": 10000 + }, + { + "name": "book of seeking arrow", + "type": "spell", + "teaches": "seeking arrow", + "icon": "Book_2", + "baseValue": 50000 + }, + { + "name": "book of arcane missiles", + "type": "spell", + "teaches": "arcane missiles", + "icon": "Book_3", + "baseValue": 200000 + }, + { + "name": "book of moon call", + "type": "spell", + "teaches": "moon call", + "icon": "Book_3", + "baseValue": 200000 + }, + { + "name": "forbidden arcane techniques vol. 1", + "type": "spell", + "teaches": "torrent", + "icon": "Book_3", + "baseValue": 500000 + }, + { + "name": "forbidden arcane techniques vol. 2", + "type": "spell", + "teaches": "moon fire", + "icon": "Book_3", + "baseValue": 1000000 + } +] + diff --git a/src/lineage-json/attack-route/rangerSpells.json b/src/lineage-json/attack-route/rangerSpells.json new file mode 100644 index 0000000..5a241af --- /dev/null +++ b/src/lineage-json/attack-route/rangerSpells.json @@ -0,0 +1,590 @@ +[ + { + "name": "throw shuriken", + "element": "assassination", + "proficiencyNeeded": "novice", + "targets": "single", + "type": "offense", + "manaCost": 10, + "damageTable": { + "physical": 15 + }, + "debuffNames": [ + { + "name": "bleed", + "chance": 0.25 + } + ], + "animation": { + "sprite": "shuriken", + "style": "missile", + "position": "enemy", + "scale": 0.3 + } + }, + { + "name": "poison blade", + "element": "assassination", + "proficiencyNeeded": "novice", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "manaCost": 15, + "damageTable": { + "physical": 5, + "poison": 5 + }, + "debuffNames": [ + { + "name": "poison", + "chance": 0.5 + } + ], + "animation": { + "sprite": "poisonDart", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 10, + "scale": 0.3 + } + }, + { + "name": "quick strike", + "element": "assassination", + "proficiencyNeeded": "novice", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "manaCost": 10, + "damageTable": { + "physical": 2 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.75 + } + ], + "animation": { + "sprite": "slashHorizontal", + "style": "static", + "position": "enemy" + } + }, + { + "name": "below the belt", + "element": "assassination", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "apprentice", + "manaCost": 20, + "damageTable": { + "physical": 15 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.5 + } + ], + "animation": { + "sprite": "puft", + "style": "static", + "position": "enemy" + } + }, + { + "name": "dual slice", + "element": "assassination", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "proficiencyNeeded": "apprentice", + "manaCost": 25, + "damageTable": { + "physical": 30 + }, + "debuffNames": [ + { + "name": "bleed", + "chance": 0.5 + } + ], + "animation": { + "sprite": "smallCross", + "style": "static", + "position": "enemy" + } + }, + { + "name": "shadow cover", + "element": "assassination", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "adept", + "manaCost": 50, + "buffNames": [ + "stealth" + ], + "animation": { + "sprite": "smoke", + "style": "static", + "position": "self" + } + }, + { + "name": "backstab", + "element": "assassination", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "proficiencyNeeded": "adept", + "manaCost": 75, + "damageTable": { + "physical": 85, + "raw": 10 + }, + "buffNames": [ + "remove stealth" + ], + "animation": { + "sprite": "slashHit", + "style": "static", + "position": "self" + } + }, + { + "name": "venomous strike", + "element": "assassination", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "proficiencyNeeded": "adept", + "manaCost": 60, + "damageTable": { + "physical": 5, + "poison": 35 + }, + "debuffNames": [ + { + "name": "severe poison", + "chance": 0.75 + } + ], + "animation": { + "sprite": "poisonSmallCross", + "style": "static", + "position": "enemy" + } + }, + { + "name": "dance of daggers", + "element": "assassination", + "targets": "self", + "type": "defense", + "usesWeapon": "melee", + "proficiencyNeeded": "expert", + "manaCost": 100, + "buffNames": [ + "dance of daggers" + ], + "animation": { + "sprite": "desaturatedCrossSwords", + "style": "static", + "position": "self" + } + }, + { + "name": "garrote", + "element": "assassination", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "proficiencyNeeded": "expert", + "manaCost": 125, + "damage": { + "physical": 75 + }, + "debuffNames": [ + { + "name": "silence", + "chance": 0.75 + }, + { + "name": "severe bleed", + "chance": 0.5 + } + ], + "animation": { + "sprite": "crossAndBleed", + "style": "static", + "position": "enemy" + } + }, + { + "name": "shadow step", + "element": "assassination", + "targets": "single", + "type": "offense", + "proficiencyNeeded": "master", + "manaCost": 250, + "damageTable": { + "physical": 100 + }, + "buffNames": [ + "stealth" + ], + "animation": { + "sprite": "slashAndDust", + "style": "static", + "position": "enemy" + } + }, + { + "name": "blade fan", + "element": "assassination", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 400, + "damageTable": { + "physical": 275 + }, + "debuffNames": [ + { + "name": "bleed", + "chance": 1.0 + } + ], + "animation": { + "sprite": "teeth", + "style": "static", + "position": "enemy" + } + }, + { + "name": "finalé", + "element": "assassination", + "targets": "single", + "type": "offense", + "usesWeapon": "melee", + "proficiencyNeeded": "legend", + "manaCost": 500, + "damageTable": { + "physical": 250, + "raw": 250 + }, + "buffNames": [ + "remove stealth" + ], + "animation": { + "sprite": "largeCrossAndBleed", + "style": "static", + "position": "enemy" + } + }, + { + "name": "call raven", + "element": "beastMastery", + "targets": "self", + "type": "offense", + "proficiencyNeeded": "novice", + "manaCost": 10, + "rangerPetName": "raven" + }, + { + "name": "call wolf", + "element": "beastMastery", + "targets": "self", + "type": "offense", + "proficiencyNeeded": "novice", + "manaCost": 15, + "rangerPetName": "wolf" + }, + { + "name": "animal instinct", + "element": "beastMastery", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "novice", + "manaCost": 20, + "effects": { + "damage": null, + "buffNames": [ + "minor fortitude" + ], + "debuffNames": null + } + }, + { + "name": "rat trap", + "element": "beastMastery", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "apprentice", + "manaCost": 20, + "buffNames": [ + "rat trap" + ] + }, + { + "name": "beast bond", + "element": "beastMastery", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "apprentice", + "manaCost": 35, + "buffNames": [ + "beast empowerment" + ] + }, + { + "name": "call bear", + "element": "beastMastery", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "apprentice", + "manaCost": 50, + "rangerPetName": "bear" + }, + { + "name": "bear trap", + "element": "beastMastery", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 100, + "buffNames": [ + "bear trap" + ] + }, + { + "name": "call dragon welp", + "element": "beastMastery", + "targets": "self", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 125, + "rangerPetName": "dragon welp" + }, + { + "name": "beastial fury", + "element": "beastMastery", + "targets": "self", + "type": "offense", + "proficiencyNeeded": "adept", + "manaCost": 75, + "buffNames": [ + "dual rage" + ] + }, + { + "name": "dragon trap", + "element": "beastMastery", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "expert", + "manaCost": 200, + "buffNames": [ + "dragon trap" + ] + }, + { + "name": "call griffon", + "element": "beastMastery", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "master", + "manaCost": 200, + "rangerPetName": "griffon" + }, + { + "name": "entangle", + "element": "beastMastery", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "master", + "manaCost": 300, + "duration": 5, + "damageTable": { + "physical": 75 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.75 + } + ] + }, + { + "name": "call dragon", + "element": "beastMastery", + "targets": "self", + "type": "offense", + "proficiencyNeeded": "legend", + "manaCost": 600, + "rangerPetName": "dragon" + }, + { + "name": "arcane shot", + "element": "arcane", + "targets": "single", + "type": "offense", + "usesWeapon": "bow", + "proficiencyNeeded": "novice", + "manaCost": 20, + "damageTable": { + "physical": 5, + "magic": 5 + }, + "animation": { + "sprite": "arcaneArrow", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 7, + "scale": 0.3 + } + }, + { + "name": "enchanted quiver", + "element": "arcane", + "targets": "self", + "type": "offense", + "usesWeapon": "bow", + "proficiencyNeeded": "apprentice", + "manaCost": 25, + "buffNames": [ + "enchanted quiver" + ], + "animation": { + "sprite": "risingBlue", + "style": "static", + "position": "self" + } + }, + { + "name": "arcane arrow", + "element": "arcane", + "targets": "single", + "type": "offense", + "usesWeapon": "bow", + "proficiencyNeeded": "apprentice", + "manaCost": 35, + "damageTable": { + "physical": 15, + "magic": 25 + }, + "animation": { + "sprite": "arcaneArrow", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 7, + "scale": 0.3 + } + }, + { + "name": "seeking arrow", + "element": "arcane", + "targets": "single", + "type": "offense", + "usesWeapon": "bow", + "proficiencyNeeded": "adept", + "manaCost": 65, + "damageTable": { + "physical": 35, + "magic": 15 + }, + "animation": { + "sprite": "arcaneArrow", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 7, + "scale": 0.3 + } + }, + { + "name": "arcane missiles", + "element": "arcane", + "targets": "single", + "type": "offense", + "usesWeapon": "bow", + "proficiencyNeeded": "master", + "manaCost": 100, + "damageTable": { + "magic": 90 + }, + "debuffNames": [ + { + "name": "burn", + "chance": 0.4 + } + ], + "animation": { + "sprite": "risingBlue", + "style": "static", + "position": "enemy" + } + }, + { + "name": "moon call", + "element": "arcane", + "targets": "self", + "type": "defense", + "proficiencyNeeded": "master", + "manaCost": 125, + "buffNames": [ + "guard", + "quickened mind" + ], + "animation": { + "sprite": "moonCall", + "style": "static", + "position": "self" + } + }, + { + "name": "torrent", + "element": "arcane", + "targets": "area", + "type": "offense", + "usesWeapon": "bow", + "proficiencyNeeded": "legend", + "duration": 3, + "manaCost": 500, + "damageTable": { + "magic": 100 + }, + "animation": { + "sprite": "arrowTorrent", + "style": "missile", + "position": "enemy", + "reachTargetAtFrame": 7, + "topOffset": 30 + } + }, + { + "name": "moon fire", + "element": "arcane", + "targets": "area", + "type": "offense", + "proficiencyNeeded": "legend", + "duration": 5, + "manaCost": 1250, + "damageTable": { + "fire": 25, + "magic": 75 + }, + "debuffNames": [ + { + "name": "burn", + "chance": 0.5 + } + ], + "animation": { + "sprite": "blueBeam", + "style": "static", + "position": "enemy" + } + } +] + diff --git a/src/lineage-json/attack-route/summons.json b/src/lineage-json/attack-route/summons.json new file mode 100644 index 0000000..c60b432 --- /dev/null +++ b/src/lineage-json/attack-route/summons.json @@ -0,0 +1,353 @@ +[ + { + "name": "flying skull", + "beingType": "undead", + "sprite": null, + "health": 30, + "baseResistanceTable": { + "poison": 10, + "holy": -50 + }, + "baseDamageTable": { + "physical": 2 + }, + "mana": { + "maximum": 50, + "regen": 5 + }, + "attackStrings": [ + "head slam" + ], + "turns": 5 + }, + { + "name": "skeleton", + "beingType": "undead", + "health": 50, + "sprite": "skeleton", + "baseStrength": 5, + "baseResistanceTable": { + "poison": 10, + "holy": -50 + }, + "baseDamageTable": { + "physical": 3 + }, + "mana": { + "maximum": 50, + "regen": 5 + }, + "attackStrings": [ + "stab", + "cleave" + ], + "turns": 8 + }, + { + "name": "zombie", + "beingType": "undead", + "health": 100, + "sprite": "zombie", + "baseStrength": 8, + "baseResistanceTable": { + "poison": 10, + "fire": -20, + "cold": 20, + "holy": -75 + }, + "baseDamageTable": { + "physical": 10 + }, + "mana": { + "maximum": 50, + "regen": 3 + }, + "attackStrings": [ + "zombie bite", + "grab" + ], + "turns": 10 + }, + { + "name": "wraith", + "beingType": "undead", + "health": 100, + "sprite": "ghost", + "baseIntelligence": 15, + "baseResistanceTable": { + "poison": 40, + "fire": -20, + "cold": 40, + "holy": -50 + }, + "baseDamageTable": { + "magic": 20 + }, + "mana": { + "maximum": 75, + "regen": 8 + }, + "attackStrings": [ + "life drain", + "terrorize" + ], + "turns": 10 + }, + { + "name": "death knight", + "beingType": "undead", + "health": 200, + "baseStrength": 20, + "sprite": null, + "baseResistanceTable": { + "poison": 40, + "fire": 10, + "cold": 40, + "lightning": -20, + "holy": -75 + }, + "baseDamageTable": { + "physical": 35 + }, + "mana": { + "maximum": 100, + "regen": 10 + }, + "attackStrings": [ + "soul strike", + "death blade", + "corrupted cleave" + ], + "turns": 5 + }, + { + "name": "lich", + "beingType": "undead", + "health": 175, + "baseStrength": 30, + "sprite": null, + "baseResistanceTable": { + "poison": 60, + "fire": 15, + "cold": 30, + "lightning": -20, + "holy": -75 + }, + "baseDamageTable": { + "physical": 20, + "magic": 32 + }, + "mana": { + "maximum": 150, + "regen": 15 + }, + "attackStrings": [ + "death bolt", + "soul rip", + "curse" + ], + "turns": 10 + }, + { + "name": "bandit", + "beingType": "human", + "sanity": 50, + "health": 50, + "sprite": "bandit_light", + "baseStrength": 8, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 15, + "poison": -15, + "cold": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 16 + }, + "attackStrings": [ + "stab", + "pocket sand", + "serrate" + ], + "turns": 100 + }, + { + "name": "hobgoblin", + "beingType": "demi-human", + "sanity": null, + "health": 160, + "sprite": "goblin", + "baseStrength": 15, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 15, + "poison": -15, + "cold": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 24 + }, + "attackStrings": [ + "stab", + "frenzy" + ], + "turns": 1000 + }, + { + "name": "raven", + "beingType": "beast", + "sprite": null, + "health": 50, + "baseStrength": 2, + "mana": { + "maximum": 50, + "regen": 5 + }, + "baseResistanceTable": { + "physical": 5, + "lightning": -25, + "cold": 10, + "fire": -15 + }, + "baseDamageTable": { + "physical": 1 + }, + "attackStrings": [ + "pluck eye", + "scratch" + ], + "turns": 1000 + }, + { + "name": "wolf", + "sprite": "wolf_black", + "beingType": "beast", + "health": 100, + "baseStrength": 8, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 10, + "cold": 15, + "fire": -15 + }, + "baseDamageTable": { + "physical": 8 + }, + "attackStrings": [ + "bite" + ], + "turns": 1000 + }, + { + "name": "bear", + "beingType": "beast", + "health": 200, + "baseStrength": 10, + "mana": { + "maximum": 50, + "regen": 3 + }, + "baseResistanceTable": { + "physical": 45, + "cold": 30, + "fire": -10 + }, + "baseDamageTable": { + "physical": 10 + }, + "attackStrings": [ + "bite", + "scratch" + ], + "turns": 1000 + }, + { + "name": "dragon welp", + "beingType": "draconic", + "health": 225, + "baseStrength": 15, + "mana": { + "maximum": 100, + "regen": 15 + }, + "baseResistanceTable": { + "physical": 45, + "cold": 30, + "fire": 75, + "lightning": -30 + }, + "baseDamageTable": { + "physical": 8, + "fire": 10 + }, + "attackStrings": [ + "bite", + "scratch", + "fire breath" + ], + "turns": 1000 + }, + { + "name": "griffon", + "beingType": "beast", + "health": 300, + "baseStrength": 25, + "mana": { + "maximum": 100, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 45, + "cold": 30, + "fire": -15, + "lightning": -30 + }, + "baseDamageTable": { + "physical": 24 + }, + "attackStrings": [ + "claw", + "bite", + "wing buffet" + ], + "turns": 1000 + }, + { + "name": "dragon", + "beingType": "draconic", + "health": 500, + "baseStrength": 30, + "mana": { + "maximum": 100, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 45, + "cold": 30, + "fire": 75, + "lightning": -30 + }, + "baseDamageTable": { + "physical": 22, + "fire": 20 + }, + "attackStrings": [ + "fire breath", + "bite", + "scratch" + ], + "turns": 1000 + } +] + diff --git a/src/lineage-json/conditions-route/conditions.json b/src/lineage-json/conditions-route/conditions.json new file mode 100644 index 0000000..7e6c27a --- /dev/null +++ b/src/lineage-json/conditions-route/conditions.json @@ -0,0 +1,1069 @@ +[ + { + "name": "stun", + "style": "debuff", + "turns": 1, + "effect": [ + "stun" + ], + "effectStyle": [ + null + ], + "effectAmount": [ + null + ], + "icon": "stun" + }, + { + "name": "heavy stun", + "style": "debuff", + "turns": 2, + "effect": [ + "stun" + ], + "effectStyle": [ + null + ], + "effectAmount": [ + null + ], + "icon": "stun" + }, + { + "name": "blind", + "style": "debuff", + "turns": 3, + "effect": [ + "accuracy reduction" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.20 + ], + "icon": "blind" + }, + { + "name": "keen eye", + "style": "buff", + "turns": 5, + "effect": [ + "accuracy increase" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.35 + ], + "icon": "eagle" + }, + { + "name": "bleed", + "style": "debuff", + "turns": 4, + "effect": [ + "health damage" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.020 + ], + "icon": "bleed" + }, + { + "name": "hemmorage", + "style": "debuff", + "turns": 2, + "effect": [ + "health damage" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.050 + ], + "icon": "bleed" + }, + { + "name": "poison", + "style": "debuff", + "turns": 10, + "effect": [ + "health damage" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.05 + ], + "icon": "skull_and_crossbones" + }, + { + "name": "diseased", + "style": "debuff", + "turns": 10, + "effect": [ + "health damage", + "accuracy reduction" + ], + "effectStyle": [ + "percentage", + "multiplier" + ], + "effectAmount": [ + 0.010, + 0.05 + ], + "icon": "viruses" + }, + { + "name": "burn", + "style": "debuff", + "turns": 4, + "effect": [ + "health damage" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.35 + ], + "icon": "flame" + }, + { + "name": "chill", + "style": "debuff", + "turns": 2, + "effect": [ + "weaken" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.25 + ], + "icon": "snowflake" + }, + { + "name": "fear", + "style": "debuff", + "turns": 5, + "effect": [ + "sanity damage" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 2 + ], + "icon": "scarecrow" + }, + { + "name": "undead cower", + "style": "debuff", + "turns": 5, + "effect": [ + "undead cower" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.5 + ], + "icon": "scarecrow" + }, + { + "name": "revoke undead", + "style": "debuff", + "turns": 1, + "effect": [ + "destroy undead" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 100 + ], + "icon": "scarecrow" + }, + { + "name": "lasting heal", + "style": "buff", + "turns": 5, + "effect": [ + "heal" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 25 + ], + "icon": "holding_heart" + }, + { + "name": "delayed heal", + "style": "buff", + "turns": 2, + "effect": [ + "heal" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 100 + ], + "icon": "holding_heart" + }, + { + "name": "head trauma", + "style": "debuff", + "turns": 5, + "effect": [ + "healthMax decrease" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.1 + ], + "icon": "holding_heart" + }, + { + "name": "guard", + "style": "buff", + "turns": 7, + "effect": [ + "armor increase" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 25 + ], + "icon": "shield" + }, + { + "name": "sunder", + "style": "debuff", + "turns": 4, + "effect": [ + "armor decrease" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.25 + ], + "icon": "broken_shield" + }, + { + "name": "blessed defense", + "style": "buff", + "turns": 5, + "effect": [ + "armor increase" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.5 + ], + "icon": "shield" + }, + { + "name": "frenzy", + "style": "buff", + "turns": 3, + "effect": [ + "strengthen" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.25 + ], + "icon": "anger" + }, + { + "name": "enlighten", + "style": "buff", + "turns": 5, + "effect": [ + "manaMax increase" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.25 + ], + "icon": "glow_star" + }, + { + "name": "quickened mind", + "style": "buff", + "turns": 10, + "effect": [ + "mana regen" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 5 + ], + "icon": "glow_star" + }, + { + "name": "greater quickened mind", + "style": "buff", + "turns": 10, + "effect": [ + "mana regen" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 50 + ], + "icon": "glow_star" + }, + { + "name": "dulled mind", + "style": "debuff", + "turns": 5, + "effect": [ + "manaMax decrease" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.1 + ], + "icon": "hollow_disk" + }, + { + "name": "calm", + "style": "buff", + "turns": 5, + "effect": [ + "sanity heal" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 10 + ], + "icon": "pray_hands" + }, + { + "name": "overwhelming glow", + "style": "buff", + "turns": 5, + "effect": [ + "blur" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.75 + ], + "icon": "pray_hands" + }, + { + "name": "unending cure", + "style": "buff", + "turns": 15, + "effect": [ + "heal" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 50 + ], + "icon": "pray_hands" + }, + { + "name": "reflective bulwark", + "style": "buff", + "turns": 10, + "effect": [ + "thorns" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 20 + ], + "icon": "pray_hands" + }, + { + "name": "dance of daggers", + "style": "buff", + "turns": 4, + "effect": [ + "thorns" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 50 + ], + "icon": "dagger_ring" + }, + { + "name": "empowered guarding", + "style": "buff", + "turns": -1, + "aura": true, + "effect": [ + "revenge", + "mana drain" + ], + "effectStyle": [ + "multiplier", + "flat" + ], + "effectAmount": [ + 500, + 5 + ], + "icon": "pray_hands" + }, + { + "name": "protection aura", + "style": "buff", + "turns": -1, + "aura": true, + "effect": [ + "armor increase" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 75 + ], + "icon": "pray_hands" + }, + { + "name": "blood orb", + "style": "buff", + "turns": 25, + "aura": false, + "effect": [ + "blood magic consumable" + ], + "effectStyle": [ + null + ], + "effectAmount": [ + null + ], + "icon": "blood_orb" + }, + { + "name": "execute", + "style": "buff", + "turns": 1, + "aura": false, + "effect": [ + "execute" + ], + "effectStyle": [ + null + ], + "effectAmount": [ + null + ], + "icon": "skull_and_crossbones" + }, + { + "name": "shadow cover", + "style": "buff", + "turns": 5, + "effect": [ + "blur" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.75 + ], + "icon": "hollow_disk" + }, + { + "name": "stone hands", + "style": "buff", + "turns": 5, + "effect": [ + "strengthen" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 10 + ], + "icon": "rock_hands" + }, + { + "name": "rat trap", + "style": "buff", + "turns": 10, + "trapSetupTime": 2, + "effect": [ + "trap" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 10 + ], + "icon": "trap" + }, + { + "name": "bear trap", + "style": "buff", + "turns": 10, + "trapSetupTime": 2, + "effect": [ + "trap" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 100 + ], + "icon": "trap" + }, + { + "name": "dragon trap", + "style": "buff", + "turns": 15, + "trapSetupTime": 5, + "effect": [ + "trap" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 300 + ], + "icon": "trap" + }, + { + "name": "enchanted quiver", + "style": "buff", + "turns": 10, + "effect": [ + "strengthen" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 5 + ], + "icon": "glow_star" + }, + { + "name": "bone shield", + "style": "buff", + "turns": 5, + "effect": [ + "armor increase" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 15 + ], + "icon": "shield" + }, + { + "name": "stealth", + "style": "buff", + "turns": 3, + "effect": [ + "stealth" + ], + "effectStyle": null, + "effectAmount": null, + "icon": "hidden" + }, + { + "name": "remove stealth", + "style": "buff", + "turns": 1, + "effect": [ + "remove stealth" + ], + "effectStyle": null, + "effectAmount": null, + "icon": null + }, + { + "name": "siphon poison", + "style": "buff", + "turns": 5, + "effect": [ + "siphon poison" + ], + "effectStyle": null, + "effectAmount": null, + "icon": "poison_heart" + }, + { + "name": "weaken", + "style": "debuff", + "turns": 3, + "effect": [ + "weaken" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.25 + ], + "icon": "broken_sword" + }, + { + "name": "shocked", + "style": "debuff", + "turns": 1, + "effect": [ + "stun", + "mana drain" + ], + "effectStyle": [ + null, + "flat" + ], + "effectAmount": [ + null, + 20 + ], + "icon": "lightning" + }, + { + "name": "severe burn", + "style": "debuff", + "turns": 6, + "effect": [ + "health damage" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.75 + ], + "icon": "flame" + }, + { + "name": "severe bleed", + "style": "debuff", + "turns": 5, + "effect": [ + "health damage" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.08 + ], + "icon": "bleed" + }, + { + "name": "severe poison", + "style": "debuff", + "turns": 15, + "effect": [ + "health damage" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.1 + ], + "icon": "skull_and_crossbones" + }, + { + "name": "severe disease", + "style": "debuff", + "turns": 15, + "effect": [ + "health damage", + "accuracy reduction" + ], + "effectStyle": [ + "percentage", + "multiplier" + ], + "effectAmount": [ + 0.03, + 0.5 + ], + "icon": "viruses" + }, + { + "name": "severe hemmorage", + "style": "debuff", + "turns": 4, + "effect": [ + "health damage" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.1 + ], + "icon": "bleed" + }, + { + "name": "severe sunder", + "style": "debuff", + "turns": 6, + "effect": [ + "armor decrease" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.5 + ], + "icon": "shield" + }, + { + "name": "silence", + "style": "debuff", + "turns": 3, + "effect": [ + "silenced" + ], + "effectStyle": [ + null + ], + "effectAmount": [ + null + ], + "icon": "blank" + }, + { + "name": "weakened", + "style": "debuff", + "turns": 3, + "effect": [ + "weaken" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.25 + ], + "icon": "broken_sword" + }, + { + "name": "projectile suppression", + "style": "buff", + "turns": 5, + "effect": [ + "blur" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.5 + ], + "icon": "shield" + }, + { + "name": "projectile negation", + "style": "buff", + "turns": 5, + "effect": [ + "blur" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.9 + ], + "icon": "shield" + }, + { + "name": "spell resistance", + "style": "buff", + "turns": 5, + "effect": [ + "armor increase" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.25 + ], + "icon": "magic_shield" + }, + { + "name": "disease immunity", + "style": "buff", + "turns": 10, + "effect": [ + "blur" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 1.0 + ], + "icon": "viruses" + }, + { + "name": "hard to see", + "style": "buff", + "turns": 4, + "effect": [ + "blur" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.6 + ], + "icon": "hidden" + }, + { + "name": "damaging to hit", + "style": "buff", + "turns": 4, + "effect": [ + "thorns" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 25 + ], + "icon": "wind" + }, + { + "name": "minor fortitude", + "style": "buff", + "turns": 5, + "effect": [ + "healthMax increase" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.1 + ], + "icon": "heart" + }, + { + "name": "fortitude", + "style": "buff", + "turns": 5, + "effect": [ + "healthMax increase" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.25 + ], + "icon": "heart" + }, + { + "name": "minor fury", + "style": "buff", + "turns": 3, + "effect": [ + "strengthen" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.15 + ], + "icon": "anger" + }, + { + "name": "righteous fury", + "style": "buff", + "turns": 5, + "effect": [ + "strengthen" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.4 + ], + "icon": "anger" + }, + { + "name": "beast empowerment", + "style": "buff", + "turns": 10, + "effect": [ + "strengthen" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.8 + ], + "icon": "paw" + }, + { + "name": "dual rage", + "style": "buff", + "turns": 5, + "effect": [ + "strengthen" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.4 + ], + "icon": "dual_rage" + }, + { + "name": "rock armor", + "style": "buff", + "turns": 8, + "effect": [ + "armor increase" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 50 + ], + "icon": "rock_shield" + }, + { + "name": "steady aim", + "style": "buff", + "turns": 5, + "effect": [ + "accuracy increase" + ], + "effectStyle": [ + "multiplier" + ], + "effectAmount": [ + 0.25 + ], + "icon": "target" + }, + { + "name": "purify", + "style": "buff", + "turns": 1, + "effect": [ + "heal", + "sanity heal" + ], + "effectStyle": [ + "flat", + "flat" + ], + "effectAmount": [ + 25, + 10 + ], + "icon": "holy_water" + }, + { + "name": "necrotic wound", + "style": "debuff", + "turns": 5, + "effect": [ + "sanityMax decrease", + "healthMax decrease", + "weaken" + ], + "effectStyle": [ + "multiplier", + "multiplier", + "multiplier" + ], + "effectAmount": [ + 0.15, + 0.15, + 0.15 + ], + "icon": "necrotic" + }, + { + "name": "death mark", + "style": "debuff", + "turns": 2, + "effect": [ + "health damage" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.15 + ], + "icon": "death_mark" + } +] + diff --git a/src/lineage-json/conditions-route/debilitations.json b/src/lineage-json/conditions-route/debilitations.json new file mode 100644 index 0000000..0cb2d79 --- /dev/null +++ b/src/lineage-json/conditions-route/debilitations.json @@ -0,0 +1,134 @@ +[ + { + "name": "cataracts", + "style": "debuff", + "turns": -1, + "aura": true, + "effect": [ + "accuracy reduction" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.25 + ], + "icon": "blind" + }, + { + "name": "bad back", + "style": "debuff", + "turns": -1, + "aura": true, + "effect": [ + "weaken" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.25 + ], + "icon": "broken_sword" + }, + { + "name": "bad knee", + "style": "debuff", + "turns": -1, + "aura": true, + "effect": [ + "armor decrease" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.25 + ], + "icon": "broken_shield" + }, + { + "name": "weakened heart", + "style": "debuff", + "turns": -1, + "aura": true, + "effect": [ + "healthMax decrease" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.2 + ], + "icon": "split_heart" + }, + { + "name": "alzheimer's", + "style": "debuff", + "turns": -1, + "aura": true, + "effect": [ + "sanityMax decrease" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.5 + ], + "icon": "hollow_disk" + }, + { + "name": "carpal tunnel", + "style": "debuff", + "turns": -1, + "aura": true, + "effect": [ + "weaken" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.1 + ], + "icon": "rock_hands" + }, + { + "name": "demential", + "style": "debuff", + "turns": -1, + "aura": true, + "effect": [ + "sanityMax decrease", + "manaMax decrease" + ], + "effectStyle": [ + "percentage", + "percentage" + ], + "effectAmount": [ + 0.15, + 0.15 + ], + "icon": "blank" + }, + { + "name": "heart failure", + "style": "debuff", + "turns": -1, + "aura": true, + "effect": [ + "health damage" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 10 + ], + "icon": "split_heart" + } +] + diff --git a/src/lineage-json/conditions-route/sanityDebuffs.json b/src/lineage-json/conditions-route/sanityDebuffs.json new file mode 100644 index 0000000..69807b2 --- /dev/null +++ b/src/lineage-json/conditions-route/sanityDebuffs.json @@ -0,0 +1,171 @@ +[ + { + "name": "heart attack", + "style": "debuff", + "turns": 5, + "effect": [ + "health damage" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.10 + ], + "icon": "split_heart" + }, + { + "name": "distraught", + "style": "debuff", + "turns": 3, + "effect": [ + "sanity damage" + ], + "effectStyle": [ + "flat" + ], + "effectAmount": [ + 5 + ], + "icon": "distraught" + }, + { + "name": "paranoia", + "style": "debuff", + "turns": 4, + "effect": [ + "accuracy reduction", + "sanity damage" + ], + "effectStyle": [ + "percentage", + "flat" + ], + "effectAmount": [ + 0.15, + 2 + ], + "icon": "scarecrow" + }, + { + "name": "hallucination", + "style": "debuff", + "turns": 3, + "effect": [ + "accuracy reduction", + "blur" + ], + "effectStyle": [ + "percentage", + null + ], + "effectAmount": [ + 0.25, + null + ], + "icon": "blind" + }, + { + "name": "trembling", + "style": "debuff", + "turns": 2, + "effect": [ + "weaken" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.20 + ], + "icon": "broken_sword" + }, + { + "name": "delirium", + "style": "debuff", + "turns": 3, + "effect": [ + "sanity damage", + "accuracy reduction" + ], + "effectStyle": [ + "flat", + "percentage" + ], + "effectAmount": [ + 3, + 0.15 + ], + "icon": "viruses" + }, + { + "name": "phobia", + "style": "debuff", + "turns": 4, + "effect": [ + "weaken", + "sanity damage" + ], + "effectStyle": [ + "percentage", + "flat" + ], + "effectAmount": [ + 0.25, + 2 + ], + "icon": "skull_and_crossbones" + }, + { + "name": "dissociation", + "style": "debuff", + "turns": 3, + "effect": [ + "armor decrease" + ], + "effectStyle": [ + "percentage" + ], + "effectAmount": [ + 0.30 + ], + "icon": "broken_shield" + }, + { + "name": "madness_whispers", + "style": "debuff", + "turns": 4, + "effect": [ + "sanity damage", + "silenced" + ], + "effectStyle": [ + "flat", + null + ], + "effectAmount": [ + 2, + null + ], + "icon": "hidden" + }, + { + "name": "despair", + "style": "debuff", + "turns": 5, + "effect": [ + "sanityMax decrease", + "healthMax decrease" + ], + "effectStyle": [ + "percentage", + "percentage" + ], + "effectAmount": [ + 0.10, + 0.10 + ], + "icon": "distraught" + } +] + diff --git a/src/lineage-json/dungeon-route/dungeons.json b/src/lineage-json/dungeon-route/dungeons.json new file mode 100644 index 0000000..4591306 --- /dev/null +++ b/src/lineage-json/dungeon-route/dungeons.json @@ -0,0 +1,1600 @@ +[ + { + "name": "training grounds", + "bgName": "Castle_Atrium", + "isComplete": true, + "id": 0, + "difficulty": 0, + "levels": [ + { + "level": 1, + "tiles": 0, + "unlocked": true, + "normalEncounters": [ + [ + { + "name": "training dummy", + "scaler": 1 + } + ] + ], + "bossEncounter": [], + "specialEncounters": [] + } + ], + "unlocks": [] + }, + { + "name": "nearby cave", + "bgName": "Cave", + "isComplete": true, + "id": 1, + "difficulty": 1, + "instanceDrops": [ + { + "item": "stick", + "itemType": "melee", + "chance": 0.2 + } + ], + "levels": [ + { + "level": 1, + "tiles": 15, + "unlocked": true, + "normalEncounters": [ + [ + { + "name": "giant rat", + "scaler": 1 + } + ], + [ + { + "name": "bat", + "scaler": 1 + } + ] + ], + "bossEncounter": [ + { + "name": "zombie", + "scaler": 1 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "1": 1.0 + }, + "scaler": 1 + }, + { + "name": "chest", + "countChances": { + "0": 0.1, + "1": 0.8, + "2": 0.1 + }, + "scaler": 1 + }, + { + "name": "ritual altar", + "countChances": { + "1": 1 + }, + "scaler": 1 + } + ] + }, + { + "level": 2, + "tiles": 15, + "normalEncounters": [ + [ + { + "name": "skeleton", + "scaler": 1 + } + ], + [ + { + "name": "skeleton mage", + "scaler": 1 + } + ], + [ + { + "name": "ghost", + "scaler": 1 + } + ], + [ + { + "name": "bat", + "scaler": 0.75 + }, + { + "name": "bat", + "scaler": 0.75 + } + ] + ], + "bossEncounter": [ + { + "name": "zombie", + "scaler": 1.25 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "1": 1.0 + }, + "scaler": 1 + }, + { + "name": "chest", + "countChances": { + "0": 0.1, + "1": 0.8, + "2": 0.1 + }, + "scaler": 1 + }, + { + "name": "ritual altar", + "countChances": { + "0": 0.5, + "1": 0.5 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0.5, + "1": 0.5 + }, + "scaler": 1 + } + ] + }, + { + "level": 3, + "tiles": 25, + "normalEncounters": [ + [ + { + "name": "skeleton", + "scaler": 1 + } + ], + [ + { + "name": "skeleton mage", + "scaler": 1 + } + ], + [ + { + "name": "ghost", + "scaler": 1 + } + ], + [ + { + "name": "zombie", + "scaler": 1 + } + ], + [ + { + "name": "skeleton", + "scaler": 0.75 + }, + { + "name": "skeleton", + "scaler": 0.75 + } + ], + [ + { + "name": "skeleton", + "scaler": 0.75 + }, + { + "name": "skeleton mage", + "scaler": 0.75 + } + ], + [ + { + "name": "skeleton mage", + "scaler": 0.75 + }, + { + "name": "skeleton mage", + "scaler": 0.75 + } + ] + ], + "bossEncounter": [ + { + "name": "necromancer", + "scaler": 1 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "2": 1.0 + }, + "scaler": 1 + }, + { + "name": "chest", + "countChances": { + "0": 0.1, + "1": 0.8, + "2": 0.1 + }, + "scaler": 1 + }, + { + "name": "ritual altar", + "countChances": { + "0": 0.5, + "1": 0.5 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0.5, + "1": 0.5 + }, + "scaler": 1 + } + ] + } + ], + "unlocks": [ + "goblin cave", + "bandit hideout" + ] + }, + { + "name": "goblin cave", + "bgName": "Cave", + "isComplete": true, + "id": 2, + "difficulty": 2, + "levels": [ + { + "level": 1, + "tiles": 20, + "unlocked": true, + "normalEncounters": [ + [ + { + "name": "goblin", + "scaler": 1 + } + ], + [ + { + "name": "warg", + "scaler": 1 + } + ], + [ + { + "name": "bat", + "scaler": 0.75 + }, + { + "name": "bat", + "scaler": 0.75 + } + ] + ], + "bossEncounter": [ + { + "name": "kobold", + "scaler": 1 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "1": 1.0 + }, + "scaler": 1 + }, + { + "name": "chest", + "countChances": { + "0": 0.1, + "1": 0.8, + "2": 0.1 + }, + "scaler": 1 + }, + { + "name": "ritual altar", + "countChances": { + "0": 0.5, + "1": 0.5 + }, + "scaler": 1.5 + } + ] + }, + { + "level": 2, + "tiles": 25, + "normalEncounters": [ + [ + { + "name": "goblin", + "scaler": 1 + } + ], + [ + { + "name": "warg", + "scaler": 1 + } + ], + [ + { + "name": "kobold", + "scaler": 0.75 + }, + { + "name": "goblin", + "scaler": 0.75 + } + ], + [ + { + "name": "goblin", + "scaler": 0.75 + }, + { + "name": "warg", + "scaler": 0.75 + } + ] + ], + "bossEncounter": [ + { + "name": "warg", + "scaler": 1.0 + }, + { + "name": "goblin mage", + "scaler": 1.0 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "2": 1.0 + }, + "scaler": 1 + }, + { + "name": "chest", + "countChances": { + "0": 0.1, + "1": 0.5, + "2": 0.4 + }, + "scaler": 1 + }, + { + "name": "ritual altar", + "countChances": { + "0": 0.5, + "1": 0.5 + }, + "scaler": 1.5 + } + ] + } + ], + "unlocks": [ + "frost spire fortress" + ] + }, + { + "name": "bandit hideout", + "bgName": "Plains", + "isComplete": true, + "id": 3, + "difficulty": 3, + "levels": [ + { + "level": 1, + "tiles": 20, + "unlocked": true, + "normalEncounters": [ + [ + { + "name": "bandit", + "scaler": 1 + } + ], + [ + { + "name": "bandit", + "scaler": 0.65 + }, + { + "name": "bandit", + "scaler": 0.65 + } + ], + [ + { + "name": "bandit heavy", + "scaler": 1 + } + ] + ], + "bossEncounter": [ + { + "name": "bandit heavy", + "scaler": 1 + }, + { + "name": "bandit", + "scaler": 0.65 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "1": 1.0 + }, + "scaler": 1 + }, + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0, + "2": 0.4, + "3": 0.6 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0.5, + "1": 0.5 + }, + "scaler": 1 + } + ] + }, + { + "level": 2, + "tiles": 30, + "normalEncounters": [ + [ + { + "name": "bandit", + "scaler": 1 + } + ], + [ + { + "name": "bandit", + "scaler": 0.65 + }, + { + "name": "bandit", + "scaler": 0.65 + } + ], + [ + { + "name": "bandit heavy", + "scaler": 1 + } + ] + ], + "bossEncounter": [ + { + "name": "huge knight", + "scaler": 1.0 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "2": 1.0 + }, + "scaler": 1 + }, + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0, + "2": 0.1, + "3": 0.5, + "4": 0.4 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0, + "1": 1 + }, + "scaler": 1 + } + ] + } + ], + "unlocks": [ + "infestation", + "ancient arena" + ] + }, + { + "name": "frost spire fortress", + "id": 4, + "isComplete": true, + "difficulty": 5, + "bgName": "SnowyMountain", + "levels": [ + { + "level": 1, + "tiles": 10, + "normalEncounters": [ + [ + { + "name": "novice fire mage", + "scaler": 1 + } + ], + [ + { + "name": "novice air mage", + "scaler": 1 + } + ] + ], + "bossEncounter": [ + { + "name": "adept air mage", + "scaler": 1 + } + ], + "specialEncounters": [ + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0.5, + "2": 0.5 + }, + "scaler": 1 + }, + { + "name": "ritual altar", + "countChances": { + "1": 1 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0, + "1": 1 + }, + "scaler": 1 + } + ] + }, + { + "level": 2, + "tiles": 10, + "normalEncounters": [ + [ + { + "name": "novice fire mage", + "scaler": 1 + } + ], + [ + { + "name": "novice water mage", + "scaler": 1 + } + ] + ], + "bossEncounter": [ + { + "name": "adept water mage", + "scaler": 1 + } + ], + "specialEncounters": [ + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0.5, + "2": 0.5 + }, + "scaler": 1 + }, + { + "name": "ritual altar", + "countChances": { + "1": 1 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0, + "1": 1 + }, + "scaler": 1 + } + ] + }, + { + "level": 3, + "tiles": 10, + "normalEncounters": [ + [ + { + "name": "novice air mage", + "scaler": 1 + } + ], + [ + { + "name": "novice earth mage", + "scaler": 1 + } + ] + ], + "bossEncounter": [ + { + "name": "master earthen monk", + "scaler": 1 + } + ], + "specialEncounters": [ + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0.5, + "2": 0.5 + }, + "scaler": 1 + }, + { + "name": "ritual altar", + "countChances": { + "1": 1 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0, + "1": 1 + }, + "scaler": 1 + } + ] + }, + { + "level": 4, + "tiles": 15, + "normalEncounters": [ + [ + { + "name": "novice air mage", + "scaler": 1 + } + ], + [ + { + "name": "novice earth mage", + "scaler": 1 + } + ], + [ + { + "name": "novice fire mage", + "scaler": 1 + } + ], + [ + { + "name": "novice water mage", + "scaler": 1 + } + ], + [ + { + "name": "novice fire mage", + "scaler": 0.75 + }, + { + "name": "novice air mage", + "scaler": 0.75 + } + ] + ], + "bossEncounter": [ + { + "name": "master fire mage", + "scaler": 1 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "2": 1.0 + }, + "scaler": 1 + }, + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0.5, + "2": 0.5 + }, + "scaler": 1 + }, + { + "name": "ritual altar", + "countChances": { + "1": 1 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0, + "1": 1 + }, + "scaler": 1 + } + ] + } + ], + "unlocks": [ + "dark forest" + ] + }, + { + "name": "infestation", + "isComplete": true, + "id": 5, + "difficulty": 6, + "bgName": "SpiderLair", + "levels": [ + { + "level": 1, + "tiles": 20, + "normalEncounters": [ + [ + { + "name": "spider", + "scaler": 1.0 + }, + { + "name": "spider", + "scaler": 1.0 + }, + { + "name": "spider", + "scaler": 1.0 + } + ], + [ + { + "name": "giant spider", + "scaler": 1.0 + } + ] + ], + "bossEncounter": [ + { + "name": "giant venomous spider", + "scaler": 1 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "1": 1.0 + }, + "scaler": 1 + }, + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0.5, + "2": 0.5 + }, + "scaler": 1 + } + ] + }, + { + "level": 2, + "tiles": 25, + "normalEncounters": [ + [ + { + "name": "spider", + "scaler": 1.0 + }, + { + "name": "spider", + "scaler": 1.0 + }, + { + "name": "spider", + "scaler": 1.0 + } + ], + [ + { + "name": "venomous spider", + "scaler": 1.0 + }, + { + "name": "venomous spider", + "scaler": 1.0 + } + ], + [ + { + "name": "giant spider", + "scaler": 1.0 + } + ], + [ + { + "name": "giant venomous spider", + "scaler": 1.0 + } + ] + ], + "bossEncounter": [ + { + "name": "brood mother", + "scaler": 1.0 + }, + { + "name": "giant venomous spider", + "scaler": 1.0 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "2": 1.0 + }, + "scaler": 1 + }, + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0.5, + "2": 0.5 + }, + "scaler": 1 + } + ] + } + ], + "unlocks": [ + "crystal labyrinthine - first excavation" + ] + }, + { + "name": "dark forest", + "id": 6, + "difficulty": 6, + "isComplete": true, + "bgName": "DemonWoods", + "levels": [ + { + "level": 1, + "tiles": 25, + "normalEncounters": [ + [ + { + "name": "satyr", + "scaler": 1.0 + } + ], + [ + { + "name": "harpy", + "scaler": 1.0 + } + ], + [ + { + "name": "harpy", + "scaler": 0.7 + }, + { + "name": "harpy", + "scaler": 0.7 + } + ] + ], + "bossEncounter": [ + { + "name": "centaur", + "scaler": 1.0 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "2": 1.0 + }, + "scaler": 1 + } + ] + }, + { + "level": 2, + "tiles": 30, + "normalEncounters": [ + [ + { + "name": "centaur", + "scaler": 1.0 + } + ], + [ + { + "name": "satyr", + "scaler": 1.0 + } + ], + [ + { + "name": "harpy", + "scaler": 1.0 + } + ], + [ + { + "name": "harpy", + "scaler": 0.7 + }, + { + "name": "harpy", + "scaler": 0.7 + } + ] + ], + "bossEncounter": [ + { + "name": "gryphon", + "scaler": 1.0 + } + ], + "specialEncounters": [ + { + "name": "camp", + "countChances": { + "3": 1.0 + }, + "scaler": 1 + } + ] + } + ], + "unlocks": [ + "ancient arena" + ] + }, + { + "name": "ancient arena", + "id": 7, + "difficulty": 8, + "isComplete": true, + "bgName": "dungeon", + "levels": [ + { + "level": 1, + "tiles": 10, + "normalEncounters": [ + [ + { + "name": "legionnaire", + "scaler": 1.0 + } + ] + ], + "bossEncounter": [ + { + "name": "gladiator", + "scaler": 1.0 + } + ], + "specialEncounters": [ + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0.5, + "2": 0.5 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0, + "1": 1 + }, + "scaler": 1 + } + ] + }, + { + "level": 2, + "tiles": 10, + "normalEncounters": [ + [ + { + "name": "legionnaire", + "scaler": 1.0 + } + ], + [ + { + "name": "gladiator", + "scaler": 1.0 + } + ] + ], + "bossEncounter": [ + { + "name": "lizardman", + "scaler": 1.0 + } + ], + "specialEncounters": [ + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0.5, + "2": 0.5 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0, + "1": 1 + }, + "scaler": 1 + } + ] + }, + { + "level": 3, + "tiles": 10, + "normalEncounters": [ + [ + { + "name": "lizardman", + "scaler": 0.75 + }, + { + "name": "lizardman", + "scaler": 0.75 + } + ], + [ + { + "name": "legionnaire", + "scaler": 1.0 + } + ], + [ + { + "name": "gladiator", + "scaler": 1.0 + } + ] + ], + "bossEncounter": [ + { + "name": "the hammer", + "scaler": 1.0 + } + ], + "specialEncounters": [ + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0.5, + "2": 0.5 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0, + "1": 1 + }, + "scaler": 1 + } + ] + }, + { + "level": 4, + "tiles": 10, + "normalEncounters": [ + [ + { + "name": "lizardman", + "scaler": 0.75 + }, + { + "name": "lizardman", + "scaler": 0.75 + } + ], + [ + { + "name": "legionnaire", + "scaler": 1.0 + } + ], + [ + { + "name": "gladiator", + "scaler": 1.0 + } + ] + ], + "bossEncounter": [ + { + "name": "her", + "scaler": 1.0 + }, + { + "name": "the deadeye", + "scaler": 1.0 + }, + { + "name": "the spear", + "scaler": 1.0 + } + ], + "specialEncounters": [ + { + "name": "chest", + "countChances": { + "0": 0, + "1": 0.5, + "2": 0.5 + }, + "scaler": 1 + }, + { + "name": "water basin", + "countChances": { + "0": 0, + "1": 1 + }, + "scaler": 1 + } + ] + } + ], + "unlocks": [] + }, + { + "name": "crystal labyrinthine - first excavation", + "id": 7, + "difficulty": 8, + "isComplete": false, + "bgName": "CrystalCave_1", + "levels": [ + { + "level": 1, + "tiles": 20, + "isComplete": false, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + }, + { + "level": 2, + "tiles": 25, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + } + ], + "unlocks": [ + "crystal labyrinthine - second excavation" + ] + }, + { + "name": "crystal labyrinthine - second excavation", + "id": 8, + "difficulty": 9, + "isComplete": false, + "bgName": "CrystalCave_2", + "levels": [ + { + "level": 1, + "tiles": 20, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + }, + { + "level": 2, + "tiles": 25, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + } + ], + "unlocks": [ + "crystal labyrinthine - third excavation" + ] + }, + { + "name": "crystal labyrinthine - third excavation", + "id": 9, + "difficulty": 10, + "isComplete": false, + "bgName": "CrystalCave_3", + "levels": [ + { + "level": 1, + "tiles": 20, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + }, + { + "level": 2, + "tiles": 25, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + } + ], + "unlocks": [ + "crystal labyrinthine - fourth excavation" + ] + }, + { + "name": "crystal labyrinthine - fourth excavation", + "id": 10, + "difficulty": 11, + "isComplete": false, + "bgName": "CrystalCave_4", + "levels": [ + { + "level": 1, + "tiles": 20, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + }, + { + "level": 2, + "tiles": 25, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + } + ], + "unlocks": [ + "corrupted temple - outer terrace" + ] + }, + { + "name": "corrupted temple - outer terrace", + "id": 11, + "difficulty": 12, + "isComplete": false, + "bgName": "Castle_Entrance", + "levels": [ + { + "level": 1, + "tiles": 20, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + }, + { + "level": 2, + "tiles": 25, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + } + ], + "unlocks": [ + "corrupted temple - ramparts" + ] + }, + { + "name": "corrupted temple - ramparts", + "id": 12, + "difficulty": 14, + "isComplete": false, + "bgName": "Castle_Ramparts", + "levels": [ + { + "level": 1, + "tiles": 20, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + }, + { + "level": 2, + "tiles": 25, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + } + ], + "unlocks": [ + "corrupted temple - hall" + ] + }, + { + "name": "corrupted temple - hall", + "id": 13, + "difficulty": 16, + "isComplete": false, + "bgName": "Castle_Hall", + "levels": [ + { + "level": 1, + "tiles": 20, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + }, + { + "level": 2, + "tiles": 25, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + } + ], + "unlocks": [] + }, + { + "name": "corrupted temple - atrium", + "id": 14, + "difficulty": 18, + "isComplete": false, + "bgName": "Castle_Atrium", + "levels": [ + { + "level": 1, + "tiles": 20, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + }, + { + "level": 2, + "tiles": 25, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "", + "scaler": 1.0 + } + ], + "specialEncounters": [] + } + ], + "unlocks": [ + "ronin's redoubt" + ] + }, + { + "name": "ronin's redoubt", + "id": 15, + "difficulty": 20, + "isComplete": false, + "bgName": "AutumnForest", + "levels": [ + { + "level": 1, + "tiles": 2, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "samurai old", + "scaler": 1.0 + } + ], + "specialEncounters": [] + } + ], + "unlocks": [ + "blade fortress" + ] + }, + { + "name": "kōtetsu moses", + "id": 16, + "difficulty": 25, + "isComplete": false, + "bgName": "AutumnForest", + "levels": [ + { + "level": 1, + "tiles": 20, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "samurai old", + "scaler": 1.0 + } + ], + "specialEncounters": [] + }, + { + "level": 2, + "tiles": 30, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "samurai old", + "scaler": 1.0 + } + ], + "specialEncounters": [] + }, + { + "level": 3, + "tiles": 10, + "normalEncounters": [], + "bossEncounter": [ + { + "name": "samurai old", + "scaler": 1.0 + } + ], + "specialEncounters": [] + } + ], + "unlocks": [ + "ronin's redoubt" + ] + } +] + diff --git a/src/lineage-json/dungeon-route/specialEncounters.json b/src/lineage-json/dungeon-route/specialEncounters.json new file mode 100644 index 0000000..8092272 --- /dev/null +++ b/src/lineage-json/dungeon-route/specialEncounters.json @@ -0,0 +1,126 @@ +[ + { + "name": "camp", + "image": "camp", + "prompt": "This seems like a good place to rest... Set up camp and rest a while?", + "goodOutcome": { + "chance": 0.25, + "message": "You awaken massively restored.", + "result": { + "effect": { + "sanity": 50, + "health": 50 + } + } + }, + "neutralOutcome": { + "chance": 0.65, + "message": "You feel rested.", + "result": { + "effect": { + "sanity": 10, + "health": 10 + } + } + }, + "badOutcome": { + "chance": 0.1, + "message": "Ambush! Prepare for battle!", + "result": { + "battle": [ + "bandit" + ] + } + } + }, + { + "name": "chest", + "image": "chest", + "prompt": "You come across a chest. Do you open it?", + "goodOutcome": { + "chance": 0.65, + "message": "For the explorer, the spoils...", + "result": { + "drops": [ + { + "name": "iron chestpiece", + "itemType": "bodyArmor", + "chance": 0.1 + } + ], + "gold": { + "min": 50, + "max": 250 + } + } + }, + "neutralOutcome": { + "chance": 0.25, + "message": "The chest appears to already have been looted, also you got a splinter", + "result": { + "effect": { + "sanity": -5, + "health": -1 + } + } + }, + "badOutcome": { + "chance": 0.10, + "message": "There is more here than it seems... Prepare for battle!", + "result": { + "battle": [ + "mimic" + ] + } + } + }, + { + "name": "ritual altar", + "image": "ritual_altar", + "prompt": "You come across an ominous altar. Do you approach it?", + "goodOutcome": { + "chance": 0.5, + "message": "A soothing calm comes over you...", + "result": { + "effect": { + "health": 50, + "sanity": 30 + } + } + }, + "badOutcome": { + "chance": 0.5, + "message": "A feeling of dread overwhelms you.", + "result": { + "effect": { + "sanity": -30 + } + } + } + }, + { + "name": "water basin", + "image": "water_basin", + "prompt": "You come across an ominous altar. Do you approach it?", + "goodOutcome": { + "chance": 0.5, + "message": "A soothing calm comes over you...", + "result": { + "effect": { + "health": 50, + "sanity": 30 + } + } + }, + "badOutcome": { + "chance": 0.5, + "message": "A feeling of dread overwhelms you.", + "result": { + "effect": { + "sanity": -30 + } + } + } + } +] + diff --git a/src/lineage-json/enemy-route/bosses.json b/src/lineage-json/enemy-route/bosses.json new file mode 100644 index 0000000..69dc953 --- /dev/null +++ b/src/lineage-json/enemy-route/bosses.json @@ -0,0 +1,900 @@ +[ + { + "name": "zombie", + "sprite": "zombie", + "beingType": "undead", + "sanity": null, + "health": 80, + "mana": { + "maximum": 50, + "regen": 5 + }, + "attackStrings": [ + "grab", + "zombie bite" + ], + "animationStrings": { + "grab": "attack_1", + "zombie bite": "attack_1" + }, + "baseResistanceTable": { + "physical": 15, + "poison": 75, + "fire": 15, + "cold": 15, + "holy": -50 + }, + "baseDamageTable": { + "physical": 6 + }, + "drops": [ + { + "item": "chunk of flesh", + "itemType": "junk", + "chance": 0.75 + }, + { + "item": "chunk of flesh", + "itemType": "junk", + "chance": 0.75 + } + ], + "goldDropRange": { + "minimum": 100, + "maximum": 150 + } + }, + { + "name": "necromancer", + "beingType": "human", + "sprite": "necromancer", + "sanity": 50, + "health": 130, + "mana": { + "maximum": 80, + "regen": 8 + }, + "attackStrings": [ + "dark burst", + "raise skeleton", + "terrorize" + ], + "animationStrings": { + "dark burst": "attack_1", + "raise skeleton": "attack_2", + "terrorize": "attack_3" + }, + "baseResistanceTable": { + "physical": 15, + "poison": 10, + "fire": 5, + "cold": 15, + "holy": -20 + }, + "baseDamageTable": { + "physical": 8 + }, + "phases": [ + { + "triggerHealth": 0, + "health": 70, + "sprite": "reaper", + "dialogue": { + "1": "Witness my true power!" + }, + "baseResistanceTable": { + "physical": 15, + "poison": 50, + "fire": 15, + "cold": 30, + "holy": -75 + }, + "baseDamageTable": { + "poison": 5, + "cold": 12 + }, + "attackStrings": [ + "soul strike", + "death blade" + ], + "animationStrings": { + "soul strike": "attack_1", + "death blade": "attack_2" + } + } + ], + "drops": [ + { + "item": "dagger", + "itemType": "melee", + "chance": 0.15 + }, + { + "item": "dagger", + "itemType": "melee", + "chance": 0.15 + }, + { + "item": "adept robes", + "itemType": "robe", + "chance": 1.0 + } + ], + "storyDrops": [ + { + "item": "the deed to the whispering raven inn" + } + ], + "goldDropRange": { + "minimum": 250, + "maximum": 450 + } + }, + { + "name": "kobold", + "beingType": "demi-human", + "sprite": "kobold", + "sanity": null, + "health": 160, + "mana": { + "maximum": 60, + "regen": 15 + }, + "baseResistanceTable": { + "lightning": 15, + "cold": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 6, + "lightning": 10 + }, + "attackStrings": [ + "stab", + "cleave", + "chop", + "charged pierce" + ], + "animationStrings": { + "stab": "attack_1", + "cleave": "attack_2", + "chop": "attack_3", + "charged pierce": "attack_4" + }, + "drops": [ + { + "item": "longsword", + "itemType": "melee", + "chance": 0.65 + }, + { + "item": "longsword", + "itemType": "melee", + "chance": 0.65 + } + ], + "goldDropRange": { + "minimum": 100, + "maximum": 150 + } + }, + { + "name": "goblin mage", + "sprite": "goblin_mage", + "beingType": "demi-human", + "sanity": 50, + "health": 140, + "mana": { + "maximum": 100, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 10, + "poison": -5, + "lightning": -5 + }, + "baseDamageTable": { + "magic": 16 + }, + "attackStrings": [ + "pulse", + "dark bolt" + ], + "animationStrings": { + "pulse": "attack_1", + "dark bolt": "attack_2" + }, + "drops": [ + { + "item": "goblin totem", + "itemType": "staff", + "chance": 1.0 + } + ], + "storyDrops": [ + { + "item": "head of goblin shaman" + } + ], + "goldDropRange": { + "minimum": 100, + "maximum": 150 + } + }, + { + "name": "warg", + "sprite": "wolf_black", + "beingType": "beast", + "sanity": null, + "health": 160, + "mana": { + "maximum": 50, + "regen": 5 + }, + "baseResistanceTable": { + "physical": 15, + "cold": 10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 18 + }, + "attackStrings": [ + "bite", + "claw" + ], + "animationStrings": { + "bite": "attack_1", + "claw": "attack_2" + }, + "drops": [ + { + "item": "patch of hair", + "itemType": "junk", + "chance": 0.85 + }, + { + "item": "patch of hair", + "itemType": "junk", + "chance": 0.85 + }, + { + "item": "bone", + "itemType": "ingredient", + "chance": 0.75 + }, + { + "item": "bone", + "itemType": "ingredient", + "chance": 0.75 + } + ], + "goldDropRange": { + "minimum": 40, + "maximum": 55 + } + }, + { + "name": "bandit", + "beingType": "human", + "sprite": "bandit_light", + "sanity": 50, + "health": 75, + "mana": { + "maximum": 50, + "regen": 3 + }, + "baseResistanceTable": { + "physical": 15, + "poison": -15, + "cold": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 16 + }, + "attackStrings": [ + "stab", + "pocket sand", + "serrate" + ], + "animationStrings": { + "stab": "attack_1", + "pocket sand": "attack_1", + "serrate": "attack_1" + }, + "drops": [ + { + "item": "shortsword", + "itemType": "melee", + "chance": 0.15 + }, + { + "item": "cheap leather chestpiece", + "itemType": "bodyArmor", + "chance": 0.15 + }, + { + "item": "leather headgear", + "itemType": "helmet", + "chance": 0.15 + } + ], + "goldDropRange": { + "minimum": 70, + "maximum": 100 + }, + "armorValue": 15 + }, + { + "name": "bandit heavy", + "sprite": "bandit_heavy", + "beingType": "human", + "sanity": 25, + "health": 155, + "mana": { + "maximum": 30, + "regen": 2 + }, + "baseResistanceTable": { + "physical": 25, + "poison": -15, + "cold": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 14 + }, + "attackStrings": [ + "pommel strike", + "heavy swing" + ], + "animationStrings": { + "pommel strike": "attack_1", + "heavy swing": "attack_1" + }, + "drops": [ + { + "item": "longsword", + "itemType": "melee", + "chance": 0.25 + }, + { + "item": "cheap iron chestpiece", + "itemType": "bodyArmor", + "chance": 0.25 + } + ], + "goldDropRange": { + "minimum": 80, + "maximum": 100 + }, + "armorValue": 15 + }, + { + "name": "huge knight", + "beingType": "human", + "sprite": "huge_knight", + "sanity": 50, + "health": 200, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 35, + "poison": -15, + "cold": -20, + "fire": -15 + }, + "baseDamageTable": { + "physical": 18 + }, + "attackStrings": [ + "heavy swing", + "call backup", + "stab" + ], + "animationStrings": { + "heavy swing": "attack_1", + "call backup": "attack_1", + "stab": "attack_1" + }, + "drops": [], + "storyDrops": [ + { + "item": "broken seal contract" + } + ], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 30 + }, + { + "name": "giant venomous spider", + "beingType": "beast", + "sprite": "spider_default", + "sanity": null, + "health": 140, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "poison": 75, + "cold": -10, + "lightning": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 10, + "poison": 20 + }, + "attackStrings": [ + "venomous bite" + ], + "animationStrings": { + "venomous bite": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + } + }, + { + "name": "brood mother", + "beingType": "beast", + "sprite": "spider_default_brood", + "sanity": null, + "health": 240, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "poison": 75, + "cold": -10, + "lightning": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 4, + "poison": 20 + }, + "attackStrings": [ + "venomous bite" + ], + "animationStrings": { + "venomous bite": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + } + }, + { + "name": "adept air mage", + "beingType": "human", + "sprite": "wizard_gray", + "sanity": 50, + "health": 150, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 5 + }, + "baseDamageTable": { + "physical": 36 + }, + "attackStrings": [ + "bonk", + "gust" + ], + "animationStrings": { + "gust": "attack_1", + "bonk": "attack_2" + }, + "drops": [], + "storyDrops": [], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 10 + }, + { + "name": "adept water mage", + "sprite": "wizard_classic", + "beingType": "human", + "sanity": 50, + "health": 150, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 5 + }, + "baseDamageTable": { + "cold": 35 + }, + "attackStrings": [ + "bonk", + "frost" + ], + "animationStrings": { + "frost": "attack_1", + "bonk": "attack_2" + }, + "drops": [], + "storyDrops": [], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 10 + }, + { + "name": "master earthen monk", + "sprite": "ground_monk", + "beingType": "human", + "sanity": 100, + "health": 190, + "mana": { + "maximum": 100, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 10, + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 5 + }, + "baseDamageTable": { + "physical": 24, + "raw": 16 + }, + "attackStrings": [ + "kick", + "punch", + "flurry", + "rock spike", + "burial" + ], + "animationStrings": { + "kick": "attack_1", + "punch": "attack_2", + "flurry": "attack_3", + "rock spike": "attack_4", + "burial": "attack_5" + }, + "drops": [], + "storyDrops": [], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 10 + }, + { + "name": "master fire mage", + "sprite": "pyromancer", + "beingType": "human", + "sanity": 100, + "health": 180, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 10, + "poison": -15, + "cold": -10, + "fire": 50, + "holy": 5 + }, + "baseDamageTable": { + "fire": 40 + }, + "attackStrings": [ + "fire bolt" + ], + "animationStrings": { + "fire bolt": "attack_1" + }, + "drops": [], + "storyDrops": [], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 10 + }, + { + "name": "centaur", + "beingType": "demi-human", + "sprite": "centaur", + "sanity": 50, + "health": 200, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 15, + "poison": 20, + "cold": -5, + "holy": 25 + }, + "baseDamageTable": { + "physical": 45 + }, + "attackStrings": [ + "stampede", + "chop" + ], + "animationStrings": { + "chop": "attack_1", + "stampede": "attack_2" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + } + }, + { + "name": "gryphon", + "sprite": "gryphon", + "beingType": "beast", + "sanity": null, + "health": 225, + "mana": { + "maximum": 60, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 15, + "poison": 20, + "cold": 10, + "holy": 25, + "lightning": -35 + }, + "baseDamageTable": { + "physical": 45 + }, + "attackStrings": [ + "claw", + "wing buffet" + ], + "animationStrings": { + "claw": "attack_1", + "wing buffet": "attack_2" + }, + "drops": [], + "storyDrops": [], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 10 + }, + { + "name": "gladiator", + "sprite": "gladiator_ls", + "beingType": "human", + "sanity": 50, + "health": 220, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 5 + }, + "baseDamageTable": { + "physical": 40 + }, + "attackStrings": [ + "pierce" + ], + "animationStrings": { + "pierce": "attack_1" + }, + "drops": [], + "storyDrops": [], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 50 + }, + { + "name": "lizardman", + "sprite": "lizardman", + "beingType": "demi-human", + "sanity": null, + "health": 180, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "poison": 35, + "cold": 70, + "fire": 30, + "holy": 5, + "lightning": -5 + }, + "baseDamageTable": { + "physical": 40, + "poison": 10 + }, + "attackStrings": [ + "cleave" + ], + "animationStrings": { + "cleave": "attack_1" + }, + "drops": [], + "storyDrops": [], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 50 + }, + { + "name": "the hammer", + "sprite": "gladiator_hammer", + "beingType": "human", + "sanity": 50, + "health": 295, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 5 + }, + "baseDamageTable": { + "physical": 40, + "raw": 15 + }, + "attackStrings": [ + "heavy swing" + ], + "animationStrings": { + "heavy swing": "attack_1" + }, + "drops": [], + "storyDrops": [], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 50 + }, + { + "name": "the spear", + "sprite": "gladiator_spear", + "beingType": "human", + "sanity": 50, + "health": 270, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 5 + }, + "baseDamageTable": { + "physical": 55 + }, + "attackStrings": [ + "pierce" + ], + "animationStrings": { + "pierce": "attack_1" + }, + "drops": [], + "storyDrops": [], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 50 + }, + { + "name": "the deadeye", + "sprite": "gladiator_archer", + "beingType": "human", + "sanity": 50, + "health": 245, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 5 + }, + "baseDamageTable": { + "physical": 55 + }, + "attackStrings": [ + "shoot" + ], + "animationStrings": { + "shoot": "attack_1" + }, + "drops": [], + "storyDrops": [], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 50 + }, + { + "name": "her", + "sprite": "gladiator_female", + "beingType": "human", + "sanity": 50, + "health": 285, + "attackPower": 30, + "mana": { + "maximum": 50, + "regen": 10 + }, + "baseResistanceTable": { + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 15 + }, + "baseDamageTable": { + "physical": 60 + }, + "attackStrings": [ + "cleave", + "stab" + ], + "animationStrings": { + "cleave": "attack_1", + "stab": "attack_2" + }, + "drops": [], + "storyDrops": [], + "goldDropRange": { + "minimum": 2500, + "maximum": 3000 + }, + "armorValue": 50 + } +] + diff --git a/src/lineage-json/enemy-route/enemy.json b/src/lineage-json/enemy-route/enemy.json new file mode 100644 index 0000000..cae19b3 --- /dev/null +++ b/src/lineage-json/enemy-route/enemy.json @@ -0,0 +1,1112 @@ +[ + { + "name": "training dummy", + "beingType": "block o wood", + "sprite": "training_dummy", + "sanity": null, + "healthRange": { + "minimum": 9999, + "maximum": 9999 + }, + "mana": { + "maximum": 30, + "regen": 0 + }, + "baseResistanceTable": {}, + "baseDamageTable": {}, + "attackStrings": [], + "animationStrings": {}, + "drops": [ + { + "item": "stick", + "itemType": "melee", + "chance": 1.0 + }, + { + "item": "big stick", + "itemType": "melee", + "chance": 0.85 + } + ], + "goldDropRange": { + "minimum": 0, + "maximum": 0 + } + }, + { + "name": "giant rat", + "beingType": "beast", + "sanity": null, + "sprite": "rat", + "healthRange": { + "minimum": 20, + "maximum": 35 + }, + "baseResistanceTable": { + "physical": 5, + "poison": 10, + "fire": -5 + }, + "baseDamageTable": { + "physical": 1 + }, + "mana": { + "maximum": 30, + "regen": 5 + }, + "attackStrings": [ + "bite", + "claw" + ], + "animationStrings": { + "bite": "attack_1", + "claw": "attack_2" + }, + "drops": [ + { + "item": "patch of hair", + "itemType": "junk", + "chance": 0.85 + }, + { + "item": "rat tail", + "itemType": "ingredient", + "chance": 0.5 + } + ], + "goldDropRange": { + "minimum": 5, + "maximum": 15 + } + }, + { + "name": "bat", + "sprite": "bat", + "beingType": "beast", + "sanity": null, + "healthRange": { + "minimum": 10, + "maximum": 20 + }, + "mana": { + "maximum": 30, + "regen": 5 + }, + "baseResistanceTable": { + "physical": 0, + "poison": 10, + "fire": -10, + "lightning": -20 + }, + "baseDamageTable": { + "physical": 1 + }, + "attackStrings": [ + "vampiric bite" + ], + "animationStrings": { + "vampiric bite": "attack_1" + }, + "drops": [ + { + "item": "bat wing", + "itemType": "ingredient", + "chance": 0.75 + }, + { + "item": "vampiric tooth", + "itemType": "junk", + "chance": 0.5 + } + ], + "goldDropRange": { + "minimum": 5, + "maximum": 20 + } + }, + { + "name": "skeleton", + "sprite": "skeleton", + "beingType": "undead", + "sanity": null, + "healthRange": { + "minimum": 35, + "maximum": 45 + }, + "mana": { + "maximum": 50, + "regen": 5 + }, + "baseResistanceTable": { + "physical": 15, + "poison": 30, + "cold": -5, + "holy": -50 + }, + "baseDamageTable": { + "physical": 3 + }, + "attackStrings": [ + "stab", + "cleave" + ], + "animationStrings": { + "stab": "attack_1", + "cleave": "attack_2" + }, + "drops": [ + { + "item": "bone", + "itemType": "ingredient", + "chance": 0.75 + }, + { + "item": "bone", + "itemType": "ingredient", + "chance": 0.75 + }, + { + "item": "dagger", + "itemType": "melee", + "chance": 0.10 + } + ], + "armorValue": 10, + "goldDropRange": { + "minimum": 10, + "maximum": 20 + } + }, + { + "name": "skeleton mage", + "sprite": "skeleton_mage", + "beingType": "undead", + "sanity": null, + "healthRange": { + "minimum": 30, + "maximum": 40 + }, + "mana": { + "maximum": 50, + "regen": 5 + }, + "baseResistanceTable": { + "physical": 15, + "poison": 30, + "cold": -5, + "holy": -50 + }, + "baseDamageTable": { + "magic": 4 + }, + "attackStrings": [ + "dark bolt" + ], + "animationStrings": { + "dark bolt": "attack_1" + }, + "drops": [ + { + "item": "bone", + "itemType": "ingredient", + "chance": 0.75 + } + ], + "goldDropRange": { + "minimum": 15, + "maximum": 25 + }, + "armorValue": 10 + }, + { + "name": "zombie", + "beingType": "undead", + "sprite": "zombie", + "sanity": null, + "healthRange": { + "minimum": 40, + "maximum": 55 + }, + "mana": { + "maximum": 50, + "regen": 5 + }, + "attackStrings": [ + "grab", + "zombie bite" + ], + "animationStrings": { + "grab": "attack_1", + "zombie bite": "attack_1" + }, + "baseResistanceTable": { + "physical": 15, + "poison": 75, + "fire": 15, + "cold": 15, + "holy": -50 + }, + "baseDamageTable": { + "physical": 4 + }, + "drops": [ + { + "item": "chunk of flesh", + "itemType": "junk", + "chance": 0.75 + }, + { + "item": "chunk of flesh", + "itemType": "junk", + "chance": 0.75 + } + ], + "goldDropRange": { + "minimum": 20, + "maximum": 25 + } + }, + { + "name": "ghost", + "sprite": "ghost", + "beingType": "undead", + "sanity": null, + "healthRange": { + "minimum": 20, + "maximum": 45 + }, + "mana": { + "maximum": 25, + "regen": 5 + }, + "baseResistanceTable": { + "physical": 50, + "poison": 50, + "cold": 25, + "holy": -50, + "magic": -20 + }, + "baseDamageTable": { + "raw": 2 + }, + "attackStrings": [ + "wail" + ], + "animationStrings": { + "wail": "attack_1" + }, + "drops": [ + { + "item": "ghostly residue", + "itemType": "ingredient", + "chance": 0.5 + } + ], + "goldDropRange": { + "minimum": 0, + "maximum": 0 + } + }, + { + "name": "goblin", + "sprite": "goblin", + "beingType": "demi-human", + "sanity": 20, + "healthRange": { + "minimum": 45, + "maximum": 65 + }, + "mana": { + "maximum": 50, + "regen": 5 + }, + "baseResistanceTable": { + "physical": 10, + "poison": -5, + "lightning": -5 + }, + "baseDamageTable": { + "physical": 4, + "poison": 2 + }, + "attackStrings": [ + "blunt cleave", + "heavy swing" + ], + "animationStrings": { + "blunt cleave": "attack_1", + "heavy swing": "attack_2" + }, + "drops": [ + { + "item": "mace", + "itemType": "melee", + "chance": 0.2 + } + ], + "goldDropRange": { + "minimum": 35, + "maximum": 65 + }, + "armorValue": 10 + }, + { + "name": "warg", + "sprite": "wolf_black", + "beingType": "beast", + "sanity": null, + "healthRange": { + "minimum": 40, + "maximum": 70 + }, + "mana": { + "maximum": 50, + "regen": 5 + }, + "baseResistanceTable": { + "physical": 5, + "cold": 10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 10 + }, + "attackStrings": [ + "bite", + "claw" + ], + "animationStrings": { + "bite": "attack_1", + "claw": "attack_2" + }, + "drops": [ + { + "item": "patch of hair", + "itemType": "junk", + "chance": 0.85 + }, + { + "item": "bone", + "itemType": "ingredient", + "chance": 0.75 + }, + { + "item": "bone", + "itemType": "ingredient", + "chance": 0.75 + } + ], + "goldDropRange": { + "minimum": 40, + "maximum": 55 + } + }, + { + "name": "kobold", + "sprite": "kobold", + "beingType": "demi-human", + "sanity": null, + "healthRange": { + "minimum": 50, + "maximum": 70 + }, + "mana": { + "maximum": 50, + "regen": 5 + }, + "baseResistanceTable": { + "lightning": 15, + "cold": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 5, + "lightning": 4 + }, + "attackStrings": [ + "stab", + "cleave", + "chop", + "charged pierce" + ], + "animationStrings": { + "stab": "attack_1", + "cleave": "attack_2", + "chop": "attack_3", + "charged pierce": "attack_4" + }, + "drops": [ + { + "item": "patch of hair", + "itemType": "junk", + "chance": 0.85 + }, + { + "item": "bone", + "itemType": "ingredient", + "chance": 0.75 + }, + { + "item": "bone", + "itemType": "ingredient", + "chance": 0.75 + }, + { + "item": "dagger", + "itemType": "melee", + "chance": 0.25 + } + ], + "goldDropRange": { + "minimum": 40, + "maximum": 55 + } + }, + { + "name": "bandit", + "sprite": "bandit_light", + "beingType": "human", + "sanity": 50, + "healthRange": { + "minimum": 50, + "maximum": 75 + }, + "mana": { + "maximum": 50, + "regen": 3 + }, + "baseResistanceTable": { + "physical": 15, + "poison": -15, + "cold": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 16 + }, + "attackStrings": [ + "stab", + "pocket sand", + "serrate" + ], + "animationStrings": { + "stab": "attack_1", + "pocket sand": "attack_1", + "serrate": "attack_1" + }, + "drops": [ + { + "item": "shortsword", + "itemType": "melee", + "chance": 0.15 + }, + { + "item": "cheap leather chestpiece", + "itemType": "bodyArmor", + "chance": 0.15 + }, + { + "item": "leather headgear", + "itemType": "helmet", + "chance": 0.15 + } + ], + "goldDropRange": { + "minimum": 30, + "maximum": 50 + }, + "armorValue": 15 + }, + { + "name": "bandit heavy", + "sprite": "bandit_heavy", + "beingType": "human", + "sanity": 25, + "healthRange": { + "minimum": 85, + "maximum": 125 + }, + "mana": { + "maximum": 30, + "regen": 2 + }, + "baseResistanceTable": { + "physical": 25, + "poison": -15, + "cold": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 14 + }, + "attackStrings": [ + "pommel strike", + "heavy swing" + ], + "animationStrings": { + "pommel strike": "attack_1", + "heavy swing": "attack_1" + }, + "drops": [ + { + "item": "longsword", + "itemType": "melee", + "chance": 0.25 + }, + { + "item": "cheap iron chestpiece", + "itemType": "bodyArmor", + "chance": 0.25 + } + ], + "goldDropRange": { + "minimum": 80, + "maximum": 100 + }, + "armorValue": 15 + }, + { + "name": "novice water mage", + "beingType": "human", + "sprite": "wizard_blue", + "sanity": 50, + "healthRange": { + "minimum": 50, + "maximum": 75 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 5 + }, + "baseDamageTable": { + "cold": 25 + }, + "attackStrings": [ + "bonk", + "frost" + ], + "animationStrings": { + "frost": "attack_1", + "bonk": "attack_2" + }, + "drops": [], + "goldDropRange": { + "minimum": 200, + "maximum": 250 + } + }, + { + "name": "novice fire mage", + "beingType": "human", + "sprite": "wizard_redblack", + "sanity": 50, + "healthRange": { + "minimum": 50, + "maximum": 75 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 5 + }, + "baseDamageTable": { + "fire": 25 + }, + "attackStrings": [ + "bonk", + "fire bolt" + ], + "animationStrings": { + "fire bolt": "attack_1", + "bonk": "attack_2" + }, + "drops": [], + "goldDropRange": { + "minimum": 200, + "maximum": 250 + } + }, + { + "name": "novice air mage", + "beingType": "human", + "sprite": "wizard_gray", + "sanity": 50, + "healthRange": { + "minimum": 50, + "maximum": 75 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 5 + }, + "baseDamageTable": { + "physical": 30 + }, + "attackStrings": [ + "bonk", + "gust" + ], + "animationStrings": { + "gust": "attack_1", + "bonk": "attack_2" + }, + "drops": [], + "goldDropRange": { + "minimum": 200, + "maximum": 250 + } + }, + { + "name": "novice earth mage", + "beingType": "human", + "sprite": "wizard_earthen", + "sanity": 50, + "healthRange": { + "minimum": 75, + "maximum": 100 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 10, + "poison": -15, + "cold": -10, + "fire": -10, + "holy": 5 + }, + "baseDamageTable": { + "physical": 20, + "raw": 10 + }, + "attackStrings": [ + "bonk", + "rock toss" + ], + "animationStrings": { + "rock toss": "attack_1", + "bonk": "attack_2" + }, + "drops": [], + "goldDropRange": { + "minimum": 200, + "maximum": 250 + } + }, + { + "name": "spider", + "beingType": "beast", + "sprite": "spider_demon_small", + "sanity": null, + "healthRange": { + "minimum": 25, + "maximum": 40 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "poison": 50, + "cold": -10, + "lightning": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 15 + }, + "attackStrings": [ + "bite" + ], + "animationStrings": { + "bite": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + } + }, + { + "name": "venomous spider", + "beingType": "beast", + "sprite": "spider_default_small", + "sanity": null, + "healthRange": { + "minimum": 25, + "maximum": 40 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "poison": 75, + "cold": -10, + "lightning": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 5, + "poison": 10 + }, + "attackStrings": [ + "venomous bite" + ], + "animationStrings": { + "venomous bite": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + } + }, + { + "name": "giant spider", + "beingType": "beast", + "sprite": "spider_demon", + "sanity": null, + "healthRange": { + "minimum": 100, + "maximum": 140 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "poison": 50, + "cold": -10, + "lightning": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 25 + }, + "attackStrings": [ + "bite" + ], + "animationStrings": { + "bite": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + } + }, + { + "name": "giant venomous spider", + "beingType": "beast", + "sprite": "spider_default", + "sanity": null, + "healthRange": { + "minimum": 100, + "maximum": 140 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "poison": 75, + "cold": -10, + "lightning": -10, + "fire": -10 + }, + "baseDamageTable": { + "physical": 10, + "poison": 20 + }, + "attackStrings": [ + "venomous bite" + ], + "animationStrings": { + "venomous bite": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + } + }, + { + "name": "satyr", + "beingType": "demi-human", + "sprite": "satyr", + "sanity": 100, + "healthRange": { + "minimum": 100, + "maximum": 140 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 25, + "cold": -15, + "poison": -5, + "holy": 50 + }, + "baseDamageTable": { + "physical": 10, + "poison": 15, + "magic": 5 + }, + "attackStrings": [ + "poisoned shot", + "maddening shot", + "shoot" + ], + "animationStrings": { + "poisoned shot": "attack_1", + "maddening shot": "attack_1", + "shoot": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + } + }, + { + "name": "harpy", + "beingType": "demi-human", + "sprite": "harpy", + "sanity": 100, + "healthRange": { + "minimum": 90, + "maximum": 130 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 25, + "cold": -15, + "lightning": -25 + }, + "baseDamageTable": { + "physical": 40 + }, + "attackStrings": [ + "gust", + "dive", + "claw" + ], + "animationStrings": { + "gust": "attack_1", + "dive": "attack_1", + "claw": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + } + }, + { + "name": "centaur", + "beingType": "demi-human", + "sprite": "centaur", + "sanity": 50, + "healthRange": { + "minimum": 120, + "maximum": 160 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 25, + "cold": -15, + "poison": -5, + "holy": 50 + }, + "baseDamageTable": { + "physical": 35 + }, + "attackStrings": [ + "stampede", + "chop" + ], + "animationStrings": { + "chop": "attack_1", + "stampede": "" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + } + }, + { + "name": "legionnaire", + "beingType": "human", + "sprite": "gladiator_shortsword", + "sanity": 50, + "healthRange": { + "minimum": 120, + "maximum": 160 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 25, + "cold": -10, + "poison": -10, + "lightning": -10 + }, + "baseDamageTable": { + "physical": 45 + }, + "attackStrings": [ + "serrate", + "stab" + ], + "animationStrings": { + "serrate": "attack_1", + "stab": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + }, + "armorValue": 40 + }, + { + "name": "gladiator", + "beingType": "human", + "sprite": "gladiator_longsword", + "sanity": 50, + "healthRange": { + "minimum": 120, + "maximum": 160 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 20, + "poison": -10 + }, + "baseDamageTable": { + "physical": 50 + }, + "attackStrings": [ + "stab", + "pierce" + ], + "animationStrings": { + "stab": "attack_1", + "pierce": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + }, + "armorValue": 20 + }, + { + "name": "lizardman", + "beingType": "human", + "sprite": "lizardman", + "sanity": 50, + "healthRange": { + "minimum": 120, + "maximum": 160 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 10, + "poison": 15, + "cold": 15, + "fire": 15 + }, + "baseDamageTable": { + "physical": 40, + "fire": 15 + }, + "attackStrings": [ + "stab", + "pierce" + ], + "animationStrings": { + "stab": "attack_1", + "pierce": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 40, + "maximum": 50 + }, + "armorValue": 20 + }, + { + "name": "mimic", + "beingType": "demi-human", + "sprite": "mimic", + "sanity": null, + "healthRange": { + "minimum": 50, + "maximum": 60 + }, + "mana": { + "maximum": 40, + "regen": 10 + }, + "baseResistanceTable": { + "physical": 10, + "poison": 15, + "cold": 15, + "fire": 15 + }, + "baseDamageTable": { + "physical": 10, + "raw": 5 + }, + "attackStrings": [ + "stomp" + ], + "animationStrings": { + "stomp": "attack_1" + }, + "drops": [], + "goldDropRange": { + "minimum": 100, + "maximum": 200 + }, + "armorValue": 20 + } +] + diff --git a/src/lineage-json/enemy-route/enemyAttacks.json b/src/lineage-json/enemy-route/enemyAttacks.json new file mode 100644 index 0000000..f13a5f9 --- /dev/null +++ b/src/lineage-json/enemy-route/enemyAttacks.json @@ -0,0 +1,854 @@ +[ + { + "name": "stab", + "manaCost": 5, + "targets": "single", + "baseHitChance": 1.0, + "damageTable": { + "physical": 4 + }, + "debuffNames": [ + { + "name": "bleed", + "chance": 0.20 + } + ] + }, + { + "name": "serrate", + "manaCost": 15, + "targets": "single", + "baseHitChance": 0.90, + "damageTable": { + "physical": 4 + }, + "debuffNames": [ + { + "name": "hemmorage", + "chance": 0.25 + } + ] + }, + { + "name": "bite", + "manaCost": 8, + "targets": "single", + "baseHitChance": 1.0, + "damageTable": { + "physical": 4 + }, + "debuffNames": [ + { + "name": "diseased", + "chance": 0.10 + } + ] + }, + { + "name": "zombie bite", + "manaCost": 15, + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "physical": 3, + "poison": 4 + }, + "debuffNames": [ + { + "name": "diseased", + "chance": 0.50 + } + ] + }, + { + "name": "vampiric bite", + "manaCost": 10, + "targets": "single", + "baseHitChance": 0.90, + "damageTable": { + "physical": 4, + "poison": 1 + }, + "debuffNames": [ + { + "name": "lifesteal", + "chance": 0.65 + } + ] + }, + { + "name": "venomous bite", + "manaCost": 10, + "targets": "single", + "baseHitChance": 0.90, + "damageTable": { + "physical": 2, + "poison": 3 + }, + "debuffNames": [ + { + "name": "poison", + "chance": 0.65 + } + ] + }, + { + "name": "torch stab", + "manaCost": 5, + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "physical": 2, + "fire": 3 + }, + "debuffNames": [ + { + "name": "burn", + "chance": 0.75 + } + ] + }, + { + "name": "cleave", + "manaCost": 10, + "targets": "dual", + "baseHitChance": 0.95, + "damageTable": { + "physical": 5 + }, + "debuffNames": [ + { + "name": "bleed", + "chance": 0.35 + } + ] + }, + { + "name": "blunt cleave", + "manaCost": 10, + "targets": "dual", + "baseHitChance": 0.95, + "damageTable": { + "physical": 3, + "raw": 2 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.35 + } + ] + }, + { + "name": "kick", + "manaCost": 5, + "targets": "single", + "baseHitChance": 0.90, + "damageTable": { + "physical": 5, + "raw": 2 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.15 + } + ] + }, + { + "name": "rock throw", + "manaCost": 5, + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "physical": 5, + "raw": 2 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.15 + } + ] + }, + { + "name": "rock spike", + "manaCost": 20, + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "physical": 5, + "raw": 4 + }, + "debuffNames": [ + { + "name": "bleed", + "chance": 0.65 + } + ] + }, + { + "name": "burial", + "manaCost": 75, + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "physical": 5, + "raw": 20 + }, + "debuffNames": [ + { + "name": "heavy stun", + "chance": 0.75 + } + ] + }, + { + "name": "grab", + "manaCost": 20, + "targets": "single", + "baseHitChance": 0.75, + "damageTable": { + "physical": 4 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.35 + } + ] + }, + { + "name": "tackle", + "manaCost": 20, + "targets": "single", + "baseHitChance": 0.75, + "damageTable": { + "physical": 3, + "raw": 2 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.8 + } + ] + }, + { + "name": "stomp", + "manaCost": 10, + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "physical": 2, + "raw": 4 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.1 + } + ] + }, + { + "name": "bolder throw", + "manaCost": 25, + "targets": "area", + "baseHitChance": 0.85, + "damageTable": { + "physical": 2, + "raw": 4 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.25 + } + ] + }, + { + "name": "pocket sand", + "manaCost": 5, + "targets": "single", + "baseHitChance": 0.90, + "damageTable": { + "raw": 1 + }, + "debuffNames": [ + { + "name": "blind", + "chance": 0.75 + } + ] + }, + { + "name": "poison spray", + "manaCost": 10, + "targets": "single", + "baseHitChance": 1.0, + "damageTable": { + "poison": 5 + }, + "debuffNames": [ + { + "name": "poison", + "chance": 0.85 + } + ] + }, + { + "name": "poisoned shot", + "manaCost": 15, + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "physical": 5, + "poison": 3 + }, + "debuffNames": [ + { + "name": "poison", + "chance": 0.5 + } + ] + }, + { + "name": "maddening shot", + "manaCost": 20, + "targets": "single", + "baseHitChance": 0.95, + "damageTable": {}, + "sanityDamage": 5 + }, + { + "name": "wail", + "manaCost": 20, + "targets": "area", + "baseHitChance": 0.85, + "damageTable": {}, + "sanityDamage": 5, + "debuffNames": [ + { + "name": "fear", + "chance": 0.50 + } + ] + }, + { + "name": "shoot", + "manaCost": 15, + "targets": "single", + "baseHitChance": 0.75, + "damageTable": { + "physical": 11 + } + }, + { + "name": "head slam", + "manaCost": 10, + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "raw": 2, + "physical": 4 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.10 + } + ] + }, + { + "name": "stampede", + "manaCost": 15, + "targets": "area", + "baseHitChance": 0.85, + "damageTable": { + "raw": 2, + "physical": 4 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.50 + } + ] + }, + { + "name": "heavy swing", + "manaCost": 20, + "targets": "single", + "baseHitChance": 0.80, + "damageTable": { + "physical": 10 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.20 + } + ] + }, + { + "name": "flurry", + "manaCost": 20, + "targets": "single", + "baseHitChance": 0.65, + "hitsPerTurn": 4, + "damageTable": { + "physical": 5, + "raw": 2 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.15 + } + ] + }, + { + "name": "chop", + "manaCost": 5, + "targets": "single", + "baseHitChance": 1.0, + "damageTable": { + "physical": 6 + }, + "debuffNames": [ + { + "name": "bleed", + "chance": 0.25 + } + ] + }, + { + "name": "headbutt", + "manaCost": 10, + "targets": "single", + "baseHitChance": 0.85, + "damageTable": { + "raw": 2, + "physical": 4 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.20 + } + ] + }, + { + "name": "pommel strike", + "manaCost": 10, + "targets": "single", + "baseHitChance": 0.85, + "damageTable": { + "raw": 2, + "physical": 4 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.20 + } + ] + }, + { + "name": "punch", + "manaCost": 5, + "targets": "single", + "baseHitChance": 1.0, + "damageTable": { + "physical": 5 + } + }, + { + "name": "call backup", + "manaCost": 30, + "damageTable": {}, + "summonNames": [ + "bandit" + ] + }, + { + "name": "raise skeleton", + "damageTable": {}, + "manaCost": 35, + "summonNames": [ + "skeleton" + ] + }, + { + "name": "teeth", + "manaCost": 15, + "targets": "dual", + "damageTable": { + "raw": 6, + "physical": 2 + } + }, + { + "name": "frenzy", + "damageTable": {}, + "manaCost": 10, + "buffNames": [ + "frenzy" + ] + }, + { + "name": "chug beer", + "damageTable": {}, + "manaCost": 10, + "buffNames": [ + "frenzy" + ] + }, + { + "name": "spark", + "manaCost": 25, + "targets": "single", + "baseHitChance": 0.85, + "damageTable": { + "lightning": 6 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.5 + } + ] + }, + { + "name": "bonk", + "manaCost": 5, + "targets": "single", + "baseHitChance": 1.0, + "damageTable": { + "physical": 3 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.1 + } + ] + }, + { + "name": "pluck eye", + "targets": "single", + "baseHitChance": 0.90, + "damageTable": { + "physical": 3 + }, + "debuffNames": [ + { + "name": "blind", + "chance": 0.9 + } + ] + }, + { + "name": "scratch", + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "physical": 6 + } + }, + { + "name": "pulse", + "manaCost": 10, + "target": "single", + "baseHitChance": 0.90, + "damageTable": { + "magic": 6 + } + }, + { + "name": "dark burst", + "manaCost": 15, + "target": "area", + "baseHitChance": 1.0, + "damageTable": { + "poison": 2, + "physical": 1 + } + }, + { + "name": "dark bolt", + "manaCost": 20, + "target": "single", + "baseHitChance": 1.0, + "damageTable": { + "poison": 3, + "physical": 3 + } + }, + { + "name": "fire bolt", + "manaCost": 20, + "target": "single", + "baseHitChance": 1.0, + "damageTable": { + "fire": 10 + }, + "debuffNames": [ + { + "name": "burn", + "chance": 0.25 + } + ] + }, + { + "name": "frost", + "manaCost": 20, + "target": "single", + "baseHitChance": 1.0, + "damageTable": { + "cold": 6 + }, + "debuffNames": [ + { + "name": "chill", + "chance": 0.5 + } + ] + }, + { + "name": "gust", + "manaCost": 10, + "target": "single", + "baseHitChance": 1.0, + "damageTable": { + "physical": 6 + } + }, + { + "name": "rock toss", + "manaCost": 15, + "baseHitChance": 1.0, + "damageTable": { + "physical": 4, + "raw": 2 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.10 + } + ] + }, + { + "name": "life drain", + "manaCost": 25, + "targets": "single", + "baseHitChance": 0.90, + "damageTable": { + "raw": 5 + }, + "debuffNames": [ + { + "name": "lifesteal", + "chance": 0.5 + }, + { + "name": "dulled mind", + "chance": 0.35 + } + ] + }, + { + "name": "terrorize", + "manaCost": 30, + "targets": "area", + "baseHitChance": 0.85, + "sanityDamage": 10, + "damageTable": {}, + "debuffNames": [ + { + "name": "fear", + "chance": 0.75 + } + ] + }, + { + "name": "soul strike", + "manaCost": 20, + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "raw": 6 + }, + "sanityDamage": 5 + }, + { + "name": "death blade", + "manaCost": 25, + "targets": "single", + "baseHitChance": 0.90, + "damageTable": { + "raw": 12 + }, + "debuffNames": [ + { + "name": "hemmorage", + "chance": 0.50 + } + ] + }, + { + "name": "corrupted cleave", + "manaCost": 35, + "targets": "dual", + "baseHitChance": 0.85, + "damageTable": { + "raw": 15 + }, + "debuffNames": [ + { + "name": "necrotic wound", + "chance": 0.45 + } + ] + }, + { + "name": "death bolt", + "manaCost": 30, + "targets": "single", + "baseHitChance": 1.0, + "damageTable": { + "raw": 15 + }, + "debuffNames": [ + { + "name": "necrotic wound", + "chance": 0.65 + } + ] + }, + { + "name": "soul rip", + "manaCost": 45, + "targets": "single", + "baseHitChance": 0.85, + "sanityDamage": 15, + "damageTable": { + "raw": 20 + }, + "debuffNames": [ + { + "name": "dulled mind", + "chance": 0.75 + } + ] + }, + { + "name": "curse", + "manaCost": 40, + "targets": "single", + "baseHitChance": 0.90, + "damageTable": {}, + "debuffNames": [ + { + "name": "death mark", + "chance": 1.0 + } + ] + }, + { + "name": "claw", + "manaCost": 10, + "targets": "single", + "baseHitChance": 0.95, + "damageTable": { + "physical": 4 + }, + "debuffNames": [ + { + "name": "bleed", + "chance": 0.25 + } + ] + }, + { + "name": "dive", + "manaCost": 15, + "targets": "single", + "baseHitChance": 0.80, + "damageTable": { + "physical": 10 + } + }, + { + "name": "gust", + "manaCost": 15, + "targets": "area", + "baseHitChance": 0.95, + "damageTable": { + "physical": 6 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.1 + } + ] + }, + { + "name": "fire breath", + "manaCost": 20, + "targets": "area", + "baseHitChance": 0.90, + "damageTable": { + "fire": 6 + }, + "debuffNames": [ + { + "name": "burn", + "chance": 0.4 + } + ] + }, + { + "name": "wing buffet", + "manaCost": 25, + "targets": "area", + "baseHitChance": 0.90, + "damageTable": { + "physical": 8 + }, + "debuffNames": [ + { + "name": "stun", + "chance": 0.25 + } + ] + }, + { + "name": "pierce", + "manaCost": 10, + "targets": "single", + "baseHitChance": 0.85, + "damageTable": { + "physical": 8 + } + }, + { + "name": "charged pierce", + "manaCost": 35, + "targets": "single", + "baseHitChance": 0.85, + "damageTable": { + "lightning": 8, + "physical": 8 + }, + "debuffNames": [ + { + "name": "shocked", + "chance": 0.5 + } + ] + } +] + diff --git a/src/lineage-json/item-route/arrows.json b/src/lineage-json/item-route/arrows.json new file mode 100644 index 0000000..fbc65e5 --- /dev/null +++ b/src/lineage-json/item-route/arrows.json @@ -0,0 +1,75 @@ +[ + { + "name": "iron arrow", + "baseValue": 2, + "slot": "quiver", + "icon": "Arrow", + "stackable": true, + "stats": { + "physicalDamage": 1.0 + } + }, + { + "name": "steel arrow", + "baseValue": 4, + "slot": "quiver", + "icon": "Arrow", + "stats": { + "physicalDamage": 1.5 + }, + "requirements": { + "dexterity": 5 + } + }, + { + "name": "refined arrow", + "baseValue": 8, + "slot": "quiver", + "icon": "Arrow", + "stats": { + "physicalDamage": 2.5 + }, + "requirements": { + "dexterity": 10 + } + }, + { + "name": "barbed arrow", + "baseValue": 15, + "slot": "quiver", + "icon": "Arrow", + "stats": { + "physicalDamage": 3.5 + }, + "requirements": { + "dexterity": 15 + } + }, + { + "name": "great arrow", + "baseValue": 20, + "slot": "quiver", + "icon": "Arrow", + "stats": { + "physicalDamage": 4.0 + }, + "requirements": { + "strength": 10, + "dexterity": 15 + } + }, + { + "name": "explosive arrow", + "baseValue": 40, + "slot": "quiver", + "icon": "Arrow", + "stats": { + "physicalDamage": 5.0 + }, + "requirements": { + "strength": 10, + "dexterity": 25 + } + } +] + diff --git a/src/lineage-json/item-route/artifacts.json b/src/lineage-json/item-route/artifacts.json new file mode 100644 index 0000000..8d18945 --- /dev/null +++ b/src/lineage-json/item-route/artifacts.json @@ -0,0 +1,8 @@ +[ + { + "name": "golden goblet", + "icon": "Goblet", + "baseValue": 5000 + } +] + diff --git a/src/lineage-json/item-route/bodyArmor.json b/src/lineage-json/item-route/bodyArmor.json new file mode 100644 index 0000000..d44c044 --- /dev/null +++ b/src/lineage-json/item-route/bodyArmor.json @@ -0,0 +1,122 @@ +[ + { + "name": "cheap leather chestpiece", + "baseValue": 400, + "icon": "Leather_Armor", + "slot": "body", + "stats": { + "armor": 7.0 + } + }, + { + "name": "leather chestpiece", + "baseValue": 800, + "icon": "Leather_Armor", + "slot": "body", + "stats": { + "armor": 10.0 + } + }, + { + "name": "cheap iron chestpiece", + "baseValue": 1500, + "icon": "Iron_Armor", + "slot": "body", + "stats": { + "armor": 15.0 + } + }, + { + "name": "iron chestpiece", + "baseValue": 2000, + "icon": "Iron_Armor", + "slot": "body", + "stats": { + "armor": 20.0 + }, + "requirements": { + "strength": 5 + } + }, + { + "name": "steel chestpiece", + "baseValue": 5000, + "icon": "Iron_Armor", + "slot": "body", + "stats": { + "armor": 25.0 + }, + "requirements": { + "strength": 10 + } + }, + { + "name": "barbarian king armor", + "baseValue": 10000, + "icon": "Leather_Armor", + "slot": "body", + "stats": { + "armor": 20.0, + "health": 50 + }, + "requirements": { + "strength": 15 + } + }, + { + "name": "knight's breastplate", + "baseValue": 15000, + "icon": "Iron_Armor", + "slot": "body", + "stats": { + "armor": 30.0 + }, + "requirements": { + "strength": 10, + "intelligence": 5 + } + }, + { + "name": "crusader's breastplate", + "baseValue": 75000, + "icon": "Iron_Armor", + "slot": "body", + "stats": { + "armor": 30.0, + "health": 100, + "mana": 100 + }, + "requirements": { + "strength": 25, + "intelligence": 10 + } + }, + { + "name": "soldier of fortune", + "baseValue": 225000, + "icon": "Iron_Armor", + "slot": "body", + "stats": { + "armor": 58.0 + }, + "requirements": { + "strength": 35 + } + }, + { + "name": "kingsman chestpiece", + "baseValue": 550000, + "icon": "Iron_Armor", + "slot": "body", + "stats": { + "armor": 50.0, + "health": 250, + "mana": 100 + }, + "requirements": { + "strength": 30, + "intelligence": 10 + } + } +] + diff --git a/src/lineage-json/item-route/bows.json b/src/lineage-json/item-route/bows.json new file mode 100644 index 0000000..ec796e3 --- /dev/null +++ b/src/lineage-json/item-route/bows.json @@ -0,0 +1,147 @@ +[ + { + "name": "short bow", + "baseValue": 300, + "slot": "two-hand", + "attacks": [ + "shoot", + "rooting shot" + ], + "icon": "Bow", + "stats": { + "physicalDamage": 5.5 + } + }, + { + "name": "recurve bow", + "baseValue": 500, + "slot": "two-hand", + "attacks": [ + "shoot", + "rapid shot" + ], + "icon": "Bow", + "stats": { + "physicalDamage": 7.5 + }, + "requirements": { + "dexterity": 5 + } + }, + { + "name": "long bow", + "baseValue": 1000, + "slot": "two-hand", + "attacks": [ + "shoot", + "careful shot" + ], + "icon": "Bow", + "stats": { + "physicalDamage": 10.5 + }, + "requirements": { + "dexterity": 7 + } + }, + { + "name": "serpent bow", + "baseValue": 5000, + "slot": "two-hand", + "attacks": [ + "shoot", + "poison shot" + ], + "icon": "Bow", + "stats": { + "physicalDamage": 12.5 + }, + "requirements": { + "dexterity": 12 + } + }, + { + "name": "great bow", + "baseValue": 9500, + "slot": "two-hand", + "attacks": [ + "shoot", + "overdraw" + ], + "icon": "Great_Bow", + "stats": { + "physicalDamage": 19 + }, + "requirements": { + "strength": 10, + "dexterity": 10 + } + }, + { + "name": "great serpent bow", + "baseValue": 35000, + "slot": "two-hand", + "attacks": [ + "shoot", + "poison shot" + ], + "icon": "Great_Bow", + "stats": { + "physicalDamage": 22 + }, + "requirements": { + "strength": 10, + "dexterity": 15 + } + }, + { + "name": "harp bow", + "baseValue": 75000, + "slot": "two-hand", + "attacks": [ + "shoot", + "rapid shot" + ], + "icon": "Harp_Bow", + "stats": { + "physicalDamage": 25 + }, + "requirements": { + "dexterity": 20 + } + }, + { + "name": "black bow", + "baseValue": 150000, + "slot": "two-hand", + "attacks": [ + "shoot", + "seeking shot" + ], + "icon": "Black_Bow", + "stats": { + "physicalDamage": 29 + }, + "requirements": { + "dexterity": 30 + } + }, + { + "name": "hunter of bael", + "baseValue": 450000, + "slot": "two-hand", + "attacks": [ + "shoot", + "overdraw" + ], + "icon": "Great_Bow", + "stats": { + "physicalDamage": 32 + }, + "requirements": { + "strength": 20, + "dexterity": 20 + } + } +] + diff --git a/src/lineage-json/item-route/foci.json b/src/lineage-json/item-route/foci.json new file mode 100644 index 0000000..ac50fc8 --- /dev/null +++ b/src/lineage-json/item-route/foci.json @@ -0,0 +1,192 @@ +[ + { + "name": "cheap focus", + "icon": "Focus_1", + "slot": "off-hand", + "stats": { + "mana": 25, + "magicDamage": 2 + }, + "requirements": { + "intelligence": 5 + }, + "baseValue": 500 + }, + { + "name": "basic focus", + "icon": "Focus_1", + "slot": "off-hand", + "stats": { + "mana": 50, + "magicDamage": 5 + }, + "requirements": { + "intelligence": 10 + }, + "baseValue": 2500 + }, + { + "name": "cracked focus", + "icon": "Focus_1", + "slot": "off-hand", + "stats": { + "magicDamage": 15 + }, + "requirements": { + "intelligence": 15 + }, + "baseValue": 3000 + }, + { + "name": "buzzing focus", + "icon": "Focus_1", + "slot": "off-hand", + "stats": { + "magicDamage": 10, + "manaRegen": 1 + }, + "requirements": { + "intelligence": 15 + }, + "baseValue": 3500 + }, + { + "name": "apprentice's focus", + "icon": "Focus_2", + "slot": "off-hand", + "stats": { + "mana": 75, + "magicDamage": 12, + "manaRegen": 1 + }, + "requirements": { + "intelligence": 18 + }, + "baseValue": 8000 + }, + { + "name": "crystalline focus", + "icon": "Focus_2", + "slot": "off-hand", + "stats": { + "mana": 100, + "magicDamage": 15 + }, + "requirements": { + "intelligence": 20 + }, + "baseValue": 12000 + }, + { + "name": "resonating focus", + "icon": "Focus_2", + "slot": "off-hand", + "stats": { + "magicDamage": 18, + "manaRegen": 2 + }, + "requirements": { + "intelligence": 22 + }, + "baseValue": 15000 + }, + { + "name": "mage's focus", + "icon": "Focus_2", + "slot": "off-hand", + "stats": { + "mana": 150, + "magicDamage": 20, + "manaRegen": 2 + }, + "requirements": { + "intelligence": 25 + }, + "baseValue": 25000 + }, + { + "name": "arcane focus", + "icon": "Focus_3", + "slot": "off-hand", + "stats": { + "mana": 200, + "magicDamage": 25, + "manaRegen": 3 + }, + "requirements": { + "intelligence": 30 + }, + "baseValue": 45000 + }, + { + "name": "enchanted focus", + "icon": "Focus_3", + "slot": "off-hand", + "stats": { + "mana": 250, + "magicDamage": 30, + "manaRegen": 3 + }, + "requirements": { + "intelligence": 35 + }, + "baseValue": 75000 + }, + { + "name": "sorcerer's focus", + "icon": "Focus_3", + "slot": "off-hand", + "stats": { + "mana": 300, + "magicDamage": 35, + "manaRegen": 4 + }, + "requirements": { + "intelligence": 40 + }, + "baseValue": 120000 + }, + { + "name": "master's focus", + "icon": "Focus_3", + "slot": "off-hand", + "stats": { + "mana": 400, + "magicDamage": 40, + "manaRegen": 5 + }, + "requirements": { + "intelligence": 45 + }, + "baseValue": 180000 + }, + { + "name": "archmagus focus", + "icon": "Focus_4", + "slot": "off-hand", + "stats": { + "mana": 500, + "magicDamage": 45, + "manaRegen": 6 + }, + "requirements": { + "intelligence": 50 + }, + "baseValue": 250000 + }, + { + "name": "legendary focus", + "icon": "Focus_4", + "slot": "off-hand", + "stats": { + "mana": 600, + "magicDamage": 50, + "manaRegen": 7 + }, + "requirements": { + "intelligence": 55 + }, + "baseValue": 350000 + } +] + diff --git a/src/lineage-json/item-route/hats.json b/src/lineage-json/item-route/hats.json new file mode 100644 index 0000000..e29d969 --- /dev/null +++ b/src/lineage-json/item-route/hats.json @@ -0,0 +1,129 @@ +[ + { + "name": "apprentice hood", + "baseValue": 1000, + "icon": "Wizard_Hat", + "slot": "head", + "stats": { + "armor": 3, + "manaRegen": 1 + }, + "requirements": { + "intelligence": 5 + } + }, + { + "name": "adept hood", + "baseValue": 2500, + "icon": "Wizard_Hat", + "slot": "head", + "stats": { + "armor": 3, + "mana": 20, + "manaRegen": 1 + }, + "requirements": { + "intelligence": 5 + } + }, + { + "name": "mage hood", + "baseValue": 2500, + "icon": "Wizard_Hat", + "slot": "head", + "stats": { + "armor": 5, + "mana": 100 + }, + "requirements": { + "intelligence": 8 + } + }, + { + "name": "gorgeous hood", + "baseValue": 4500, + "icon": "Wizard_Hat", + "slot": "head", + "stats": { + "armor": 5, + "mana": 30, + "manaRegen": 2 + }, + "requirements": { + "intelligence": 10 + } + }, + { + "name": "expert hood", + "baseValue": 10000, + "icon": "Wizard_Hat", + "slot": "head", + "stats": { + "armor": 5, + "mana": 50, + "manaRegen": 4 + }, + "requirements": { + "intelligence": 15 + } + }, + { + "name": "rouge magi's hood", + "baseValue": 17500, + "icon": "Wizard_Hat", + "slot": "head", + "stats": { + "armor": 8, + "manaRegen": 8 + }, + "requirements": { + "strength": 5, + "intelligence": 12 + } + }, + { + "name": "war mage's cap", + "baseValue": 65000, + "icon": "Wizard_Hat", + "slot": "head", + "stats": { + "armor": 15, + "mana": 40, + "manaRegen": 5, + "health": 40 + }, + "requirements": { + "strength": 10, + "intelligence": 15 + } + }, + { + "name": "ancient veil", + "baseValue": 100000, + "icon": "Wizard_Hat", + "slot": "head", + "stats": { + "mana": 200, + "manaRegen": 10 + }, + "requirements": { + "intelligence": 25 + } + }, + { + "name": "arch-mage's veil", + "baseValue": 350000, + "icon": "Wizard_Hat", + "slot": "head", + "stats": { + "armor": 8, + "mana": 150, + "health": 50, + "manaRegen": 10 + }, + "requirements": { + "intelligence": 35 + } + } +] + diff --git a/src/lineage-json/item-route/helmets.json b/src/lineage-json/item-route/helmets.json new file mode 100644 index 0000000..ec0f03f --- /dev/null +++ b/src/lineage-json/item-route/helmets.json @@ -0,0 +1,121 @@ +[ + { + "name": "feather", + "baseValue": 100, + "icon": "Feather", + "slot": "head", + "stats": { + "armor": 1.0 + } + }, + { + "name": "leather headgear", + "baseValue": 500, + "icon": "Leather_Helmet", + "slot": "head", + "stats": { + "armor": 3.0 + } + }, + { + "name": "cheap iron helmet", + "baseValue": 1500, + "icon": "Iron_Helmet", + "slot": "head", + "stats": { + "armor": 5.0 + } + }, + { + "name": "iron helmet", + "baseValue": 2000, + "icon": "Iron_Helmet", + "slot": "head", + "stats": { + "armor": 8.0 + }, + "requirements": { + "strength": 5 + } + }, + { + "name": "steel helmet", + "baseValue": 5000, + "icon": "Iron_Helmet", + "slot": "head", + "stats": { + "armor": 10.0 + }, + "requirements": { + "strength": 8 + } + }, + { + "name": "barbarian king headgear", + "baseValue": 10000, + "icon": "Leather_Helmet", + "slot": "head", + "stats": { + "armor": 8.0, + "health": 50 + }, + "requirements": { + "strength": 12 + } + }, + { + "name": "knight's helm", + "baseValue": 15000, + "icon": "Iron_Helmet", + "slot": "head", + "stats": { + "armor": 12.0 + }, + "requirements": { + "strength": 15 + } + }, + { + "name": "crusader's helm", + "baseValue": 75000, + "icon": "Helm", + "slot": "head", + "stats": { + "armor": 15.0, + "health": 100, + "mana": 100 + }, + "requirements": { + "strength": 18, + "intelligence": 10 + } + }, + { + "name": "helm of fortune", + "baseValue": 225000, + "icon": "Helm", + "slot": "head", + "stats": { + "armor": 25.0 + }, + "requirements": { + "strength": 30 + } + }, + { + "name": "kingsman helm", + "baseValue": 550000, + "icon": "Helm", + "slot": "head", + "stats": { + "armor": 22.0, + "health": 250, + "mana": 100 + }, + "requirements": { + "strength": 25, + "intelligence": 10 + } + } +] + diff --git a/src/lineage-json/item-route/ingredients.json b/src/lineage-json/item-route/ingredients.json new file mode 100644 index 0000000..2e70b74 --- /dev/null +++ b/src/lineage-json/item-route/ingredients.json @@ -0,0 +1,18 @@ +[ + { + "name": "bat wing", + "icon": "Bat_Wing", + "baseValue": 10 + }, + { + "name": "rat tail", + "icon": "Rat_Tail", + "baseValue": 15 + }, + { + "name": "ghostly residue", + "icon": "Slime_Gel", + "baseValue": 100 + } +] + diff --git a/src/lineage-json/item-route/junk.json b/src/lineage-json/item-route/junk.json new file mode 100644 index 0000000..62a230b --- /dev/null +++ b/src/lineage-json/item-route/junk.json @@ -0,0 +1,18 @@ +[ + { + "name": "patch of fur", + "icon": "Patch_of_Fur", + "baseValue": 5 + }, + { + "name": "vampiric tooth", + "icon": "Fang", + "baseValue": 25 + }, + { + "name": "chunk of flesh", + "icon": "Chunk_of_Flesh", + "baseValue": 40 + } +] + diff --git a/src/lineage-json/item-route/melee.json b/src/lineage-json/item-route/melee.json new file mode 100644 index 0000000..0959870 --- /dev/null +++ b/src/lineage-json/item-route/melee.json @@ -0,0 +1,308 @@ +[ + { + "name": "stick", + "baseValue": 50, + "slot": "one-hand", + "attacks": [ + "hit" + ], + "icon": "Wood_Log", + "stats": { + "physicalDamage": 2.5 + } + }, + { + "name": "big stick", + "baseValue": 75, + "slot": "two-hand", + "attacks": [ + "hit" + ], + "icon": "Wood_Log", + "stats": { + "physicalDamage": 4.5 + }, + "requirements": { + "strength": 5 + } + }, + { + "name": "torch", + "baseValue": 300, + "slot": "one-hand", + "attacks": [ + "torch stab" + ], + "icon": "Torch", + "stats": { + "physicalDamage": 1.5, + "fireDamage": 1.5 + } + }, + { + "name": "dagger", + "baseValue": 400, + "slot": "one-hand", + "attacks": [ + "stab" + ], + "icon": "Knife", + "stats": { + "physicalDamage": 4.5 + } + }, + { + "name": "shortsword", + "baseValue": 1200, + "slot": "one-hand", + "attacks": [ + "slash" + ], + "icon": "Iron_Sword", + "stats": { + "physicalDamage": 7.5 + } + }, + { + "name": "mace", + "baseValue": 1100, + "slot": "one-hand", + "attacks": [ + "crushing blow" + ], + "icon": "Hammer", + "stats": { + "physicalDamage": 6.5 + }, + "requirements": { + "strength": 8 + } + }, + { + "name": "rat-smasher", + "baseValue": 2000, + "slot": "two-hand", + "attacks": [ + "crushing blow" + ], + "icon": "Hammer", + "stats": { + "physicalDamage": 10.5 + }, + "requirements": { + "strength": 10 + } + }, + { + "name": "longsword", + "baseValue": 10000, + "slot": "one-hand", + "attacks": [ + "slash" + ], + "icon": "Iron_Sword", + "stats": { + "physicalDamage": 10.0 + }, + "requirements": { + "strength": 8, + "dexterity": 5 + } + }, + { + "name": "bastard sword", + "baseValue": 15000, + "slot": "two-hand", + "attacks": [ + "slash", + "cleave" + ], + "icon": "Iron_Sword", + "stats": { + "physicalDamage": 20.0 + }, + "requirements": { + "strength": 12 + } + }, + { + "name": "elegant blade", + "baseValue": 20000, + "slot": "one-hand", + "attacks": [ + "slash" + ], + "icon": "Silver_Sword", + "stats": { + "physicalDamage": 16.0 + }, + "requirements": { + "strength": 10, + "dexterity": 8 + } + }, + { + "name": "axe", + "baseValue": 25000, + "slot": "one-hand", + "attacks": [ + "slash", + "cleave" + ], + "icon": "Axe", + "stats": { + "physicalDamage": 20.0 + }, + "requirements": { + "strength": 15 + } + }, + { + "name": "zweihänder", + "baseValue": 34000, + "slot": "two-hand", + "attacks": [ + "slash", + "cleave" + ], + "icon": "Iron_Sword", + "stats": { + "physicalDamage": 29.5 + }, + "requirements": { + "strength": 20 + } + }, + { + "name": "grotesque dagger", + "baseValue": 38000, + "slot": "one-hand", + "attacks": [ + "slash", + "serrate" + ], + "icon": "Knife", + "stats": { + "physicalDamage": 18.0, + "poisonDamage": 4.0 + }, + "requirements": { + "strength": 10, + "dexterity": 20 + } + }, + { + "name": "greataxe", + "baseValue": 65000, + "slot": "two-hand", + "attacks": [ + "hack" + ], + "icon": "Axe", + "stats": { + "physicalDamage": 33.5 + }, + "requirements": { + "strength": 28 + } + }, + { + "name": "greatsword", + "baseValue": 65000, + "slot": "two-hand", + "attacks": [ + "slash", + "cleave" + ], + "icon": "Iron_Sword", + "stats": { + "physicalDamage": 37.5 + }, + "requirements": { + "strength": 30 + } + }, + { + "name": "royal longsword", + "baseValue": 85000, + "slot": "two-hand", + "attacks": [ + "slash", + "cleave" + ], + "icon": "Golden_Sword", + "stats": { + "physicalDamage": 30.0, + "magicDamage": 5.0 + }, + "requirements": { + "strength": 25, + "dexterity": 15 + } + }, + { + "name": "barbarian war axe", + "baseValue": 150000, + "slot": "one-hand", + "attacks": [ + "hack" + ], + "icon": "Axe", + "stats": { + "physicalDamage": 30.5 + }, + "requirements": { + "strength": 33 + } + }, + { + "name": "knight's greatsword", + "baseValue": 225000, + "slot": "two-hand", + "attacks": [ + "slash", + "cleave" + ], + "icon": "Golden_Sword", + "stats": { + "physicalDamage": 35.5 + }, + "requirements": { + "strength": 32 + } + }, + { + "name": "crusader's longsword", + "baseValue": 310000, + "slot": "one-hand", + "attacks": [ + "slash", + "cleave" + ], + "icon": "Golden_Sword", + "stats": { + "physicalDamage": 27.0, + "holyDamage": 6.0 + }, + "requirements": { + "strength": 30, + "dexterity": 15 + } + }, + { + "name": "crusader's war hammer", + "baseValue": 310000, + "slot": "two-hand", + "attacks": [ + "crushing blow" + ], + "icon": "Golden_Hammer", + "stats": { + "physicalDamage": 45.0 + }, + "requirements": { + "strength": 45 + } + } +] + diff --git a/src/lineage-json/item-route/poison.json b/src/lineage-json/item-route/poison.json new file mode 100644 index 0000000..ec7382a --- /dev/null +++ b/src/lineage-json/item-route/poison.json @@ -0,0 +1,120 @@ +[ + { + "name": "basic wounding poison", + "icon": "Green_Potion", + "effect": { + "stat": "health", + "amount": { + "min": 10, + "max": 30 + }, + "isPoison": true + }, + "baseValue": 500 + }, + { + "name": "strong wounding poison", + "icon": "Green_Potion_2", + "effect": { + "stat": "health", + "amount": { + "min": 50, + "max": 75 + }, + "isPoison": true + }, + "baseValue": 2500 + }, + { + "name": "ultimate wounding poison", + "icon": "Green_Potion_3", + "effect": { + "stat": "health", + "amount": { + "min": 125, + "max": 175 + }, + "isPoison": true + }, + "baseValue": 10000 + }, + { + "name": "basic lethargic poison", + "icon": "Green_Potion", + "effect": { + "stat": "mana", + "amount": { + "min": 10, + "max": 30 + }, + "isPoison": true + }, + "baseValue": 500 + }, + { + "name": "strong lethargic poison", + "icon": "Green_Potion_2", + "effect": { + "stat": "mana", + "amount": { + "min": 50, + "max": 75 + }, + "isPoison": true + }, + "baseValue": 2500 + }, + { + "name": "ultimate lethargic poison", + "icon": "Green_Potion_3", + "effect": { + "stat": "sanity", + "amount": { + "min": 125, + "max": 175 + }, + "isPoison": true + }, + "baseValue": 10000 + }, + { + "name": "basic madness poison", + "icon": "Green_Potion", + "effect": { + "stat": "sanity", + "amount": { + "min": 10, + "max": 30 + }, + "isPoison": true + }, + "baseValue": 500 + }, + { + "name": "strong madness poison", + "icon": "Green_Potion_2", + "effect": { + "stat": "mana", + "amount": { + "min": 50, + "max": 75 + }, + "isPoison": true + }, + "baseValue": 2500 + }, + { + "name": "ultimate madness poison", + "icon": "Green_Potion_3", + "effect": { + "stat": "sanity", + "amount": { + "min": 125, + "max": 175 + }, + "isPoison": true + }, + "baseValue": 10000 + } +] + diff --git a/src/lineage-json/item-route/potions.json b/src/lineage-json/item-route/potions.json new file mode 100644 index 0000000..a339452 --- /dev/null +++ b/src/lineage-json/item-route/potions.json @@ -0,0 +1,198 @@ +[ + { + "name": "basic healing potion", + "icon": "Red_Potion", + "effect": { + "stat": "health", + "amount": { + "min": 20, + "max": 50 + }, + "isPoison": false + }, + "baseValue": 100 + }, + { + "name": "moderate healing potion", + "icon": "Red_Potion", + "effect": { + "stat": "health", + "amount": { + "min": 40, + "max": 80 + }, + "isPoison": false + }, + "baseValue": 500 + }, + { + "name": "strong healing potion", + "icon": "Red_Potion_2", + "effect": { + "stat": "health", + "amount": { + "min": 70, + "max": 100 + }, + "isPoison": false + }, + "baseValue": 2000 + }, + { + "name": "intense healing potion", + "icon": "Red_Potion_2", + "effect": { + "stat": "health", + "amount": { + "min": 100, + "max": 180 + }, + "isPoison": false + }, + "baseValue": 4000 + }, + { + "name": "extreme healing potion", + "icon": "Red_Potion_3", + "effect": { + "stat": "health", + "amount": { + "min": 150, + "max": 300 + }, + "isPoison": false + }, + "baseValue": 7500 + }, + { + "name": "ultimate healing potion", + "icon": "Red_Potion_3", + "effect": { + "stat": "health", + "amount": { + "min": 250, + "max": 500 + }, + "isPoison": false + }, + "baseValue": 10000 + }, + { + "name": "basic mana potion", + "icon": "Blue_Potion", + "effect": { + "stat": "mana", + "amount": { + "min": 20, + "max": 50 + }, + "isPoison": false + }, + "baseValue": 100 + }, + { + "name": "moderate mana potion", + "icon": "Blue_Potion", + "effect": { + "stat": "mana", + "amount": { + "min": 40, + "max": 80 + }, + "isPoison": false + }, + "baseValue": 500 + }, + { + "name": "strong mana potion", + "icon": "Blue_Potion_2", + "effect": { + "stat": "mana", + "amount": { + "min": 70, + "max": 100 + }, + "isPoison": false + }, + "baseValue": 2000 + }, + { + "name": "intense mana potion", + "icon": "Blue_Potion_2", + "effect": { + "stat": "mana", + "amount": { + "min": 100, + "max": 180 + }, + "isPoison": false + }, + "baseValue": 4000 + }, + { + "name": "extreme mana potion", + "icon": "Blue_Potion_3", + "effect": { + "stat": "mana", + "amount": { + "min": 150, + "max": 300 + }, + "isPoison": false + }, + "baseValue": 7500 + }, + { + "name": "ultimate mana potion", + "icon": "Blue_Potion_3", + "effect": { + "stat": "mana", + "amount": { + "min": 250, + "max": 500 + }, + "isPoison": false + }, + "baseValue": 10000 + }, + { + "name": "basic calming potion", + "icon": "Purple_Potion", + "effect": { + "stat": "sanity", + "amount": { + "min": 20, + "max": 35 + }, + "isPoison": false + }, + "baseValue": 100 + }, + { + "name": "strong calming potion", + "icon": "Purple_Potion_2", + "effect": { + "stat": "sanity", + "amount": { + "min": 50, + "max": 75 + }, + "isPoison": false + }, + "baseValue": 2000 + }, + { + "name": "ultimate calming potion", + "icon": "Purple_Potion_3", + "effect": { + "stat": "sanity", + "amount": { + "min": 100, + "max": 150 + }, + "isPoison": false + }, + "baseValue": 10000 + } +] + diff --git a/src/lineage-json/item-route/prefix.json b/src/lineage-json/item-route/prefix.json new file mode 100644 index 0000000..06b1fac --- /dev/null +++ b/src/lineage-json/item-route/prefix.json @@ -0,0 +1,471 @@ +[ + { + "name": { + "5": "healthy", + "4": "hearty", + "3": "hearty", + "2": "vital", + "1": "invigorating" + }, + "modifier": { + "health": [ + { + "5": { + "min": 10, + "max": 14 + }, + "4": { + "min": 15, + "max": 19 + }, + "3": { + "min": 20, + "max": 24 + }, + "2": { + "min": 25, + "max": 29 + }, + "1": { + "min": 30, + "max": 35 + } + } + ] + }, + "tiers": 5 + }, + { + "name": { + "5": "sturdy", + "4": "robust", + "3": "robust", + "2": "vigorous", + "1": "flourishing" + }, + "modifier": { + "health": [ + { + "5": { + "min": 8, + "max": 11 + }, + "4": { + "min": 12, + "max": 15 + }, + "3": { + "min": 16, + "max": 19 + }, + "2": { + "min": 20, + "max": 23 + }, + "1": { + "min": 24, + "max": 28 + } + } + ], + "healthRegen": [ + { + "5": { + "min": 1, + "max": 1 + }, + "4": { + "min": 1, + "max": 2 + }, + "3": { + "min": 2, + "max": 2 + }, + "2": { + "min": 2, + "max": 3 + }, + "1": { + "min": 3, + "max": 4 + } + } + ] + }, + "tiers": 5 + }, + { + "name": { + "4": "sage", + "3": "wise", + "2": "wise", + "1": "enlightened" + }, + "modifier": { + "mana": [ + { + "4": { + "min": 8, + "max": 11 + }, + "3": { + "min": 12, + "max": 15 + }, + "2": { + "min": 16, + "max": 19 + }, + "1": { + "min": 20, + "max": 25 + } + } + ], + "intelligence": [ + { + "4": { + "min": 1, + "max": 2 + }, + "3": { + "min": 2, + "max": 3 + }, + "2": { + "min": 3, + "max": 4 + }, + "1": { + "min": 4, + "max": 5 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "3": "balanced", + "2": "stable", + "1": "harmonious" + }, + "modifier": { + "sanity": [ + { + "3": { + "min": 10, + "max": 14 + }, + "2": { + "min": 15, + "max": 19 + }, + "1": { + "min": 20, + "max": 25 + } + } + ] + }, + "tiers": 3 + }, + { + "name": { + "4": "energetic", + "3": "vigorous", + "2": "vigorous", + "1": "revitalizing" + }, + "modifier": { + "healthRegen": [ + { + "4": { + "min": 2, + "max": 3 + }, + "3": { + "min": 3, + "max": 4 + }, + "2": { + "min": 4, + "max": 5 + }, + "1": { + "min": 5, + "max": 6 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "3": "focused", + "2": "concentrated", + "1": "meditative" + }, + "modifier": { + "manaRegen": [ + { + "3": { + "min": 2, + "max": 3 + }, + "2": { + "min": 3, + "max": 4 + }, + "1": { + "min": 4, + "max": 5 + } + } + ] + }, + "tiers": 3 + }, + { + "name": { + "3": "strong", + "2": "mighty", + "1": "herculean" + }, + "modifier": { + "strength": [ + { + "3": { + "min": 3, + "max": 4 + }, + "2": { + "min": 5, + "max": 6 + }, + "1": { + "min": 7, + "max": 8 + } + } + ] + }, + "tiers": 3 + }, + { + "name": { + "3": "nimble", + "2": "agile", + "1": "acrobatic" + }, + "modifier": { + "dexterity": [ + { + "3": { + "min": 3, + "max": 4 + }, + "2": { + "min": 5, + "max": 6 + }, + "1": { + "min": 7, + "max": 8 + } + } + ] + }, + "tiers": 3 + }, + { + "name": { + "4": "protective", + "3": "fortified", + "2": "fortified", + "1": "impenetrable" + }, + "modifier": { + "armorAdded": [ + { + "4": { + "min": 5, + "max": 7 + }, + "3": { + "min": 8, + "max": 10 + }, + "2": { + "min": 11, + "max": 13 + }, + "1": { + "min": 14, + "max": 16 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "4": "tough", + "3": "tempered", + "2": "tempered", + "1": "unyielding" + }, + "modifier": { + "armorAdded": [ + { + "4": { + "min": 3, + "max": 4 + }, + "3": { + "min": 5, + "max": 6 + }, + "2": { + "min": 7, + "max": 8 + }, + "1": { + "min": 9, + "max": 10 + } + } + ], + "health": [ + { + "4": { + "min": 4, + "max": 6 + }, + "3": { + "min": 7, + "max": 9 + }, + "2": { + "min": 10, + "max": 12 + }, + "1": { + "min": 13, + "max": 15 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "3": "flameward", + "2": "flameproof", + "1": "infernoshield" + }, + "modifier": { + "fireResistance": [ + { + "3": { + "min": 0.08, + "max": 0.10 + }, + "2": { + "min": 0.11, + "max": 0.13 + }, + "1": { + "min": 0.14, + "max": 0.16 + } + } + ] + }, + "tiers": 3 + }, + { + "name": { + "3": "frostward", + "2": "frostproof", + "1": "glacialshield" + }, + "modifier": { + "coldResistance": [ + { + "3": { + "min": 0.08, + "max": 0.10 + }, + "2": { + "min": 0.11, + "max": 0.13 + }, + "1": { + "min": 0.14, + "max": 0.16 + } + } + ] + }, + "tiers": 3 + }, + { + "name": { + "3": "stormward", + "2": "stormproof", + "1": "thundershield" + }, + "modifier": { + "lightningResistance": [ + { + "3": { + "min": 0.8, + "max": 0.10 + }, + "2": { + "min": 0.11, + "max": 0.13 + }, + "1": { + "min": 0.14, + "max": 0.16 + } + } + ] + }, + "tiers": 3 + }, + { + "name": { + "3": "toxic", + "2": "antitoxin", + "1": "venomshield" + }, + "modifier": { + "poisonResistance": [ + { + "3": { + "min": 0.08, + "max": 0.10 + }, + "2": { + "min": 0.11, + "max": 0.13 + }, + "1": { + "min": 0.14, + "max": 0.16 + } + } + ] + }, + "tiers": 3 + } +] + diff --git a/src/lineage-json/item-route/robes.json b/src/lineage-json/item-route/robes.json new file mode 100644 index 0000000..c3858cc --- /dev/null +++ b/src/lineage-json/item-route/robes.json @@ -0,0 +1,141 @@ +[ + { + "name": "cloth robes", + "baseValue": 200, + "icon": "Robes_1", + "slot": "body", + "stats": { + "armor": 4, + "mana": 15, + "manaRegen": 0 + } + }, + { + "name": "apprentice robes", + "baseValue": 1500, + "icon": "Robes_1", + "slot": "body", + "stats": { + "armor": 6, + "mana": 25, + "manaRegen": 0 + }, + "requirements": { + "intelligence": 5 + } + }, + { + "name": "adept robes", + "baseValue": 2500, + "icon": "Robes_1", + "slot": "body", + "stats": { + "armor": 8, + "mana": 50, + "manaRegen": 2 + }, + "requirements": { + "intelligence": 8 + } + }, + { + "name": "mage robes", + "baseValue": 2500, + "icon": "Robes_2", + "slot": "body", + "stats": { + "armor": 8, + "mana": 100, + "manaRegen": 0 + }, + "requirements": { + "intelligence": 15 + } + }, + { + "name": "gorgeous robes", + "baseValue": 6500, + "icon": "Robes_2", + "slot": "body", + "stats": { + "armor": 10, + "mana": 100, + "manaRegen": 2 + }, + "requirements": { + "intelligence": 20 + } + }, + { + "name": "expert robes", + "baseValue": 25000, + "icon": "Robes_2", + "slot": "body", + "stats": { + "armor": 10, + "mana": 150, + "manaRegen": 5 + }, + "requirements": { + "intelligence": 30 + } + }, + { + "name": "rouge magi's vestment", + "baseValue": 47500, + "icon": "Robes_3", + "slot": "body", + "stats": { + "armor": 16, + "manaRegen": 12 + }, + "requirements": { + "strength": 5, + "intelligence": 30 + } + }, + { + "name": "war mage's vestment", + "baseValue": 95000, + "icon": "Robes_3", + "slot": "body", + "stats": { + "armor": 28, + "mana": 100, + "manaRegen": 8, + "health": 100 + }, + "requirements": { + "strength": 10, + "intelligence": 30 + } + }, + { + "name": "ancient silks", + "baseValue": 200000, + "icon": "Robes_3", + "slot": "body", + "stats": { + "mana": 400, + "manaRegen": 20 + }, + "requirements": { + "intelligence": 40 + } + }, + { + "name": "arch-mage's robes", + "baseValue": 450000, + "icon": "Robes_3", + "slot": "body", + "stats": { + "armor": 25, + "mana": 350, + "manaRegen": 20 + }, + "requirements": { + "intelligence": 50 + } + } +] + diff --git a/src/lineage-json/item-route/shields.json b/src/lineage-json/item-route/shields.json new file mode 100644 index 0000000..4bbb980 --- /dev/null +++ b/src/lineage-json/item-route/shields.json @@ -0,0 +1,114 @@ +[ + { + "name": "block of wood", + "baseValue": 100, + "icon": "Wooden_Shield", + "slot": "one-hand", + "stats": { + "armor": 4.0, + "blockChance": 0.035 + } + }, + { + "name": "cheap buckler", + "baseValue": 500, + "icon": "Wooden_Shield", + "slot": "one-hand", + "stats": { + "armor": 6.0, + "blockChance": 0.05 + } + }, + { + "name": "block of iron", + "baseValue": 1000, + "icon": "Iron_Shield", + "slot": "one-hand", + "stats": { + "armor": 9.0, + "blockChance": 0.035 + }, + "requirements": { + "strength": 8 + } + }, + { + "name": "iron shield", + "baseValue": 7000, + "icon": "Iron_Shield", + "slot": "one-hand", + "stats": { + "armor": 11.0, + "blockChance": 0.05 + }, + "requirements": { + "strength": 10 + } + }, + { + "name": "kite shield", + "baseValue": 14000, + "icon": "Iron_Shield", + "slot": "one-hand", + "stats": { + "armor": 10.0, + "blockChance": 0.07 + }, + "requirements": { + "strength": 10 + } + }, + { + "name": "pavise", + "baseValue": 32000, + "icon": "Iron_Shield", + "slot": "one-hand", + "stats": { + "armor": 14.0, + "blockChance": 0.10 + }, + "requirements": { + "strength": 15 + } + }, + { + "name": "great shield", + "baseValue": 80000, + "icon": "Iron_Shield", + "slot": "one-hand", + "stats": { + "armor": 20.0, + "blockChance": 0.06 + }, + "requirements": { + "strength": 22 + } + }, + { + "name": "royal great shield", + "baseValue": 120000, + "icon": "Iron_Shield", + "slot": "one-hand", + "stats": { + "armor": 30.0, + "blockChance": 0.07 + }, + "requirements": { + "strength": 28 + } + }, + { + "name": "shield of glory", + "baseValue": 350000, + "icon": "Iron_Shield", + "slot": "one-hand", + "stats": { + "armor": 40.0, + "blockChance": 0.07 + }, + "requirements": { + "strength": 35 + } + } +] + diff --git a/src/lineage-json/item-route/staves.json b/src/lineage-json/item-route/staves.json new file mode 100644 index 0000000..f813188 --- /dev/null +++ b/src/lineage-json/item-route/staves.json @@ -0,0 +1,16 @@ +[ + { + "name": "Goblin Totem", + "baseValue": 5000, + "icon": "Goblin_Staff", + "slot": "one-hand", + "attacks": [ + "bonk", + "spark" + ], + "stats": { + "physicalDamage": 8.0 + } + } +] + diff --git a/src/lineage-json/item-route/storyItems.json b/src/lineage-json/item-route/storyItems.json new file mode 100644 index 0000000..26cea2d --- /dev/null +++ b/src/lineage-json/item-route/storyItems.json @@ -0,0 +1,21 @@ +[ + { + "name": "the deed to the whispering raven inn", + "icon": "Scroll", + "description": "Cursive**Holding this deed means The Whispering Raven Inn is yours, but you will need to repair the property before it can serve patrons**\n*The parchment is stained with dark splotches, its edges frayed and torn. On the back on the deed there are scribles The handwriting becomes increasingly erratic as it progresses.*\n\nTo those who would judge me,\n\nI, Alaric Shadowmere, once proprietor of the Whispering Raven Inn, pen this final testament. Let it serve as both confession and vindication for the deeds that have brought me to this wretched state.\n\nFor years, I welcomed weary travelers, offering respite from the horrors that roam our blighted lands. But with each passing season, I bore witness to the futility of it all. The weak perish, the strong exploit, and death claims us all in the end.\n\nIt was then that the whispers began. The ancient tomes hidden beneath the floorboards of my cellar spoke of power beyond mortal ken. They promised a way to conquer death itself, to build an army that would never tire, never falter.\n\nAnd so, I began my great work.\n\nEach patron who crossed my threshold became more than a guest – they became raw material for my grand design. Their flesh, their bones, their very essence – all repurposed in service of a greater cause.\n\nDo you not see the beauty in it? The drunkard who squandered his life now stands eternal guard. The abusive merchant now toils without rest or reward. I have given them purpose beyond their petty lives!\n\nBut the FOOLS outside do not understand!\n\nThey call me monster, madman, murderer. They cannot comprehend the magnitude of my vision.\n\nAs I write this, I hear them coming. The villagers, the so-called heroes, all clamoring for my head. Let them come. My children – my beautiful, rotting children – will greet them.\n\nAnd should I fall, know this: Death is but a doorway. I have peered beyond its threshold, and I fear it no longer. In time, you too will understand the gift I offered this miserable world.\n\nMay He embrace us all.\n\n*The signature at the bottom is a smeared, illegible scrawl*", + "baseValue": -1 + }, + { + "name": "head of goblin shaman", + "icon": "Skull", + "description": "*Stick it on a spike to ward off other Goblin tribes, and confer safety to a trade route*\n\n", + "baseValue": -1 + }, + { + "name": "broken seal contract", + "icon": "Paper", + "description": "Cursive*A weathered piece of parchment sealed with black wax bearing a bleeding sun. The edges are singed as if exposed to intense heat.*\n\nBy acceptance of payment in gold, the undersigned hereby commits to the procurement and delivery of specimens matching the following criteria:\n\n- Aged 12 and over.\n\n- Weight of 7.5 stone or greater.\n\nHE requires these specimens intact and breathing. The method of acquisition is at your discretion, though discretion itself is paramount. Those who draw undue attention will find their own names added to HIS ledger.\n\nPayment will be rendered upon delivery to the Blood Eye Gate. Additional compensation will be provided for specimens of exceptional quality.\n\nFailure to deliver will result in the forfeiture of not only payment but also that which you hold most precious.\n\n*The signature appears to be written in a script that hurts the eyes to look upon directly*", + "baseValue": -1 + } +] + diff --git a/src/lineage-json/item-route/suffix.json b/src/lineage-json/item-route/suffix.json new file mode 100644 index 0000000..00bd768 --- /dev/null +++ b/src/lineage-json/item-route/suffix.json @@ -0,0 +1,413 @@ +[ + { + "name": { + "4": "soldier", + "3": "warrior", + "2": "champion", + "1": "hero" + }, + "modifier": { + "physicalDamageAdded": [ + { + "4": { + "min": 6, + "max": 8 + }, + "3": { + "min": 9, + "max": 11 + }, + "2": { + "min": 12, + "max": 14 + }, + "1": { + "min": 15, + "max": 18 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "4": "smoldering", + "3": "burning", + "2": "scorching", + "1": "infernal" + }, + "modifier": { + "fireDamageAdded": [ + { + "4": { + "min": 6, + "max": 8 + }, + "3": { + "min": 9, + "max": 11 + }, + "2": { + "min": 12, + "max": 14 + }, + "1": { + "min": 15, + "max": 18 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "4": "chilled", + "3": "frozen", + "2": "glacier", + "1": "arctic" + }, + "modifier": { + "coldDamageAdded": [ + { + "4": { + "min": 6, + "max": 8 + }, + "3": { + "min": 9, + "max": 11 + }, + "2": { + "min": 12, + "max": 14 + }, + "1": { + "min": 15, + "max": 18 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "4": "static", + "3": "shocked", + "2": "electrified", + "1": "thunderous" + }, + "modifier": { + "lightningDamageAdded": [ + { + "4": { + "min": 6, + "max": 8 + }, + "3": { + "min": 9, + "max": 11 + }, + "2": { + "min": 12, + "max": 14 + }, + "1": { + "min": 15, + "max": 18 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "4": "noxious", + "3": "venomous", + "2": "virulent", + "1": "pestilent" + }, + "modifier": { + "poisonDamageAdded": [ + { + "4": { + "min": 6, + "max": 8 + }, + "3": { + "min": 9, + "max": 11 + }, + "2": { + "min": 12, + "max": 14 + }, + "1": { + "min": 15, + "max": 18 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "4": "challenger", + "3": "gladiator", + "2": "marauder", + "1": "conqueror" + }, + "modifier": { + "physicalDamageMultiplier": [ + { + "4": { + "min": 0.05, + "max": 0.08 + }, + "3": { + "min": 0.09, + "max": 0.12 + }, + "2": { + "min": 0.13, + "max": 0.16 + }, + "1": { + "min": 0.17, + "max": 0.20 + } + } + ], + "strength": [ + { + "4": { + "min": 1, + "max": 1 + }, + "3": { + "min": 1, + "max": 2 + }, + "2": { + "min": 2, + "max": 3 + }, + "1": { + "min": 3, + "max": 4 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "4": "imp", + "3": "fiend", + "2": "demon", + "1": "devil" + }, + "modifier": { + "fireDamageMultiplier": [ + { + "4": { + "min": 0.05, + "max": 0.08 + }, + "3": { + "min": 0.09, + "max": 0.12 + }, + "2": { + "min": 0.13, + "max": 0.16 + }, + "1": { + "min": 0.17, + "max": 0.20 + } + } + ], + "fireDamageAdded": [ + { + "4": { + "min": 1, + "max": 2 + }, + "3": { + "min": 2, + "max": 3 + }, + "2": { + "min": 3, + "max": 4 + }, + "1": { + "min": 4, + "max": 5 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "4": "chilly", + "3": "frigid", + "2": "frozen", + "1": "arctic" + }, + "modifier": { + "coldDamageMultiplier": [ + { + "4": { + "min": 0.05, + "max": 0.08 + }, + "3": { + "min": 0.09, + "max": 0.12 + }, + "2": { + "min": 0.13, + "max": 0.16 + }, + "1": { + "min": 0.17, + "max": 0.20 + } + } + ], + "coldDamageAdded": [ + { + "4": { + "min": 1, + "max": 2 + }, + "3": { + "min": 2, + "max": 3 + }, + "2": { + "min": 3, + "max": 4 + }, + "1": { + "min": 4, + "max": 5 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "4": "charged", + "3": "arcing", + "2": "thunderous", + "1": "superconducting" + }, + "modifier": { + "lightningDamageMultiplier": [ + { + "4": { + "min": 0.05, + "max": 0.08 + }, + "3": { + "min": 0.09, + "max": 0.12 + }, + "2": { + "min": 0.13, + "max": 0.16 + }, + "1": { + "min": 0.17, + "max": 0.20 + } + } + ], + "lightningDamageAdded": [ + { + "4": { + "min": 1, + "max": 2 + }, + "3": { + "min": 2, + "max": 3 + }, + "2": { + "min": 3, + "max": 4 + }, + "1": { + "min": 4, + "max": 5 + } + } + ] + }, + "tiers": 4 + }, + { + "name": { + "4": "toxic", + "3": "virulent", + "2": "malignant", + "1": "plague-bearer" + }, + "modifier": { + "poisonDamageMultiplier": [ + { + "4": { + "min": 0.05, + "max": 0.08 + }, + "3": { + "min": 0.09, + "max": 0.12 + }, + "2": { + "min": 0.13, + "max": 0.16 + }, + "1": { + "min": 0.17, + "max": 0.20 + } + } + ], + "poisonDamageAdded": [ + { + "4": { + "min": 1, + "max": 2 + }, + "3": { + "min": 2, + "max": 3 + }, + "2": { + "min": 3, + "max": 4 + }, + "1": { + "min": 4, + "max": 5 + } + } + ] + }, + "tiers": 4 + } +] + diff --git a/src/lineage-json/item-route/wands.json b/src/lineage-json/item-route/wands.json new file mode 100644 index 0000000..dfaa3a8 --- /dev/null +++ b/src/lineage-json/item-route/wands.json @@ -0,0 +1,244 @@ +[ + { + "name": "Apprentice Wand", + "baseValue": 1000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "magicDamage": 5.0 + } + }, + { + "name": "Scorching Wand", + "baseValue": 5000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "fireDamage": 8.0 + } + }, + { + "name": "Frozen Wand", + "baseValue": 12000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "coldDamage": 12.0 + } + }, + { + "name": "Wand of the Prodigy", + "baseValue": 18000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "magicDamage": 18.0 + } + }, + { + "name": "Adept Wand", + "baseValue": 22000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "magicDamage": 20.0 + } + }, + { + "name": "Wand of the Inferno", + "baseValue": 26000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "fireDamage": 23.0 + } + }, + { + "name": "Enchanted Wand", + "baseValue": 31000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "magicDamage": 27.0 + } + }, + { + "name": "Wand of Blizzards", + "baseValue": 40000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "coldDamage": 31.0 + } + }, + { + "name": "Expert Wand", + "baseValue": 48000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "magicDamage": 34.0 + } + }, + { + "name": "Wand of Decay", + "baseValue": 65000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "poisonDamage": 38.0 + } + }, + { + "name": "Master's Wand", + "baseValue": 84000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "magicDamage": 42.0 + } + }, + { + "name": "Ethereal Wand", + "baseValue": 121000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "magicDamage": 45.0 + } + }, + { + "name": "Celestial Wand", + "baseValue": 145000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "magicDamage": 48.0 + } + }, + { + "name": "Wand of Destiny", + "baseValue": 185000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "holyDamage": 52.0 + } + }, + { + "name": "Wand of Misfortune", + "baseValue": 245000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "poisonDamage": 55.0 + } + }, + { + "name": "Light's End", + "baseValue": 285000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "lightningDamage": 58.0 + } + }, + { + "name": "Wand of the Forgotten", + "baseValue": 335000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "physicalDamage": 60.0 + } + }, + { + "name": "Divine Wand", + "baseValue": 410000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "holyDamage": 62.0 + } + }, + { + "name": "Death Ray", + "baseValue": 520000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "poisonDamage": 65.0 + } + }, + { + "name": "Finger of God", + "baseValue": 750000, + "icon": "Magic_Wand", + "slot": "one-hand", + "attacks": [ + "cast" + ], + "stats": { + "holyDamage": 50.0, + "magicDamage": 20.0 + } + } +] + diff --git a/src/lineage-json/misc-route/activities.json b/src/lineage-json/misc-route/activities.json new file mode 100644 index 0000000..496b8d4 --- /dev/null +++ b/src/lineage-json/misc-route/activities.json @@ -0,0 +1,224 @@ +[ + { + "name": "walk in the Enchanted Forest", + "cost": 0, + "alone": { + "meetingSomeone": 0.15, + "nothingHappens": 0.25, + "randomGood": 0.40, + "randomBad": 0.20 + }, + "dateCooldown": 5, + "date": { + "increaseAffection": 0.85, + "increaseAffectionRange": { + "min": 1, + "max": 10 + }, + "decreaseAffection": 0.15, + "decreaseAffectionRange": { + "min": 1, + "max": 5 + } + }, + "randomBad": [ + { + "name": "Mugged!", + "buyOff": { + "price": 300 + }, + "dungeonTitle": "Enchanted Forest", + "fight": { + "enemies": [ + { + "name": "bandit", + "image": "bandit_light", + "scaler": 1, + "count": 1 + } + ] + } + } + ], + "randomGood": [ + { + "name": "Found a gold pouch!", + "effect": { + "gold": 50 + } + }, + { + "name": "Had a beautiful restorative walk", + "effect": { + "healthRestore": 25, + "sanityRestore": 25 + } + } + ] + }, + { + "name": "go to a Library", + "cost": 0, + "alone": { + "meetingSomeone": 0.40, + "nothingHappens": 0.40, + "randomGood": 0.10, + "randomBad": 0.10 + }, + "dateCooldown": 10, + "date": { + "increaseAffection": 0.85, + "increaseAffectionRange": { + "min": 3, + "max": 7 + }, + "decreaseAffection": 0.15, + "decreaseAffectionRange": { + "min": 1, + "max": 4 + } + }, + "randomBad": [ + { + "name": "Read a disturbing book", + "effect": { + "sanityDamage": 25 + } + } + ], + "randomGood": [ + { + "name": "Read from your favorite author", + "effect": { + "sanityRestore": 25 + } + }, + { + "name": "Found a gold pouch!", + "effect": { + "gold": 25 + } + } + ] + }, + { + "name": "go to the Pub", + "cost": 25, + "alone": { + "meetingSomeone": 0.30, + "nothingHappens": 0.20, + "randomGood": 0.15, + "randomBad": 0.35 + }, + "dateCooldown": 10, + "date": { + "increaseAffection": 0.80, + "increaseAffectionRange": { + "min": 5, + "max": 15 + }, + "decreaseAffection": 0.20, + "decreaseAffectionRange": { + "min": 3, + "max": 10 + } + }, + "randomBad": [ + { + "name": "Bar Fight", + "dungeonTitle": "Local Pub", + "fight": { + "enemies": [ + { + "name": "bandit heavy", + "image": "bandit_heavy", + "scaler": 1, + "count": 1 + } + ] + } + } + ], + "randomGood": [ + { + "name": "Free Round", + "effect": { + "gold": 25, + "sanityRestore": 25 + } + }, + { + "name": "Found a gold pouch!", + "effect": { + "gold": 75 + } + } + ] + }, + { + "name": "go to a Festival", + "cost": 50, + "alone": { + "meetingSomeone": 0.40, + "nothingHappens": 0.40, + "randomGood": 0.10, + "randomBad": 0.10 + }, + "dateCooldown": 15, + "date": { + "increaseAffection": 0.85, + "increaseAffectionRange": { + "min": 5, + "max": 15 + }, + "decreaseAffection": 0.15, + "decreaseAffectionRange": { + "min": 3, + "max": 10 + } + }, + "randomBad": [ + { + "name": "Raucous Local", + "dungeonTitle": "Festival", + "fight": { + "enemies": [ + { + "name": "bandit heavy", + "image": "bandit_heavy", + "scaler": 1, + "count": 1 + } + ] + } + }, + { + "name": "Ride broke down!", + "effect": { + "healthDamage": 20, + "sanityDamage": 25 + } + } + ], + "randomGood": [ + { + "name": "Found a gold pouch!", + "effect": { + "gold": 100 + } + } + ] + }, + { + "name": "visit a Soul-Thread Weaver", + "cost": 250, + "aloneCooldown": 20, + "alone": { + "meetingSomeone": 0.65, + "nothingHappens": 0.35, + "randomGood": 0.00, + "randomBad": 0.00 + } + } +] + diff --git a/src/lineage-json/misc-route/healthOptions.json b/src/lineage-json/misc-route/healthOptions.json new file mode 100644 index 0000000..49270de --- /dev/null +++ b/src/lineage-json/misc-route/healthOptions.json @@ -0,0 +1,12 @@ +[ + { + "serviceName": "Recover Alone", + "cost": 0, + "heathRestore": 15 + }, + { + "serviceName": "Visit Master Priest", + "cost": 150, + "heathRestore": "fill" + } +] diff --git a/src/lineage-json/misc-route/investments.json b/src/lineage-json/misc-route/investments.json new file mode 100644 index 0000000..05d0070 --- /dev/null +++ b/src/lineage-json/misc-route/investments.json @@ -0,0 +1,369 @@ +[ + { + "name": "Whispering Raven Inn", + "description": "It's cheap and in the middle of no-where. Don't expect much foot traffic. But those rare few tired souls will be grateful for a quiet refuge from the beasts of the night.", + "requires": {"requirement": "the deed to the whispering raven inn", "message": "A dangerous flood of monsters is spilling out from a nearby cave, preventing anyone from getting close to this rest stop, put an end to the source before further considering purchase.", "removes": false }, + "cost": 5000, + "turnsPerReturn": 1, + "goldReturnRange": {"min": 0, "max": 50}, + "maxGoldStockPile": 3500, + "upgrades": [ + { + "name": "Install Signage", + "cost": 1500, + "description":"Install signage along nearby roads. Increases gold returns.", + "effect": {"goldMinimumIncrease": 10, "goldMaximumIncrease": 5} + }, + { + "name": "Alcohol License", + "cost": 10000, + "description":"Alcohol often leads to heavy spenders. Increases maximum gold return.", + "effect": {"goldMaximumIncrease": 25} + }, + { + "name": "Luxury Rooms", + "cost": 17500, + "description":"Nicer rooms means you can charge more. Increases minimum gold return.", + "effect": {"goldMinimumIncrease": 60} + }, + { + "name": "New Wing", + "cost": 22500, + "description":"More rooms means more bodies at peak. Increases maximum gold return.", + "effect": {"goldMaximumIncrease": 135, "maxGoldStockPileIncrease": 1500} + } + ] + }, + { + "name": "Trading Route", + "description": "Control the arteries of commerce, provide a trader with startup costs.", + "requires": {"requirement": "head of goblin shaman", "message": "Goblins have been attacking the roads, clear out the goblin cave to ensure that they won't pose a danger.", "removes": true}, + "cost": 10000, + "turnsPerReturn": 10, + "goldReturnRange": {"min": 0, "max": 750}, + "maxGoldStockPile": 2500, + "upgrades": [ + { + "name": "Inexperienced Guard", + "cost": 2000, + "description":"Hire a nobody to travel with the trader. Increases minimum gold returns.", + "effect": {"goldMinimumIncrease": 150} + }, + { + "name": "Faster Horses", + "cost": 4000, + "description":"Purchase horses to shorten travel time. Shortens travel time.", + "effect": {"turnsPerRollChange": -2} + }, + { + "name": "Sturdy Carriage", + "cost": 6500, + "description":"Better put together carriage means lower chance of breakdowns and more carry weight. Increases max stockpile size, and shortens travel time.", + "effect": {"turnsPerRollChange": -1, "maxGoldStockPileIncrease": 2000} + }, + { + "name": "Capital Sales License", + "cost": 10000, + "description":"Acquire the ability to sell goods within the Capital City's borders. Increases gold returns.", + "effect": {"goldMinimumIncrease": 50, "goldMaximumIncrease": 400} + }, + { + "name": "Experienced Guards", + "cost": 17500, + "description":"Hire quality guards. Increases minimum gold returns.", + "effect": {"goldMinimumIncrease": 500} + }, + { + "name": "Expand Caravan", + "cost": 25000, + "description":"Expanding size of caravan means more of everything, but slows down the route. Increases gold returns, and max stockpile size", + "effect": {"turnsPerRollChange": 1, "goldMinimumIncrease": 400, "goldMaximumIncrease": 450, "maxGoldStockPileIncrease": 3500} + }, + { + "name": "Regulatory Capture", + "cost": 30000, + "description":"Bribe a judge to enact regulations few will be able to comply with. Increases gold returns.", + "effect": {"goldMinimumIncrease": 550, "goldMaximumIncrease": 800} + } + ] + }, + { + "name": "Village Inn", + "description": "A vital haven for weary travelers and thirsty locals alike. On a good night, the air is filled with laughter, music, and the clinking of golden coins (and the potential broken table).", + "requires": {"requirement": "broken seal contract", "message": "The writ of sale has been stolen by bandits! Get it back to proceed with the purchase.", "removes": false}, + "cost": 39500, + "turnsPerReturn": 1, + "goldReturnRange": {"min": -100, "max": 300}, + "maxGoldStockPile": 10000, + "upgrades": [ + { + "name": "Hire Pretty Tavern Girl", + "cost": 4500, + "description":"Drunk men are simple creatures. Increases maximum gold returns.", + "effect": {"goldMaximumIncrease": 35} + }, + { + "name": "Hire Experienced Bartender", + "cost": 14000, + "description":"Great bartenders keep the drinks flowing. Increases maximum gold return.", + "effect": {"goldMaximumIncrease": 75} + }, + { + "name": "Hire Intimidating Bouncers", + "cost": 22500, + "description":"Keeps the rowdy customers under control. Increases minimum gold return.", + "effect": {"goldMinimumIncrease": 100} + }, + { + "name": "Biergarten", + "cost": 37500, + "description":"Expand land plot with outdoor area, some may get too rowdy, leading to minor fines. Decreases minimum gold return, increases maximum gold return.", + "effect": {"goldMinimumIncrease": -150, "goldMaximumIncrease": 300} + }, + { + "name": "'Entertainment' Wing", + "cost": 85000, + "description":"Use your imagination. Increases gold returns.", + "effect": {"goldMinimumIncrease": 50, "goldMaximumIncrease": 500} + } + ] + }, + { + "name": "Spellbook Publisher", + "description": "Magic drips from the quill in this establishment, as scribes imbued with arcane knowledge ink the pages of tomes that will guide the next generation of magi.", + "requires": {"requirement": "rogue magi fortress", "message": "A nearby fortress filled to craven magi has destroyed this building and sacked the books in its stockpile. You should clear the fortress before beginning the rebuild.", "removes": false}, + "cost": 75000, + "turnsPerReturn": 25, + "goldReturnRange": {"min": 500, "max": 5500}, + "maxGoldStockPile": 25000, + "upgrades": [ + { + "name": "Stockpile Room", + "cost": 15000, + "description":"It's trademarked and highway robbery, but it works. Increases minimum gold return.", + "effect": {"maxGoldStockPileIncrease": 15000} + }, + { + "name": "Undo-able Ink™", + "cost": 22000, + "description":"It's trademarked and highway robbery, but it works. Increases minimum gold return.", + "effect": {"goldMinimumIncrease": 2500} + }, + { + "name": "Exclusivity Contracts", + "cost": 50000, + "description":"Lock down the best talent. Increases gold returns.", + "effect": {"goldMinimumIncrease": 1500, "goldMaximumIncrease": 6500} + }, + { + "name": "Arcane Quill Stockpile", + "cost": 120000, + "description":"Quills need not be held, only spoken to, one scribe can use hundreds. Decreases publishing time.", + "effect": {"turnsPerRollChange": -12} + } + ] + }, + { + "name": "Iron Mine", + "description": "Deep in the earth, the rhythmic beat of picks against stone echoes out as miners unearth veins of precious iron, indispensable for smiths and warriors.", + "requires": {"requirement": "infested mine", "message": "The mine is completely overrun. You will need to clear it before anyone will consider entering it.", "removes":false}, + "cost": 145000, + "turnsPerReturn": 10, + "goldReturnRange": {"min": 1200, "max": 3000}, + "maxGoldStockPile": 35000, + "upgrades": [ + { + "name": "Enchanted Steel Tools", + "cost": 27500, + "description":"Invest in tools to increase workers' efficiency. Increases gold returns.", + "effect": {"goldMinimumIncrease": 400, "goldMaximumIncrease": 550} + }, + { + "name": "Cart System", + "cost": 50000, + "description":"Install a cart system for more efficient transport of ore. Increases gold returns and max stockpile size.", + "effect": {"goldMinimumIncrease": 400, "goldMaximumIncrease": 550, "maxGoldStockPileIncrease": 25000} + }, + { + "name": "Miner's Training", + "cost": 75000, + "description":"Provide advanced training for miners to improve their work. Increases gold returns.", + "effect": {"goldMinimumIncrease": 1100, "goldMaximumIncrease": 1000} + }, + { + "name": "Improved Mining Techniques", + "cost": 100000, + "description":"Adopt and implement breakthrough mining techniques. Increases gold returns.", + "effect": {"goldMinimumIncrease": 900, "goldMaximumIncrease": 1250} + }, + { + "name": "Ward Against Cave-Ins", + "cost": 150000, + "description":"Enchant the mine with magic wards to prevent cave-ins. Increases gold returns and shortens ore extraction time.", + "effect": {"turnsPerRollChange": -2, "goldMinimumIncrease": 650, "goldMaximumIncrease": 1100} + }, + { + "name": "Profit Sharing", + "cost": 200000, + "excludes": "Indentured Servitude", + "style": "neutral", + "description": "Hire a lawyer to draft a plan to share part of the profits with top employees as an incentive. Increases minimum gold returns.", + "effect": {"goldMinimumIncrease": 4000, "goldMaximumIncrease": 1000} + }, + { + "name": "Indentured Servitude", + "cost": 200000, + "excludes": "Profit Sharing", + "style": "evil", + "description":"Bribe nearby city officials for access to their criminal population, its a win-win if you don't consider morality, but that's what the gold is for. Decreases extraction time and dramatically increases gold returns at the cost of max sanity", + "effect": {"turnsPerRollChange": -2, "goldMinimumIncrease": 1000, "goldMaximumIncrease": 4250, "changeMaxSanity": -5} + } + ] + }, + { + "name": "Monster Breeding Ranch", + "description": "Here, creatures of nightmare and wonder are reared. Some are bred for their power in battle, others for exotic pets. All fetch a fair price on the market.", + "requires": {"requirement":"dark forest", "message": "This project will require an immense amount of land. Clear out the Dark Forest and you will have all the land you need.", "removes":false}, + "cost": 220000, + "turnsPerReturn": 15, + "goldReturnRange": {"min": 2000, "max": 6000}, + "maxGoldStockPile": 15000, + "upgrades": [ + { + "name": "Nest Mimicry", + "description": "Replicate the natural habitats of different creatures to boost their growth and health. Increases gold returns.", + "cost": 65000, + "effect": {"goldMinimumIncrease": 500, "goldMaximumIncrease": 1150} + }, + { + "name": "Magical Supplements", + "description": "Feed beasts nutrient-rich, magic-infused feed to promote growth. Shortens time to market.", + "cost": 120000, + "effect": {"turnsPerRollChange": -5 } + }, + { + "name": "Black Market Sales", + "cost": 145000, + "excludes": "Ethical Breeding", + "style": "evil", + "description":"Sell to disreputable buyers. Increases gold returns and decreases time to market. decreases sanity.", + "effect": {"turnsPerRollChange": -2, "goldMinimumIncrease": 1500, "goldMaximumIncrease": 4000, "changeMaxSanity": -5} + }, + { + "name": "Ethical Breeding", + "cost": 145000, + "excludes": "Black Market Sales", + "style": "neutral", + "description":"Only breed and sell creatures ethically, some with pay the premium. Increases maximum gold returns.", + "effect": {"goldMaximumIncrease": 5000} + }, + { + + "name": "Extended Territory", + "description": "Increase the ranch size, accommodating a larger variety of monsters. Increases gold returns and max stockpile size.", + "cost": 210000, + "effect": {"goldMinimumIncrease": 1500, "goldMaximumIncrease": 3500, "maxGoldStockPileIncrease": 15000} + }, + { + "name": "Enchanted Enclosures", + "description": "Enhance the creatures' environments with magic to stimulate faster and more significant growth. Shortens time to market, increases gold returns, and max stockpile size.", + "cost": 255000, + "effect": {"turnsPerRollChange": -2, "goldMinimumIncrease": 1500,"goldMaximumIncrease": 3000, "maxGoldStockPileIncrease": 10000} + } + ] + }, + { + "name": "Crystal Mine", + "description": "Beneath ominous stone outcroppings, miners burrow deep below all other mines, discovering veins of shimmering crystal. Their glow is mesmerizing, and their magical potential immense.", + "requires": {"requirement": "", "message": "", "removes":false}, + "cost": 480000, + "turnsPerReturn": 20, + "goldReturnRange": {"min": 7000, "max": 12000}, + "maxGoldStockPile": 200000, + "upgrades": [ + { + "name": "Gem Laden Tools", + "cost": 195000, + "description":"Invest in enchanted tools to increase workers' success rate in mining. Increases gold returns.", + "effect": {"turnsPerRollChange": -3,"goldMinimumIncrease": 3000, "goldMaximumIncrease": 6000} + }, + { + "name": "Portal System", + "cost": 345000, + "description":"Hire magi to create a teleportation system. Significantly decreases time of extraction.", + "effect": {"turnsPerRollChange": -5} + }, + { + "name": "Explosive Mining", + "cost": 460000, + "excludes": "Seismic Monitoring", + "style": "neutral", + "description": "Utilize explosives for rapid extraction. Wildly increases maximum gold returns, decreases minimum gold return.", + "effect": {"turnsPerRollChange": -3,"goldMinimumIncrease": -2000, "goldMaximumIncrease": 16000} + }, + { + "name": "Seismic Monitoring", + "cost": 490000, + "excludes": "Profit Sharing", + "style": "good", + "description":"Hire earth magi to keep watch and temper seismic activity. Increases gold returns and maximum sanity.", + "effect": {"goldMinimumIncrease": 7500, "goldMaximumIncrease": 7500, "changeMaxSanity": 5} + } + ] + }, + { + "name": "Artifact Excavation Site", + "description": "Hidden beneath the millennia of earth lie ancient artifacts infused with forgotten magics. Each find promises wealth, power, and a chance to rewrite history.", + "requires": {"requirement": "", "message": "", "removes":false}, + "cost": 1850000, + "turnsPerReturn": 50, + "goldReturnRange": {"min": 28500, "max": 100000}, + "maxGoldStockPile": 500000, + "upgrades": [ + { + "name": "Magic Detectors", + "cost": 575000, + "description":"", + "effect": {"turnsPerRollChange": -5, "goldMinimumIncrease": 12000, "goldMaximumIncrease": 24000} + }, + { + "name": "Renowned Archeologists", + "cost": 1350000, + "description":"Hire the most preeminent archeolgy experts from around the world to overlook the excavation and to make sure nothing goes overlooked.", + "effect": {"turnsPerRollChange": -5, "goldMinimumIncrease": 18000, "goldMaximumIncrease": 40000} + }, + { + "name": "Develop Local Infrastructure", + "cost": 6500000, + "excludes": "Exploit Local Labor", + "style": "good", + "description": "You will be here a long time, building up local roads, hospitals and fire departments will aid both you and the locals. Reduces gold return, but speeds time to market and increases max sanity.", + "effect": {"turnsPerRollChange": -10,"goldMinimumIncrease": -2500, "goldMaximumIncrease": -2500, "changeMaxSanity": 5} + }, + { + "name": "Exploit Local Labor", + "cost": 5750000, + "excludes": "Develop Local Infrastructure", + "style": "evil", + "description": "Sabotage other local industries leaving the local population with few other options for work. Increases gold returns, and decreases both time to market and player max sanity.", + "effect": {"turnsPerRollChange": -5,"goldMinimumIncrease": 10000, "goldMaximumIncrease": 50000, "changeMaxSanity": -5} + }, + { + "name": "Rapid Excavation", + "cost": 2100000, + "excludes": "Careful Excavation", + "style": "neutral", + "description":"Nothing else matters than moving fast, you may miss things. Decreases time to market, but decreases minimum gold returns.", + "effect": {"turnsPerRollChange": -5,"goldMinimumIncrease": -15000} + }, + { + "name": "Careful Excavation", + "cost": 2100000, + "excludes": "Rapid Excavation", + "style": "neutral", + "description":"Make sure to miss nothing. Decreases time to market but increase returns.", + "effect": {"turnsPerRollChange": 5, "goldMinimumIncrease": 45000, "goldMaximumIncrease": 65000} + } + ] + } +] diff --git a/src/lineage-json/misc-route/jobs.json b/src/lineage-json/misc-route/jobs.json new file mode 100644 index 0000000..87a32e9 --- /dev/null +++ b/src/lineage-json/misc-route/jobs.json @@ -0,0 +1,89 @@ +[ + { + "title": "Broomstick Boxer", + "cost": { + "mana": 5 + }, + "qualifications": null, + "experienceToPromote": 50, + "reward": { + "gold": 20 + }, + "rankMultiplier": 0.15 + }, + { + "title": "Crusty Cauldron Cleaner", + "cost": { + "mana": 5, + "health": 3 + }, + "qualifications": null, + "experienceToPromote": 50, + "reward": { + "gold": 30 + }, + "rankMultiplier": 0.20 + }, + { + "title": "Apprentice Scribe", + "cost": { + "mana": 8 + }, + "qualifications": [ + "high school education" + ], + "experienceToPromote": 50, + "reward": { + "gold": 45 + }, + "rankMultiplier": 0.20 + }, + { + "title": "Imp CareTaker", + "cost": { + "mana": 7, + "health": 4 + }, + "qualifications": [ + "high school education" + ], + "experienceToPromote": 50, + "reward": { + "gold": 55 + }, + "rankMultiplier": 0.25 + }, + { + "title": "Private Dueling Partner", + "cost": { + "mana": 10, + "health": 4 + }, + "qualifications": [ + "high school education", + "defense course" + ], + "experienceToPromote": 50, + "reward": { + "gold": 90 + }, + "rankMultiplier": 0.25 + }, + { + "title": "Wild Monster Hunter", + "cost": { + "mana": 10, + "health": 6 + }, + "qualifications": [ + "high school education", + "defense course" + ], + "experienceToPromote": 50, + "reward": { + "gold": 120 + }, + "rankMultiplier": 0.30 + } +] + diff --git a/src/lineage-json/misc-route/manaOptions.json b/src/lineage-json/misc-route/manaOptions.json new file mode 100644 index 0000000..b6353da --- /dev/null +++ b/src/lineage-json/misc-route/manaOptions.json @@ -0,0 +1,12 @@ +[ + { + "serviceName": "Meditate On The Elements", + "cost": 0, + "manaRestore": 25 + }, + { + "serviceName": "Hire Shamanic Guide", + "cost": 150, + "manaRestore": "fill" + } +] diff --git a/src/lineage-json/misc-route/otherOptions.json b/src/lineage-json/misc-route/otherOptions.json new file mode 100644 index 0000000..e60b586 --- /dev/null +++ b/src/lineage-json/misc-route/otherOptions.json @@ -0,0 +1,12 @@ +[ + { + "serviceName": "Visit Witch Doctor", + "cost": 25, + "removeDebuffs": 1 + }, + { + "serviceName": "Seek Paladin Aid", + "cost": 150, + "removeDebuffs": 5 + } +] diff --git a/src/lineage-json/misc-route/pvpRewards.json b/src/lineage-json/misc-route/pvpRewards.json new file mode 100644 index 0000000..a96e97b --- /dev/null +++ b/src/lineage-json/misc-route/pvpRewards.json @@ -0,0 +1,24 @@ +[ + { + "id": 1, + "name": "respec potion", + "price": 5, + "icon": "flask", + "description": "gives the user the ability to respec their attributes" + }, + { + "id": 2, + "name": "tome of knowledge", + "price": 15, + "icon": "book", + "description": "provides 1 skill point" + }, + { + "id": 3, + "name": "potion of undeath", + "price": 20, + "icon": "potion", + "description": "reduces the users age by 1 year" + } +] + diff --git a/src/lineage-json/misc-route/sanityOptions.json b/src/lineage-json/misc-route/sanityOptions.json new file mode 100644 index 0000000..7c8a8e2 --- /dev/null +++ b/src/lineage-json/misc-route/sanityOptions.json @@ -0,0 +1,12 @@ +[ + { + "serviceName": "Pray", + "cost": 0, + "sanityRestore": 10 + }, + { + "serviceName": "Seek Spiritual Leader", + "cost": 200, + "sanityRestore": "fill" + } +] diff --git a/src/routes/api/trpc/[trpc].ts b/src/routes/api/trpc/[trpc].ts index 451d21a..c87dfe2 100644 --- a/src/routes/api/trpc/[trpc].ts +++ b/src/routes/api/trpc/[trpc].ts @@ -1,10 +1,11 @@ import type { APIEvent } from "@solidjs/start/server"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { appRouter } from "~/server/api/root"; +import { createTRPCContext } from "~/server/api/utils"; -const handler = (event: APIEvent) => +const handler = (event: APIEvent) => { // adapts tRPC to fetch API style requests - fetchRequestHandler({ + return fetchRequestHandler({ // the endpoint handling the requests endpoint: "/api/trpc", // the request object @@ -12,8 +13,9 @@ const handler = (event: APIEvent) => // the router for handling the requests router: appRouter, // any arbitrary data that should be available to all actions - createContext: () => event + createContext: () => createTRPCContext(event), }); +}; export const GET = handler; export const POST = handler; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index b548e11..12831b7 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -2,7 +2,7 @@ import { Typewriter } from "~/components/Typewriter"; export default function Home() { return ( - +
{/* fill in a ipsum lorem */} ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem diff --git a/src/routes/test.tsx b/src/routes/test.tsx new file mode 100644 index 0000000..6d2079a --- /dev/null +++ b/src/routes/test.tsx @@ -0,0 +1,249 @@ +import { createSignal, For, Show } from "solid-js"; + +type EndpointTest = { + name: string; + router: string; + procedure: string; + method: "query" | "mutation"; + input?: object; + description: string; +}; + +const endpoints: EndpointTest[] = [ + // JSON Service (no input needed) + { + name: "Get Attacks", + router: "lineage.jsonService", + procedure: "attacks", + method: "query", + description: "Get all attack data", + }, + { + name: "Get Conditions", + router: "lineage.jsonService", + procedure: "conditions", + method: "query", + description: "Get all condition data", + }, + { + name: "Get Dungeons", + router: "lineage.jsonService", + procedure: "dungeons", + method: "query", + description: "Get all dungeon data", + }, + { + name: "Get Enemies", + router: "lineage.jsonService", + procedure: "enemies", + method: "query", + description: "Get all enemy data", + }, + { + name: "Get Items", + router: "lineage.jsonService", + procedure: "items", + method: "query", + description: "Get all item data", + }, + { + name: "Get Misc", + router: "lineage.jsonService", + procedure: "misc", + method: "query", + description: "Get all misc data", + }, + + // Misc + { + name: "Offline Secret", + router: "lineage.misc", + procedure: "offlineSecret", + method: "query", + description: "Get offline serialization secret", + }, + + // PvP + { + name: "Get Opponents", + router: "lineage.pvp", + procedure: "getOpponents", + method: "query", + description: "Get 3 random PvP opponents", + }, +]; + +export default function TestPage() { + const [results, setResults] = createSignal>({}); + const [loading, setLoading] = createSignal>({}); + const [errors, setErrors] = createSignal>({}); + + const testEndpoint = async (endpoint: EndpointTest) => { + const key = `${endpoint.router}.${endpoint.procedure}`; + setLoading({ ...loading(), [key]: true }); + setErrors({ ...errors(), [key]: "" }); + + try { + const url = `/api/trpc/${endpoint.router}.${endpoint.procedure}`; + const response = await fetch(url, { + method: endpoint.method === "query" ? "GET" : "POST", + headers: endpoint.input ? { "Content-Type": "application/json" } : {}, + body: endpoint.input ? JSON.stringify(endpoint.input) : undefined, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${await response.text()}`); + } + + const data = await response.json(); + setResults({ ...results(), [key]: data }); + } catch (error: any) { + setErrors({ ...errors(), [key]: error.message }); + } finally { + setLoading({ ...loading(), [key]: false }); + } + }; + + const testAll = async () => { + for (const endpoint of endpoints) { + await testEndpoint(endpoint); + // Small delay to avoid overwhelming the server + await new Promise((resolve) => setTimeout(resolve, 100)); + } + }; + + return ( +
+
+
+

Lineage API Testing Dashboard

+

Test all migrated tRPC endpoints

+ + +
+ +
+ + {(endpoint) => { + const key = `${endpoint.router}.${endpoint.procedure}`; + return ( +
+
+
+

{endpoint.name}

+

+ {endpoint.description} +

+ + {key} + +
+ +
+ + +
+

Error:

+

{errors()[key]}

+
+
+ + +
+

+ Response: +

+
+                        {JSON.stringify(results()[key], null, 2)}
+                      
+
+
+
+ ); + }} +
+
+ +
+

Testing Instructions

+ +
+
+

+ ✅ Endpoints on This Page +

+

These endpoints require no authentication or setup:

+
    +
  • All JSON Service endpoints (game data)
  • +
  • Offline Secret
  • +
  • Get PvP Opponents
  • +
+
+ +
+

+ 🔐 Manual Testing Required +

+

+ For authentication-required endpoints, use curl or Postman: +

+ +
+
+

# Email Registration

+ + curl -X POST + http://localhost:3001/api/trpc/lineage.auth.emailRegistration + \ + +
+ -H "Content-Type: application/json" \ +
+ + {" "} + -d '{"{"} + "email":"test@example.com","password":"pass123","password_conf":"pass123" + {"}"}' + +
+ +
+

# Get Items (query)

+ + curl + http://localhost:3000/api/trpc/lineage.jsonService.items + +
+
+
+ +
+

📚 Full Documentation

+

+ See tasks/lineage-testing-guide.md for complete + testing instructions including: +

+
    +
  • All 27 endpoint examples
  • +
  • Authentication workflows
  • +
  • Database verification queries
  • +
  • Integration testing scenarios
  • +
+
+
+
+
+
+ ); +} diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index 3171a51..b1bc07d 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -1,34 +1,326 @@ import { createTRPCRouter, publicProcedure } from "../utils"; import { z } from "zod"; +import { TRPCError } from "@trpc/server"; +import { v4 as uuidV4 } from "uuid"; +import { env } from "~/env/server"; +import { ConnectionFactory } from "~/server/utils"; +import { SignJWT, jwtVerify } from "jose"; +import { setCookie } from "vinxi/http"; + +// Helper to create JWT token +async function createJWT(userId: string): Promise { + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const token = await new SignJWT({ id: userId }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime("14d") // 14 days + .sign(secret); + return token; +} + +// User type for database rows +interface User { + id: string; + email?: string; + display_name?: string; + provider?: string; + image?: string; + email_verified?: boolean; +} export const authRouter = createTRPCRouter({ // GitHub callback route githubCallback: publicProcedure - .query(async () => { - // Implementation for GitHub OAuth callback - return { message: "GitHub callback endpoint" }; + .input(z.object({ code: z.string() })) + .mutation(async ({ input, ctx }) => { + const { code } = input; + + try { + // Exchange code for access token + const tokenResponse = await fetch( + "https://github.com/login/oauth/access_token", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ + client_id: env.VITE_GITHUB_CLIENT_ID || env.NEXT_PUBLIC_GITHUB_CLIENT_ID, + client_secret: env.GITHUB_CLIENT_SECRET, + code, + }), + }, + ); + const { access_token } = await tokenResponse.json(); + + // Fetch user info from GitHub + const userResponse = await fetch("https://api.github.com/user", { + headers: { + Authorization: `token ${access_token}`, + }, + }); + + const user = await userResponse.json(); + const login = user.login; + const conn = ConnectionFactory(); + + // Check if user exists + const query = `SELECT * FROM User WHERE provider = ? AND display_name = ?`; + const params = ["github", login]; + const res = await conn.execute({ sql: query, args: params }); + + let userId: string; + + if (res.rows[0]) { + // User exists + userId = (res.rows[0] as unknown as User).id; + } else { + // Create new user + const icon = user.avatar_url; + const email = user.email; + userId = uuidV4(); + + const insertQuery = `INSERT INTO User (id, email, display_name, provider, image) VALUES (?, ?, ?, ?, ?)`; + const insertParams = [userId, email, login, "github", icon]; + await conn.execute({ sql: insertQuery, args: insertParams }); + } + + // Create JWT token + const token = await createJWT(userId); + + // Set cookie + setCookie(ctx.event.nativeEvent, "userIDToken", token, { + maxAge: 60 * 60 * 24 * 14, // 14 days + path: "/", + httpOnly: true, + secure: env.NODE_ENV === "production", + sameSite: "lax", + }); + + return { + success: true, + redirectTo: "/account", + }; + } catch (error) { + console.error("GitHub authentication failed:", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "GitHub authentication failed", + }); + } }), // Google callback route googleCallback: publicProcedure - .query(async () => { - // Implementation for Google OAuth callback - return { message: "Google callback endpoint" }; + .input(z.object({ code: z.string() })) + .mutation(async ({ input, ctx }) => { + const { code } = input; + + try { + // Exchange code for access token + const tokenResponse = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + code: code, + client_id: env.VITE_GOOGLE_CLIENT_ID || env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "", + client_secret: env.GOOGLE_CLIENT_SECRET, + redirect_uri: `${env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN}/api/auth/callback/google`, + grant_type: "authorization_code", + }), + }); + + const { access_token } = await tokenResponse.json(); + + // Fetch user info from Google + const userResponse = await fetch( + "https://www.googleapis.com/oauth2/v3/userinfo", + { + headers: { + Authorization: `Bearer ${access_token}`, + }, + }, + ); + + const userData = await userResponse.json(); + const name = userData.name; + const image = userData.picture; + const email = userData.email; + const email_verified = userData.email_verified; + + const conn = ConnectionFactory(); + + // Check if user exists + const query = `SELECT * FROM User WHERE provider = ? AND email = ?`; + const params = ["google", email]; + const res = await conn.execute({ sql: query, args: params }); + + let userId: string; + + if (res.rows[0]) { + // User exists + userId = (res.rows[0] as unknown as User).id; + } else { + // Create new user + userId = uuidV4(); + + const insertQuery = `INSERT INTO User (id, email, email_verified, display_name, provider, image) VALUES (?, ?, ?, ?, ?, ?)`; + const insertParams = [ + userId, + email, + email_verified, + name, + "google", + image, + ]; + await conn.execute({ + sql: insertQuery, + args: insertParams, + }); + } + + // Create JWT token + const token = await createJWT(userId); + + // Set cookie + setCookie(ctx.event.nativeEvent, "userIDToken", token, { + maxAge: 60 * 60 * 24 * 14, // 14 days + path: "/", + httpOnly: true, + secure: env.NODE_ENV === "production", + sameSite: "lax", + }); + + return { + success: true, + redirectTo: "/account", + }; + } catch (error) { + console.error("Google authentication failed:", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Google authentication failed", + }); + } }), // Email login route emailLogin: publicProcedure - .input(z.object({ email: z.string().email() })) - .mutation(async ({ input }) => { - // Implementation for email login - return { message: `Email login initiated for ${input.email}` }; + .input( + z.object({ + email: z.string().email(), + token: z.string(), + rememberMe: z.boolean().optional(), + }), + ) + .mutation(async ({ input, ctx }) => { + const { email, token, rememberMe } = input; + + try { + // Verify JWT token + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const { payload } = await jwtVerify(token, secret); + + // Check if email matches + if (payload.email !== email) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Email mismatch", + }); + } + + const conn = ConnectionFactory(); + const query = `SELECT * FROM User WHERE email = ?`; + const params = [email]; + const res = await conn.execute({ sql: query, args: params }); + + if (!res.rows[0]) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + + const userId = (res.rows[0] as unknown as User).id; + + // Create JWT token + const userToken = await createJWT(userId); + + // Set cookie based on rememberMe flag + const cookieOptions: any = { + path: "/", + httpOnly: true, + secure: env.NODE_ENV === "production", + sameSite: "lax", + }; + + if (rememberMe) { + cookieOptions.maxAge = 60 * 60 * 24 * 14; // 14 days + } + // If rememberMe is false, cookie will be session-only (no maxAge) + + setCookie(ctx.event.nativeEvent, "userIDToken", userToken, cookieOptions); + + return { + success: true, + redirectTo: "/account", + }; + } catch (error) { + if (error instanceof TRPCError) { + throw error; + } + console.error("Email login failed:", error); + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Authentication failed", + }); + } }), // Email verification route emailVerification: publicProcedure - .input(z.object({ email: z.string().email() })) - .query(async ({ input }) => { - // Implementation for email verification - return { message: `Email verification requested for ${input.email}` }; + .input( + z.object({ + email: z.string().email(), + token: z.string(), + }), + ) + .mutation(async ({ input }) => { + const { email, token } = input; + + try { + // Verify JWT token + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const { payload } = await jwtVerify(token, secret); + + // Check if email matches + if (payload.email !== email) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Email mismatch", + }); + } + + const conn = ConnectionFactory(); + const query = `UPDATE User SET email_verified = ? WHERE email = ?`; + const params = [true, email]; + await conn.execute({ sql: query, args: params }); + + return { + success: true, + message: "Email verification success, you may close this window", + }; + } catch (error) { + if (error instanceof TRPCError) { + throw error; + } + console.error("Email verification failed:", error); + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Invalid token", + }); + } }), }); \ No newline at end of file diff --git a/src/server/api/routers/database.ts b/src/server/api/routers/database.ts index b7a843e..2227a64 100644 --- a/src/server/api/routers/database.ts +++ b/src/server/api/routers/database.ts @@ -1,101 +1,563 @@ import { createTRPCRouter, publicProcedure } from "../utils"; import { z } from "zod"; +import { ConnectionFactory } from "~/server/utils"; +import { TRPCError } from "@trpc/server"; +import { env } from "~/env/server"; export const databaseRouter = createTRPCRouter({ - // Comment reactions routes + // ============================================================ + // Comment Reactions Routes + // ============================================================ + getCommentReactions: publicProcedure - .input(z.object({ commentId: z.string() })) - .query(({ input }) => { - // Implementation for getting comment reactions - return { commentId: input.commentId, reactions: [] }; - }), - - postCommentReaction: publicProcedure - .input(z.object({ - commentId: z.string(), - reactionType: z.string() - })) - .mutation(({ input }) => { - // Implementation for posting comment reaction - return { success: true, commentId: input.commentId }; - }), - - deleteCommentReaction: publicProcedure - .input(z.object({ - commentId: z.string(), - reactionType: z.string() - })) - .mutation(({ input }) => { - // Implementation for deleting comment reaction - return { success: true, commentId: input.commentId }; + .input(z.object({ commentID: z.string() })) + .query(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const query = "SELECT * FROM CommentReaction WHERE comment_id = ?"; + const results = await conn.execute({ + sql: query, + args: [input.commentID], + }); + return { commentReactions: results.rows }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch comment reactions", + }); + } }), - // Comments routes - getComments: publicProcedure - .input(z.object({ postId: z.string() })) - .query(({ input }) => { - // Implementation for getting comments - return { postId: input.postId, comments: [] }; + addCommentReaction: publicProcedure + .input(z.object({ + type: z.string(), + comment_id: z.string(), + user_id: z.string(), + })) + .mutation(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const query = ` + INSERT INTO CommentReaction (type, comment_id, user_id) + VALUES (?, ?, ?) + `; + await conn.execute({ + sql: query, + args: [input.type, input.comment_id, input.user_id], + }); + + const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`; + const res = await conn.execute({ + sql: followUpQuery, + args: [input.comment_id], + }); + + return { commentReactions: res.rows }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to add comment reaction", + }); + } }), - // Post manipulation routes - getPosts: publicProcedure - .input(z.object({ - limit: z.number().optional(), - offset: z.number().optional() + removeCommentReaction: publicProcedure + .input(z.object({ + type: z.string(), + comment_id: z.string(), + user_id: z.string(), })) - .query(({ input }) => { - // Implementation for getting posts - return { posts: [], total: 0 }; + .mutation(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const query = ` + DELETE FROM CommentReaction + WHERE type = ? AND comment_id = ? AND user_id = ? + `; + await conn.execute({ + sql: query, + args: [input.type, input.comment_id, input.user_id], + }); + + const followUpQuery = `SELECT * FROM CommentReaction WHERE comment_id = ?`; + const res = await conn.execute({ + sql: followUpQuery, + args: [input.comment_id], + }); + + return { commentReactions: res.rows }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to remove comment reaction", + }); + } }), - + + // ============================================================ + // Comments Routes + // ============================================================ + + getAllComments: publicProcedure + .query(async () => { + try { + const conn = ConnectionFactory(); + const query = `SELECT * FROM Comment`; + const res = await conn.execute(query); + return { comments: res.rows }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch comments", + }); + } + }), + + getCommentsByPostId: publicProcedure + .input(z.object({ post_id: z.string() })) + .query(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const query = `SELECT * FROM Comment WHERE post_id = ?`; + const res = await conn.execute({ + sql: query, + args: [input.post_id], + }); + return { comments: res.rows }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch comments by post ID", + }); + } + }), + + // ============================================================ + // Post Routes + // ============================================================ + + getPostById: publicProcedure + .input(z.object({ + category: z.enum(["blog", "project"]), + id: z.number(), + })) + .query(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const query = `SELECT * FROM Post WHERE id = ?`; + const results = await conn.execute({ + sql: query, + args: [input.id], + }); + + const tagQuery = `SELECT * FROM Tag WHERE post_id = ?`; + const tagRes = await conn.execute({ + sql: tagQuery, + args: [input.id], + }); + + if (results.rows[0]) { + return { + post: results.rows[0], + tags: tagRes.rows, + }; + } else { + return { post: null, tags: [] }; + } + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch post by ID", + }); + } + }), + + getPostByTitle: publicProcedure + .input(z.object({ + category: z.enum(["blog", "project"]), + title: z.string(), + })) + .query(async ({ input, ctx }) => { + try { + const conn = ConnectionFactory(); + + // Get post by title + const postQuery = "SELECT * FROM Post WHERE title = ? AND category = ? AND published = ?"; + const postResults = await conn.execute({ + sql: postQuery, + args: [input.title, input.category, true], + }); + + if (!postResults.rows[0]) { + return null; + } + + const post_id = (postResults.rows[0] as any).id; + + // Get comments + const commentQuery = "SELECT * FROM Comment WHERE post_id = ?"; + const commentResults = await conn.execute({ + sql: commentQuery, + args: [post_id], + }); + + // Get likes + const likeQuery = "SELECT * FROM PostLike WHERE post_id = ?"; + const likeResults = await conn.execute({ + sql: likeQuery, + args: [post_id], + }); + + // Get tags + const tagsQuery = "SELECT * FROM Tag WHERE post_id = ?"; + const tagResults = await conn.execute({ + sql: tagsQuery, + args: [post_id], + }); + + return { + project: postResults.rows[0], + comments: commentResults.rows, + likes: likeResults.rows, + tagResults: tagResults.rows, + }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch post by title", + }); + } + }), + createPost: publicProcedure - .input(z.object({ - title: z.string(), - content: z.string() + .input(z.object({ + category: z.enum(["blog", "project"]), + title: z.string(), + subtitle: z.string().nullable(), + body: z.string().nullable(), + banner_photo: z.string().nullable(), + published: z.boolean(), + tags: z.array(z.string()).nullable(), + author_id: z.string(), })) - .mutation(({ input }) => { - // Implementation for creating post - return { success: true, post: { id: "1", ...input } }; - }), - - updatePost: publicProcedure - .input(z.object({ - id: z.string(), - title: z.string().optional(), - content: z.string().optional() - })) - .mutation(({ input }) => { - // Implementation for updating post - return { success: true, postId: input.id }; - }), - - deletePost: publicProcedure - .input(z.object({ id: z.string() })) - .mutation(({ input }) => { - // Implementation for deleting post - return { success: true, postId: input.id }; + .mutation(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const fullURL = input.banner_photo + ? env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.banner_photo + : null; + + const query = ` + INSERT INTO Post (title, category, subtitle, body, banner_photo, published, author_id) + VALUES (?, ?, ?, ?, ?, ?, ?) + `; + const params = [ + input.title, + input.category, + input.subtitle, + input.body, + fullURL, + input.published, + input.author_id, + ]; + + const results = await conn.execute({ sql: query, args: params }); + + if (input.tags && input.tags.length > 0) { + let tagQuery = "INSERT INTO Tag (value, post_id) VALUES "; + let values = input.tags.map( + (tag) => `("${tag}", ${results.lastInsertRowid})` + ); + tagQuery += values.join(", "); + await conn.execute(tagQuery); + } + + return { data: results.lastInsertRowid }; + } catch (error) { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to create post", + }); + } }), - // Post likes routes - getPostLikes: publicProcedure - .input(z.object({ postId: z.string() })) - .query(({ input }) => { - // Implementation for getting post likes - return { postId: input.postId, likes: [] }; + updatePost: publicProcedure + .input(z.object({ + id: z.number(), + title: z.string().nullable().optional(), + subtitle: z.string().nullable().optional(), + body: z.string().nullable().optional(), + banner_photo: z.string().nullable().optional(), + published: z.boolean().nullable().optional(), + tags: z.array(z.string()).nullable().optional(), + author_id: z.string(), + })) + .mutation(async ({ input }) => { + try { + const conn = ConnectionFactory(); + + let query = "UPDATE Post SET "; + let params: any[] = []; + let first = true; + + if (input.title !== undefined && input.title !== null) { + query += first ? "title = ?" : ", title = ?"; + params.push(input.title); + first = false; + } + + if (input.subtitle !== undefined && input.subtitle !== null) { + query += first ? "subtitle = ?" : ", subtitle = ?"; + params.push(input.subtitle); + first = false; + } + + if (input.body !== undefined && input.body !== null) { + query += first ? "body = ?" : ", body = ?"; + params.push(input.body); + first = false; + } + + if (input.banner_photo !== undefined && input.banner_photo !== null) { + query += first ? "banner_photo = ?" : ", banner_photo = ?"; + if (input.banner_photo === "_DELETE_IMAGE_") { + params.push(null); + } else { + params.push(env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.banner_photo); + } + first = false; + } + + if (input.published !== undefined && input.published !== null) { + query += first ? "published = ?" : ", published = ?"; + params.push(input.published); + first = false; + } + + query += first ? "author_id = ?" : ", author_id = ?"; + params.push(input.author_id); + + query += " WHERE id = ?"; + params.push(input.id); + + const results = await conn.execute({ sql: query, args: params }); + + // Handle tags + const deleteTagsQuery = `DELETE FROM Tag WHERE post_id = ?`; + await conn.execute({ sql: deleteTagsQuery, args: [input.id.toString()] }); + + if (input.tags && input.tags.length > 0) { + let tagQuery = "INSERT INTO Tag (value, post_id) VALUES "; + let values = input.tags.map((tag) => `("${tag}", ${input.id})`); + tagQuery += values.join(", "); + await conn.execute(tagQuery); + } + + return { data: results.lastInsertRowid }; + } catch (error) { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to update post", + }); + } }), - - likePost: publicProcedure - .input(z.object({ postId: z.string() })) - .mutation(({ input }) => { - // Implementation for liking post - return { success: true, postId: input.postId }; + + // ============================================================ + // Post Likes Routes + // ============================================================ + + addPostLike: publicProcedure + .input(z.object({ + user_id: z.string(), + post_id: z.string(), + })) + .mutation(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const query = `INSERT INTO PostLike (user_id, post_id) VALUES (?, ?)`; + await conn.execute({ + sql: query, + args: [input.user_id, input.post_id], + }); + + const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`; + const res = await conn.execute({ + sql: followUpQuery, + args: [input.post_id], + }); + + return { newLikes: res.rows }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to add post like", + }); + } }), - - unlikePost: publicProcedure - .input(z.object({ postId: z.string() })) - .mutation(({ input }) => { - // Implementation for unliking post - return { success: true, postId: input.postId }; + + removePostLike: publicProcedure + .input(z.object({ + user_id: z.string(), + post_id: z.string(), + })) + .mutation(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const query = ` + DELETE FROM PostLike + WHERE user_id = ? AND post_id = ? + `; + await conn.execute({ + sql: query, + args: [input.user_id, input.post_id], + }); + + const followUpQuery = `SELECT * FROM PostLike WHERE post_id = ?`; + const res = await conn.execute({ + sql: followUpQuery, + args: [input.post_id], + }); + + return { newLikes: res.rows }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to remove post like", + }); + } }), -}); \ No newline at end of file + + // ============================================================ + // User Routes + // ============================================================ + + getUserById: publicProcedure + .input(z.object({ id: z.string() })) + .query(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const query = "SELECT * FROM User WHERE id = ?"; + const res = await conn.execute({ + sql: query, + args: [input.id], + }); + + if (res.rows[0]) { + const user = res.rows[0] as any; + if (user && user.display_name !== "user deleted") { + return { + id: user.id, + email: user.email, + emailVerified: user.email_verified, + image: user.image, + displayName: user.display_name, + provider: user.provider, + hasPassword: !!user.password_hash, + }; + } + } + return null; + } catch (error) { + console.error(error); + return null; + } + }), + + getUserPublicData: publicProcedure + .input(z.object({ id: z.string() })) + .query(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const query = "SELECT email, display_name, image FROM User WHERE id = ?"; + const res = await conn.execute({ + sql: query, + args: [input.id], + }); + + if (res.rows[0]) { + const user = res.rows[0] as any; + if (user && user.display_name !== "user deleted") { + return { + email: user.email, + image: user.image, + display_name: user.display_name, + }; + } + } + return null; + } catch (error) { + console.error(error); + return null; + } + }), + + getUserImage: publicProcedure + .input(z.object({ id: z.string() })) + .query(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const query = "SELECT * FROM User WHERE id = ?"; + const results = await conn.execute({ + sql: query, + args: [input.id], + }); + return { user: results.rows[0] }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch user image", + }); + } + }), + + updateUserImage: publicProcedure + .input(z.object({ + id: z.string(), + imageURL: z.string(), + })) + .mutation(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const fullURL = input.imageURL + ? env.NEXT_PUBLIC_AWS_BUCKET_STRING + input.imageURL + : null; + const query = `UPDATE User SET image = ? WHERE id = ?`; + await conn.execute({ + sql: query, + args: [fullURL, input.id], + }); + return { res: "success" }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to update user image", + }); + } + }), + + updateUserEmail: publicProcedure + .input(z.object({ + id: z.string(), + newEmail: z.string().email(), + oldEmail: z.string().email(), + })) + .mutation(async ({ input }) => { + try { + const conn = ConnectionFactory(); + const query = `UPDATE User SET email = ? WHERE id = ? AND email = ?`; + const res = await conn.execute({ + sql: query, + args: [input.newEmail, input.id, input.oldEmail], + }); + return { res }; + } catch (error) { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to update user email", + }); + } + }), +}); diff --git a/src/server/api/routers/example.ts b/src/server/api/routers/example.ts index 4e171b9..7cb60c2 100644 --- a/src/server/api/routers/example.ts +++ b/src/server/api/routers/example.ts @@ -1,11 +1,34 @@ import { wrap } from "@typeschema/valibot"; import { string } from "valibot"; -import { createTRPCRouter, publicProcedure } from "../utils"; +import { + createTRPCRouter, + publicProcedure, + protectedProcedure, + adminProcedure +} from "../utils"; export const exampleRouter = createTRPCRouter({ hello: publicProcedure .input(wrap(string())) .query(({ input }) => { return `Hello ${input}!`; - }) + }), + + // Example of a protected procedure (requires authentication) + getProfile: protectedProcedure.query(({ ctx }) => { + return { + userId: ctx.userId, + privilegeLevel: ctx.privilegeLevel, + message: "You are authenticated!", + }; + }), + + // Example of an admin-only procedure + adminDashboard: adminProcedure.query(({ ctx }) => { + return { + userId: ctx.userId, + message: "Welcome to the admin dashboard!", + isAdmin: true, + }; + }), }); diff --git a/src/server/api/routers/lineage.ts b/src/server/api/routers/lineage.ts index 0723489..0ba8961 100644 --- a/src/server/api/routers/lineage.ts +++ b/src/server/api/routers/lineage.ts @@ -1,124 +1,27 @@ -import { createTRPCRouter, publicProcedure } from "../utils"; -import { z } from "zod"; +import { createTRPCRouter } from "../utils"; +import { lineageAuthRouter } from "./lineage/auth"; +import { lineageDatabaseRouter } from "./lineage/database"; +import { lineageJsonServiceRouter } from "./lineage/json-service"; +import { lineageMiscRouter } from "./lineage/misc"; +import { lineagePvpRouter } from "./lineage/pvp"; +import { lineageMaintenanceRouter } from "./lineage/maintenance"; export const lineageRouter = createTRPCRouter({ - // Database management routes (GET) - databaseManagement: publicProcedure - .query(async () => { - // Implementation for database management - return { message: "Database management endpoint" }; - }), + // Authentication + auth: lineageAuthRouter, - // Analytics route (GET) - analytics: publicProcedure - .query(async () => { - // Implementation for analytics - return { message: "Analytics endpoint" }; - }), + // Database Management + database: lineageDatabaseRouter, - // Apple authentication routes (GET) - appleAuth: publicProcedure - .query(async () => { - // Implementation for Apple authentication - return { message: "Apple authentication endpoint" }; - }), + // PvP + pvp: lineagePvpRouter, - // Email login/registration/verification routes (GET/POST) - emailLogin: publicProcedure - .input(z.object({ email: z.string().email(), password: z.string() })) - .mutation(async ({ input }) => { - // Implementation for email login - return { message: `Email login for ${input.email}` }; - }), + // JSON Service + jsonService: lineageJsonServiceRouter, - emailRegister: publicProcedure - .input(z.object({ email: z.string().email(), password: z.string() })) - .mutation(async ({ input }) => { - // Implementation for email registration - return { message: `Email registration for ${input.email}` }; - }), + // Misc (Analytics, Tokens, etc.) + misc: lineageMiscRouter, - emailVerify: publicProcedure - .input(z.object({ token: z.string() })) - .mutation(async ({ input }) => { - // Implementation for email verification - return { message: "Email verification endpoint" }; - }), - - // Google registration route (POST) - googleRegister: publicProcedure - .input(z.object({ - googleId: z.string(), - email: z.string().email(), - name: z.string() - })) - .mutation(async ({ input }) => { - // Implementation for Google registration - return { message: `Google registration for ${input.email}` }; - }), - - // JSON service routes (GET - attacks, conditions, dungeons, enemies, items, misc) - attacks: publicProcedure - .query(async () => { - // Implementation for attacks data - return { message: "Attacks data" }; - }), - - conditions: publicProcedure - .query(async () => { - // Implementation for conditions data - return { message: "Conditions data" }; - }), - - dungeons: publicProcedure - .query(async () => { - // Implementation for dungeons data - return { message: "Dungeons data" }; - }), - - enemies: publicProcedure - .query(async () => { - // Implementation for enemies data - return { message: "Enemies data" }; - }), - - items: publicProcedure - .query(async () => { - // Implementation for items data - return { message: "Items data" }; - }), - - misc: publicProcedure - .query(async () => { - // Implementation for miscellaneous data - return { message: "Miscellaneous data" }; - }), - - // Offline secret route (GET) - offlineSecret: publicProcedure - .query(async () => { - // Implementation for offline secret - return { message: "Offline secret endpoint" }; - }), - - // PvP routes (GET/POST) - pvpGet: publicProcedure - .query(async () => { - // Implementation for PvP GET - return { message: "PvP GET endpoint" }; - }), - - pvpPost: publicProcedure - .input(z.object({ player1: z.string(), player2: z.string() })) - .mutation(async ({ input }) => { - // Implementation for PvP POST - return { message: `PvP battle between ${input.player1} and ${input.player2}` }; - }), - - // Tokens route (GET) - tokens: publicProcedure - .query(async () => { - // Implementation for tokens - return { message: "Tokens endpoint" }; - }), + // Maintenance (Protected) + maintenance: lineageMaintenanceRouter, }); \ No newline at end of file diff --git a/src/server/api/routers/lineage/auth.ts b/src/server/api/routers/lineage/auth.ts new file mode 100644 index 0000000..32b5e7a --- /dev/null +++ b/src/server/api/routers/lineage/auth.ts @@ -0,0 +1,493 @@ +import { createTRPCRouter, publicProcedure } from "../../utils"; +import { z } from "zod"; +import { + LineageConnectionFactory, + LineageDBInit, + hashPassword, + checkPassword, + sendEmailVerification, + LINEAGE_JWT_EXPIRY, +} from "~/server/utils"; +import { env } from "~/env/server"; +import { TRPCError } from "@trpc/server"; +import { SignJWT, jwtVerify } from "jose"; +import { LibsqlError } from "@libsql/client/web"; +import { createClient as createAPIClient } from "@tursodatabase/api"; + +export const lineageAuthRouter = createTRPCRouter({ + emailLogin: publicProcedure + .input( + z.object({ + email: z.string().email(), + password: z.string().min(8), + }) + ) + .mutation(async ({ input }) => { + const { email, password } = input; + + const conn = LineageConnectionFactory(); + const query = `SELECT * FROM User WHERE email = ? AND provider = ? LIMIT 1`; + const params = [email, "email"]; + const res = await conn.execute({ sql: query, args: params }); + + if (res.rows.length === 0) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Invalid Credentials", + }); + } + + const user = res.rows[0]; + + if (user.email_verified === 0) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Email not yet verified!", + }); + } + + const valid = await checkPassword(password, user.password_hash as string); + if (!valid) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Invalid Credentials", + }); + } + + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const token = await new SignJWT({ userId: user.id, email: user.email }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime(LINEAGE_JWT_EXPIRY) + .sign(secret); + + return { + success: true, + message: "Login successful", + token, + email, + }; + }), + + emailRegistration: publicProcedure + .input( + z.object({ + email: z.string().email(), + password: z.string().min(8), + password_conf: z.string().min(8), + }) + ) + .mutation(async ({ input }) => { + const { email, password, password_conf } = input; + + if (password !== password_conf) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Password mismatch", + }); + } + + const passwordHash = await hashPassword(password); + const conn = LineageConnectionFactory(); + const userCreationQuery = ` + INSERT INTO User (email, provider, password_hash) + VALUES (?, ?, ?) + `; + const params = [email, "email", passwordHash]; + + try { + await conn.execute({ sql: userCreationQuery, args: params }); + + const emailResult = await sendEmailVerification(email); + if (emailResult.success && emailResult.messageId) { + return { + success: true, + message: "Email verification sent!", + }; + } else { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: emailResult.message || "Failed to send verification email", + }); + } + } catch (e) { + console.error(e); + if (e instanceof LibsqlError && e.code === "SQLITE_CONSTRAINT") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "User already exists", + }); + } + if (e instanceof TRPCError) throw e; + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while creating the user", + }); + } + }), + + emailVerification: publicProcedure + .input( + z.object({ + email: z.string().email(), + token: z.string(), + }) + ) + .mutation(async ({ input }) => { + const { email: userEmail, token } = input; + + let conn; + let dbName; + let dbToken; + + try { + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const { payload } = await jwtVerify(token, secret); + + if (payload.email !== userEmail) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Authentication failed: email mismatch", + }); + } + + conn = LineageConnectionFactory(); + const dbInit = await LineageDBInit(); + dbName = dbInit.dbName; + dbToken = dbInit.token; + + const query = `UPDATE User SET email_verified = ?, database_name = ?, database_token = ? WHERE email = ?`; + const queryParams = [true, dbName, dbToken, userEmail]; + const res = await conn.execute({ sql: query, args: queryParams }); + + if (res.rowsAffected === 0) { + throw new Error("User not found or update failed"); + } + + return { + success: true, + message: + "Email verification success. You may close this window and sign in within the app.", + }; + } catch (err) { + console.error("Error in email verification:", err); + + if (dbName) { + try { + const turso = createAPIClient({ + org: "mikefreno", + token: env.TURSO_DB_API_TOKEN, + }); + await turso.databases.delete(dbName); + console.log(`Database ${dbName} deleted due to error`); + } catch (deleteErr) { + console.error("Error deleting database:", deleteErr); + } + } + + if (conn) { + try { + await conn.execute({ + sql: `UPDATE User SET email_verified = ?, database_name = ?, database_token = ? WHERE email = ?`, + args: [false, null, null, userEmail], + }); + console.log("User table update reverted"); + } catch (revertErr) { + console.error("Error reverting User table update:", revertErr); + } + } + + if (err instanceof TRPCError) throw err; + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "Authentication failed: An error occurred during email verification. Please try again.", + }); + } + }), + + refreshVerification: publicProcedure + .input(z.object({ email: z.string().email() })) + .mutation(async ({ input }) => { + const { email } = input; + const conn = LineageConnectionFactory(); + const query = "SELECT * FROM User WHERE email = ?"; + const params = [email]; + + const res = await conn.execute({ sql: query, args: params }); + + if (res.rows.length === 0 || res.rows[0].email_verified) { + throw new TRPCError({ + code: "CONFLICT", + message: "Invalid Request", + }); + } + + const emailResult = await sendEmailVerification(email); + if (emailResult.success && emailResult.messageId) { + return { + success: true, + message: "Email verification sent!", + }; + } else { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: emailResult.message || "Failed to send verification email", + }); + } + }), + + refreshToken: publicProcedure + .input(z.object({ token: z.string() })) + .query(async ({ input }) => { + const { token } = input; + + try { + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const { payload } = await jwtVerify(token, secret); + + const newToken = await new SignJWT({ + userId: payload.userId, + email: payload.email, + }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime(LINEAGE_JWT_EXPIRY) + .sign(secret); + + return { + status: 200, + ok: true, + valid: true, + token: newToken, + email: payload.email, + }; + } catch (error) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Invalid or expired token", + }); + } + }), + + googleRegistration: publicProcedure + .input(z.object({ email: z.string().email() })) + .mutation(async ({ input }) => { + const { email } = input; + + const conn = LineageConnectionFactory(); + + try { + const checkUserQuery = "SELECT * FROM User WHERE email = ?"; + const checkUserResult = await conn.execute({ + sql: checkUserQuery, + args: [email], + }); + + if (checkUserResult.rows.length > 0) { + const updateQuery = ` + UPDATE User + SET provider = ? + WHERE email = ? + `; + const updateRes = await conn.execute({ + sql: updateQuery, + args: ["google", email], + }); + + if (updateRes.rowsAffected !== 0) { + return { + success: true, + message: "User information updated", + }; + } else { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "User update failed!", + }); + } + } else { + let db_name; + try { + const { token, dbName } = await LineageDBInit(); + db_name = dbName; + console.log("init success"); + const insertQuery = ` + INSERT INTO User (email, email_verified, provider, database_name, database_token) + VALUES (?, ?, ?, ?, ?) + `; + await conn.execute({ + sql: insertQuery, + args: [email, true, "google", dbName, token], + }); + + console.log("insert success"); + + return { + success: true, + message: "New user created", + }; + } catch (error) { + if (db_name) { + const turso = createAPIClient({ + org: "mikefreno", + token: env.TURSO_DB_API_TOKEN, + }); + await turso.databases.delete(db_name); + } + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to create user", + }); + } + } + } catch (error) { + console.error("Error in Google Sign-Up handler:", error); + if (error instanceof TRPCError) throw error; + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while processing the request", + }); + } + }), + + appleRegistration: publicProcedure + .input( + z.object({ + email: z.string().email().optional(), + userString: z.string(), + }) + ) + .mutation(async ({ input }) => { + const { email, userString } = input; + + let dbName; + let dbToken; + const conn = LineageConnectionFactory(); + + try { + let checkUserQuery = "SELECT * FROM User WHERE apple_user_string = ?"; + + let args: string[] = [userString]; + if (email) { + args.push(email); + checkUserQuery += " OR email = ?"; + } + const checkUserResult = await conn.execute({ + sql: checkUserQuery, + args: args, + }); + + if (checkUserResult.rows.length > 0) { + const setClauses = []; + const values = []; + + if (email) { + setClauses.push("email = ?"); + values.push(email); + } + setClauses.push("provider = ?", "apple_user_string = ?"); + values.push("apple", userString); + const whereClause = `WHERE apple_user_string = ?${ + email ? " OR email = ?" : "" + }`; + values.push(userString); + if (email) { + values.push(email); + } + + const updateQuery = `UPDATE User SET ${setClauses.join( + ", " + )} ${whereClause}`; + const updateRes = await conn.execute({ + sql: updateQuery, + args: values, + }); + + if (updateRes.rowsAffected !== 0) { + return { + success: true, + message: "User information updated", + email: checkUserResult.rows[0].email as string, + }; + } else { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "User update failed!", + }); + } + } else { + const dbInit = await LineageDBInit(); + dbToken = dbInit.token; + dbName = dbInit.dbName; + + try { + const insertQuery = ` + INSERT INTO User (email, email_verified, apple_user_string, provider, database_name, database_token) + VALUES (?, ?, ?, ?, ?, ?) + `; + await conn.execute({ + sql: insertQuery, + args: [email, true, userString, "apple", dbName, dbToken], + }); + + return { + success: true, + message: "New user created", + dbName, + dbToken, + }; + } catch (error) { + if (dbName) { + const turso = createAPIClient({ + org: "mikefreno", + token: env.TURSO_DB_API_TOKEN, + }); + await turso.databases.delete(dbName); + } + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to create user", + }); + } + } + } catch (error) { + if (dbName) { + try { + const turso = createAPIClient({ + org: "mikefreno", + token: env.TURSO_DB_API_TOKEN, + }); + await turso.databases.delete(dbName); + } catch (deleteErr) { + console.error("Error deleting database:", deleteErr); + } + } + console.error("Error in Apple Sign-Up handler:", error); + if (error instanceof TRPCError) throw error; + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "An error occurred while processing the request", + }); + } + }), + + appleGetEmail: publicProcedure + .input(z.object({ userString: z.string() })) + .mutation(async ({ input }) => { + const { userString } = input; + + const conn = LineageConnectionFactory(); + const query = "SELECT * FROM User WHERE apple_user_string = ?"; + const res = await conn.execute({ sql: query, args: [userString] }); + + if (res.rows.length > 0) { + return { success: true, email: res.rows[0].email as string }; + } else { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + }), +}); diff --git a/src/server/api/routers/lineage/database.ts b/src/server/api/routers/lineage/database.ts new file mode 100644 index 0000000..41526e5 --- /dev/null +++ b/src/server/api/routers/lineage/database.ts @@ -0,0 +1,393 @@ +import { createTRPCRouter, publicProcedure } from "../../utils"; +import { z } from "zod"; +import { + LineageConnectionFactory, + validateLineageRequest, + dumpAndSendDB, +} from "~/server/utils"; +import { env } from "~/env/server"; +import { TRPCError } from "@trpc/server"; +import { OAuth2Client } from "google-auth-library"; +import { jwtVerify } from "jose"; + +export const lineageDatabaseRouter = createTRPCRouter({ + credentials: publicProcedure + .input( + z.object({ + email: z.string().email(), + provider: z.enum(["email", "google", "apple"]), + authToken: z.string(), + }) + ) + .mutation(async ({ input }) => { + const { email, provider, authToken } = input; + + try { + let valid_request = false; + + if (provider === "email") { + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const { payload } = await jwtVerify(authToken, secret); + if (payload.email === email) { + valid_request = true; + } + } else if (provider === "google") { + const CLIENT_ID = env.VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE || env.NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE; + if (!CLIENT_ID) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Google client ID not configured", + }); + } + const client = new OAuth2Client(CLIENT_ID); + const ticket = await client.verifyIdToken({ + idToken: authToken, + audience: CLIENT_ID, + }); + if (ticket.getPayload()?.email === email) { + valid_request = true; + } + } else { + const conn = LineageConnectionFactory(); + const query = "SELECT * FROM User WHERE apple_user_string = ?"; + const res = await conn.execute({ sql: query, args: [authToken] }); + if (res.rows.length > 0 && res.rows[0].email === email) { + valid_request = true; + } + } + + if (valid_request) { + const conn = LineageConnectionFactory(); + const query = "SELECT * FROM User WHERE email = ? LIMIT 1"; + const params = [email]; + const res = await conn.execute({ sql: query, args: params }); + + if (res.rows.length === 1) { + const user = res.rows[0]; + return { + success: true, + db_name: user.database_name as string, + db_token: user.database_token as string, + }; + } + + throw new TRPCError({ + code: "NOT_FOUND", + message: "No user found", + }); + } else { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Invalid credentials", + }); + } + } catch (error) { + if (error instanceof TRPCError) throw error; + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Authentication failed", + }); + } + }), + + deletionInit: publicProcedure + .input( + z.object({ + email: z.string().email(), + db_name: z.string(), + db_token: z.string(), + authToken: z.string(), + skip_cron: z.boolean().optional(), + send_dump_target: z.string().email().optional(), + }) + ) + .mutation(async ({ input }) => { + const { email, db_name, db_token, authToken, skip_cron, send_dump_target } = input; + + const conn = LineageConnectionFactory(); + const res = await conn.execute({ + sql: `SELECT * FROM User WHERE email = ?`, + args: [email], + }); + const userRow = res.rows[0]; + + if (!userRow) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + + const valid = await validateLineageRequest({ + auth_token: authToken, + userRow, + }); + + if (!valid) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Invalid Verification", + }); + } + + const { database_token, database_name } = userRow; + + if (database_token !== db_token || database_name !== db_name) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Incorrect Verification", + }); + } + + if (skip_cron) { + if (send_dump_target) { + const dumpRes = await dumpAndSendDB({ + dbName: db_name, + dbToken: db_token, + sendTarget: send_dump_target, + }); + + if (dumpRes.success) { + const deleteRes = await fetch( + `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, + }, + } + ); + + if (deleteRes.ok) { + await conn.execute({ + sql: `DELETE FROM User WHERE email = ?`, + args: [email], + }); + return { + ok: true, + status: 200, + message: `Account and Database deleted, db dump sent to email: ${send_dump_target}`, + }; + } else { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to delete database", + }); + } + } else { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: dumpRes.reason || "Failed to dump database", + }); + } + } else { + const deleteRes = await fetch( + `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, + }, + } + ); + + if (deleteRes.ok) { + await conn.execute({ + sql: `DELETE FROM User WHERE email = ?`, + args: [email], + }); + return { + ok: true, + status: 200, + message: `Account and Database deleted`, + }; + } else { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to delete database", + }); + } + } + } else { + const insertRes = await conn.execute({ + sql: `INSERT INTO cron (email, db_name, db_token, send_dump_target) VALUES (?, ?, ?, ?)`, + args: [email, db_name, db_token, send_dump_target], + }); + + if (insertRes.rowsAffected > 0) { + return { + ok: true, + status: 200, + message: `Deletion scheduled.`, + }; + } else { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Deletion not scheduled, due to server failure`, + }); + } + } + }), + + deletionCheck: publicProcedure + .input(z.object({ email: z.string().email() })) + .mutation(async ({ input }) => { + const { email } = input; + const conn = LineageConnectionFactory(); + + try { + const res = await conn.execute({ + sql: `SELECT * FROM cron WHERE email = ?`, + args: [email], + }); + const cronRow = res.rows[0]; + + if (!cronRow) { + return { status: 204, ok: true }; + } + + return { + ok: true, + status: 200, + created_at: cronRow.created_at as string, + }; + } catch (e) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to check deletion status", + }); + } + }), + + deletionCancel: publicProcedure + .input( + z.object({ + email: z.string().email(), + authToken: z.string(), + }) + ) + .mutation(async ({ input }) => { + const { email, authToken } = input; + + const conn = LineageConnectionFactory(); + + const resUser = await conn.execute({ + sql: `SELECT * FROM User WHERE email = ?;`, + args: [email], + }); + + if (resUser.rows.length === 0) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found.", + }); + } + + const userRow = resUser.rows[0]; + + const valid = await validateLineageRequest({ + auth_token: authToken, + userRow, + }); + + if (!valid) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Invalid credentials for cancelation.", + }); + } + + const result = await conn.execute({ + sql: `DELETE FROM cron WHERE email = ?;`, + args: [email], + }); + + if (result.rowsAffected > 0) { + return { + status: 200, + ok: true, + message: "Cron job(s) canceled successfully.", + }; + } else { + throw new TRPCError({ + code: "NOT_FOUND", + message: "No cron job found for the given email.", + }); + } + }), + + deletionCron: publicProcedure.query(async () => { + const conn = LineageConnectionFactory(); + const res = await conn.execute( + `SELECT * FROM cron WHERE created_at <= datetime('now', '-1 day');` + ); + + if (res.rows.length > 0) { + const executed_ids: (number | string)[] = []; + + for (const row of res.rows) { + const { id, db_name, db_token, send_dump_target, email } = row; + + if (send_dump_target) { + const dumpRes = await dumpAndSendDB({ + dbName: db_name as string, + dbToken: db_token as string, + sendTarget: send_dump_target as string, + }); + + if (dumpRes.success) { + const deleteRes = await fetch( + `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, + }, + } + ); + + if (deleteRes.ok) { + await conn.execute({ + sql: `DELETE FROM User WHERE email = ?`, + args: [email], + }); + executed_ids.push(id as number); + } + } + } else { + const deleteRes = await fetch( + `https://api.turso.tech/v1/organizations/mikefreno/databases/${db_name}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${env.TURSO_DB_API_TOKEN}`, + }, + } + ); + + if (deleteRes.ok) { + await conn.execute({ + sql: `DELETE FROM User WHERE email = ?`, + args: [email], + }); + executed_ids.push(id as number); + } + } + } + + if (executed_ids.length > 0) { + const placeholders = executed_ids.map(() => "?").join(", "); + const deleteQuery = `DELETE FROM cron WHERE id IN (${placeholders});`; + await conn.execute({ sql: deleteQuery, args: executed_ids }); + + return { + status: 200, + message: + "Processed databases deleted and corresponding cron rows removed.", + }; + } + } + + return { status: 200, ok: true }; + }), +}); diff --git a/src/server/api/routers/lineage/json-service.ts b/src/server/api/routers/lineage/json-service.ts new file mode 100644 index 0000000..3b7fcc1 --- /dev/null +++ b/src/server/api/routers/lineage/json-service.ts @@ -0,0 +1,141 @@ +import { createTRPCRouter, publicProcedure } from "../../utils"; + +// Attack data imports +import playerAttacks from "~/lineage-json/attack-route/playerAttacks.json"; +import mageBooks from "~/lineage-json/attack-route/mageBooks.json"; +import mageSpells from "~/lineage-json/attack-route/mageSpells.json"; +import necroBooks from "~/lineage-json/attack-route/necroBooks.json"; +import necroSpells from "~/lineage-json/attack-route/necroSpells.json"; +import rangerBooks from "~/lineage-json/attack-route/rangerBooks.json"; +import rangerSpells from "~/lineage-json/attack-route/rangerSpells.json"; +import paladinBooks from "~/lineage-json/attack-route/paladinBooks.json"; +import paladinSpells from "~/lineage-json/attack-route/paladinSpells.json"; +import summons from "~/lineage-json/attack-route/summons.json"; + +// Conditions data imports +import conditions from "~/lineage-json/conditions-route/conditions.json"; +import debilitations from "~/lineage-json/conditions-route/debilitations.json"; +import sanityDebuffs from "~/lineage-json/conditions-route/sanityDebuffs.json"; + +// Dungeon data imports +import dungeons from "~/lineage-json/dungeon-route/dungeons.json"; +import specialEncounters from "~/lineage-json/dungeon-route/specialEncounters.json"; + +// Enemy data imports +import bosses from "~/lineage-json/enemy-route/bosses.json"; +import enemies from "~/lineage-json/enemy-route/enemy.json"; +import enemyAttacks from "~/lineage-json/enemy-route/enemyAttacks.json"; + +// Item data imports +import arrows from "~/lineage-json/item-route/arrows.json"; +import bows from "~/lineage-json/item-route/bows.json"; +import foci from "~/lineage-json/item-route/foci.json"; +import hats from "~/lineage-json/item-route/hats.json"; +import junk from "~/lineage-json/item-route/junk.json"; +import melee from "~/lineage-json/item-route/melee.json"; +import robes from "~/lineage-json/item-route/robes.json"; +import wands from "~/lineage-json/item-route/wands.json"; +import ingredients from "~/lineage-json/item-route/ingredients.json"; +import storyItems from "~/lineage-json/item-route/storyItems.json"; +import artifacts from "~/lineage-json/item-route/artifacts.json"; +import shields from "~/lineage-json/item-route/shields.json"; +import bodyArmor from "~/lineage-json/item-route/bodyArmor.json"; +import helmets from "~/lineage-json/item-route/helmets.json"; +import suffix from "~/lineage-json/item-route/suffix.json"; +import prefix from "~/lineage-json/item-route/prefix.json"; +import potions from "~/lineage-json/item-route/potions.json"; +import poison from "~/lineage-json/item-route/poison.json"; +import staves from "~/lineage-json/item-route/staves.json"; + +// Misc data imports +import activities from "~/lineage-json/misc-route/activities.json"; +import investments from "~/lineage-json/misc-route/investments.json"; +import jobs from "~/lineage-json/misc-route/jobs.json"; +import manaOptions from "~/lineage-json/misc-route/manaOptions.json"; +import otherOptions from "~/lineage-json/misc-route/otherOptions.json"; +import healthOptions from "~/lineage-json/misc-route/healthOptions.json"; +import sanityOptions from "~/lineage-json/misc-route/sanityOptions.json"; +import pvpRewards from "~/lineage-json/misc-route/pvpRewards.json"; + +export const lineageJsonServiceRouter = createTRPCRouter({ + attacks: publicProcedure.query(() => { + return { + ok: true, + playerAttacks, + mageBooks, + mageSpells, + necroBooks, + necroSpells, + rangerBooks, + rangerSpells, + paladinBooks, + paladinSpells, + summons, + }; + }), + + conditions: publicProcedure.query(() => { + return { + ok: true, + conditions, + debilitations, + sanityDebuffs, + }; + }), + + dungeons: publicProcedure.query(() => { + return { + ok: true, + dungeons, + specialEncounters, + }; + }), + + enemies: publicProcedure.query(() => { + return { + ok: true, + bosses, + enemies, + enemyAttacks, + }; + }), + + items: publicProcedure.query(() => { + return { + ok: true, + arrows, + bows, + foci, + hats, + junk, + melee, + robes, + wands, + ingredients, + storyItems, + artifacts, + shields, + bodyArmor, + helmets, + suffix, + prefix, + potions, + poison, + staves, + }; + }), + + misc: publicProcedure.query(() => { + return { + ok: true, + activities, + investments, + jobs, + manaOptions, + otherOptions, + healthOptions, + sanityOptions, + pvpRewards, + }; + }), +}); diff --git a/src/server/api/routers/lineage/maintenance.ts b/src/server/api/routers/lineage/maintenance.ts new file mode 100644 index 0000000..81c6a0b --- /dev/null +++ b/src/server/api/routers/lineage/maintenance.ts @@ -0,0 +1,83 @@ +import { createTRPCRouter, adminProcedure } from "../../utils"; +import { LineageConnectionFactory } from "~/server/utils"; +import { env } from "~/env/server"; +import { TRPCError } from "@trpc/server"; +import { createClient as createAPIClient } from "@tursodatabase/api"; + +const IGNORE = ["frenome", "magic-delve-conductor"]; + +export const lineageMaintenanceRouter = createTRPCRouter({ + findLooseDatabases: adminProcedure.query(async () => { + const conn = LineageConnectionFactory(); + const query = "SELECT database_url FROM User WHERE database_url IS NOT NULL"; + + try { + const res = await conn.execute(query); + const turso = createAPIClient({ + org: "mikefreno", + token: env.TURSO_DB_API_TOKEN, + }); + const linkedDatabaseUrls = res.rows.map((row) => row.database_url); + + const all_dbs = await turso.databases.list(); + const dbs_to_delete = all_dbs.filter((db) => { + return !IGNORE.includes(db.name) && !linkedDatabaseUrls.includes(db.name); + }); + + return { + success: true, + looseDatabases: dbs_to_delete, + count: dbs_to_delete.length, + }; + } catch (e) { + console.error("Error finding loose databases:", e); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to find loose databases", + }); + } + }), + + cleanupExpiredDatabases: adminProcedure.query(async () => { + const conn = LineageConnectionFactory(); + const query = + "SELECT * FROM User WHERE datetime(db_destroy_date) < datetime('now');"; + + try { + const res = await conn.execute(query); + const turso = createAPIClient({ + org: "mikefreno", + token: env.TURSO_DB_API_TOKEN, + }); + + const deletedDatabases = []; + + for (const row of res.rows) { + const db_url = row.database_url; + + try { + await turso.databases.delete(db_url as string); + const updateQuery = + "UPDATE User SET database_url = ?, database_token = ?, db_destroy_date = ? WHERE id = ?"; + const params = [null, null, null, row.id]; + await conn.execute({ sql: updateQuery, args: params }); + deletedDatabases.push(db_url); + } catch (deleteErr) { + console.error(`Failed to delete database ${db_url}:`, deleteErr); + } + } + + return { + success: true, + deletedDatabases, + count: deletedDatabases.length, + }; + } catch (e) { + console.error("Error cleaning up expired databases:", e); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to cleanup expired databases", + }); + } + }), +}); diff --git a/src/server/api/routers/lineage/misc.ts b/src/server/api/routers/lineage/misc.ts new file mode 100644 index 0000000..9be61a0 --- /dev/null +++ b/src/server/api/routers/lineage/misc.ts @@ -0,0 +1,95 @@ +import { createTRPCRouter, publicProcedure } from "../../utils"; +import { z } from "zod"; +import { LineageConnectionFactory } from "~/server/utils"; +import { env } from "~/env/server"; +import { TRPCError } from "@trpc/server"; + +export const lineageMiscRouter = createTRPCRouter({ + analytics: publicProcedure + .input( + z.object({ + playerID: z.string(), + dungeonProgression: z.record(z.unknown()), + playerClass: z.string(), + spellCount: z.number(), + proficiencies: z.record(z.unknown()), + jobs: z.record(z.unknown()), + resistanceTable: z.record(z.unknown()), + damageTable: z.record(z.unknown()), + }) + ) + .mutation(async ({ input }) => { + const { + playerID, + dungeonProgression, + playerClass, + spellCount, + proficiencies, + jobs, + resistanceTable, + damageTable, + } = input; + + const conn = LineageConnectionFactory(); + + try { + const res = await conn.execute({ + sql: ` + INSERT OR REPLACE INTO Analytics + (playerID, dungeonProgression, playerClass, spellCount, proficiencies, jobs, resistanceTable, damageTable) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, + args: [ + playerID, + JSON.stringify(dungeonProgression), + playerClass, + spellCount, + JSON.stringify(proficiencies), + JSON.stringify(jobs), + JSON.stringify(resistanceTable), + JSON.stringify(damageTable), + ], + }); + + return { success: true, status: 200 }; + } catch (e) { + console.error("Analytics error:", e); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to store analytics", + }); + } + }), + + tokens: publicProcedure + .input(z.object({ token: z.string() })) + .mutation(async ({ input }) => { + const { token } = input; + + if (!token) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Missing token in body", + }); + } + + const conn = LineageConnectionFactory(); + const query = "SELECT * FROM Token WHERE token = ?"; + const res = await conn.execute({ sql: query, args: [token] }); + + if (res.rows.length > 0) { + const queryUpdate = + "UPDATE Token SET last_updated_at = datetime('now') WHERE token = ?"; + const resUpdate = await conn.execute({ sql: queryUpdate, args: [token] }); + return { success: true, action: "updated", result: resUpdate }; + } else { + const queryInsert = "INSERT INTO Token (token) VALUES (?)"; + const resInsert = await conn.execute({ sql: queryInsert, args: [token] }); + return { success: true, action: "inserted", result: resInsert }; + } + }), + + offlineSecret: publicProcedure.query(() => { + return { secret: env.LINEAGE_OFFLINE_SERIALIZATION_SECRET }; + }), +}); diff --git a/src/server/api/routers/lineage/pvp.ts b/src/server/api/routers/lineage/pvp.ts new file mode 100644 index 0000000..20b0c99 --- /dev/null +++ b/src/server/api/routers/lineage/pvp.ts @@ -0,0 +1,229 @@ +import { createTRPCRouter, publicProcedure } from "../../utils"; +import { z } from "zod"; +import { LineageConnectionFactory } from "~/server/utils"; +import { TRPCError } from "@trpc/server"; + +const characterSchema = z.object({ + playerClass: z.string(), + blessing: z.string().optional(), + name: z.string(), + maxHealth: z.number(), + maxSanity: z.number(), + maxMana: z.number(), + baseManaRegen: z.number(), + strength: z.number(), + intelligence: z.number(), + dexterity: z.number(), + resistanceTable: z.string(), + damageTable: z.string(), + attackStrings: z.string(), + knownSpells: z.string(), +}); + +export const lineagePvpRouter = createTRPCRouter({ + registerCharacter: publicProcedure + .input( + z.object({ + character: characterSchema, + linkID: z.string(), + pushToken: z.string().optional(), + pushCurrentlyEnabled: z.boolean().optional(), + }) + ) + .mutation(async ({ input }) => { + const { character, linkID, pushToken, pushCurrentlyEnabled } = input; + + try { + const conn = LineageConnectionFactory(); + const res = await conn.execute({ + sql: `SELECT * FROM PvP_Characters WHERE linkID = ?`, + args: [linkID], + }); + + if (res.rows.length === 0) { + await conn.execute({ + sql: `INSERT INTO PvP_Characters ( + linkID, + blessing, + playerClass, + name, + maxHealth, + maxSanity, + maxMana, + baseManaRegen, + strength, + intelligence, + dexterity, + resistanceTable, + damageTable, + attackStrings, + knownSpells, + pushToken, + pushCurrentlyEnabled + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + args: [ + linkID, + character.blessing, + character.playerClass, + character.name, + character.maxHealth, + character.maxSanity, + character.maxMana, + character.baseManaRegen, + character.strength, + character.intelligence, + character.dexterity, + character.resistanceTable, + character.damageTable, + character.attackStrings, + character.knownSpells, + pushToken, + pushCurrentlyEnabled, + ], + }); + + return { + ok: true, + winCount: 0, + lossCount: 0, + tokenRedemptionCount: 0, + status: 201, + }; + } else { + await conn.execute({ + sql: `UPDATE PvP_Characters SET + playerClass = ?, + blessing = ?, + name = ?, + maxHealth = ?, + maxSanity = ?, + maxMana = ?, + baseManaRegen = ?, + strength = ?, + intelligence = ?, + dexterity = ?, + resistanceTable = ?, + damageTable = ?, + attackStrings = ?, + knownSpells = ?, + pushToken = ?, + pushCurrentlyEnabled = ? + WHERE linkID = ?`, + args: [ + character.playerClass, + character.blessing, + character.name, + character.maxHealth, + character.maxSanity, + character.maxMana, + character.baseManaRegen, + character.strength, + character.intelligence, + character.dexterity, + character.resistanceTable, + character.damageTable, + character.attackStrings, + character.knownSpells, + pushToken, + pushCurrentlyEnabled, + linkID, + ], + }); + + return { + ok: true, + winCount: res.rows[0].winCount as number, + lossCount: res.rows[0].lossCount as number, + tokenRedemptionCount: res.rows[0].tokenRedemptionCount as number, + status: 200, + }; + } + } catch (e) { + console.error(e); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to register character", + }); + } + }), + + getOpponents: publicProcedure.query(async () => { + const conn = LineageConnectionFactory(); + + try { + const res = await conn.execute( + ` + SELECT playerClass, + blessing, + name, + maxHealth, + maxSanity, + maxMana, + baseManaRegen, + strength, + intelligence, + dexterity, + resistanceTable, + damageTable, + attackStrings, + knownSpells, + linkID, + winCount, + lossCount + FROM PvP_Characters + ORDER BY RANDOM() + LIMIT 3 + ` + ); + + return { + ok: true, + characters: res.rows, + status: 200, + }; + } catch (e) { + console.error(e); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to get opponents", + }); + } + }), + + battleResult: publicProcedure + .input( + z.object({ + winnerLinkID: z.string(), + loserLinkID: z.string(), + }) + ) + .mutation(async ({ input }) => { + const { winnerLinkID, loserLinkID } = input; + + const conn = LineageConnectionFactory(); + + try { + await conn.execute({ + sql: ` + UPDATE PvP_Characters + SET + winCount = winCount + CASE WHEN linkID = ? THEN 1 ELSE 0 END, + lossCount = lossCount + CASE WHEN linkID = ? THEN 1 ELSE 0 END + WHERE linkID IN (?, ?) + `, + args: [winnerLinkID, loserLinkID, winnerLinkID, loserLinkID], + }); + + return { + ok: true, + status: 200, + }; + } catch (e) { + console.error(e); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to record battle result", + }); + } + }), +}); diff --git a/src/server/api/routers/misc.ts b/src/server/api/routers/misc.ts index 63e4911..3cbff03 100644 --- a/src/server/api/routers/misc.ts +++ b/src/server/api/routers/misc.ts @@ -1,34 +1,204 @@ import { createTRPCRouter, publicProcedure } from "../utils"; import { z } from "zod"; +import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { env } from "~/env/server"; +import { TRPCError } from "@trpc/server"; +import { ConnectionFactory } from "~/server/utils"; +import * as bcrypt from "bcrypt"; + +const assets: Record = { + "shapes-with-abigail": "shapes-with-abigail.apk", + "magic-delve": "magic-delve.apk", + cork: "Cork.zip", +}; export const miscRouter = createTRPCRouter({ - // Downloads endpoint (GET) - downloads: publicProcedure - .query(async () => { - // Implementation for downloads logic would go here - return { message: "Downloads endpoint" }; - }), - - // S3 operations (DELETE/GET) - s3Delete: publicProcedure - .input(z.object({ key: z.string() })) - .mutation(async ({ input }) => { - // Implementation for S3 delete logic would go here - return { message: `Deleted S3 object with key: ${input.key}` }; - }), - - s3Get: publicProcedure - .input(z.object({ key: z.string() })) + // ============================================================ + // Downloads endpoint + // ============================================================ + + getDownloadUrl: publicProcedure + .input(z.object({ asset_name: z.string() })) .query(async ({ input }) => { - // Implementation for S3 get logic would go here - return { message: `Retrieved S3 object with key: ${input.key}` }; + const bucket = "frenomedownloads"; + const params = { + Bucket: bucket, + Key: assets[input.asset_name], + }; + + if (!assets[input.asset_name]) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Asset not found", + }); + } + + const credentials = { + accessKeyId: env._AWS_ACCESS_KEY, + secretAccessKey: env._AWS_SECRET_KEY, + }; + + try { + const client = new S3Client({ + region: env.AWS_REGION, + credentials: credentials, + }); + + const command = new GetObjectCommand(params); + const signedUrl = await getSignedUrl(client, command, { expiresIn: 120 }); + return { downloadURL: signedUrl }; + } catch (error) { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to generate download URL", + }); + } }), - // Password hashing endpoint (POST) - hashPassword: publicProcedure - .input(z.object({ password: z.string() })) + // ============================================================ + // S3 Operations + // ============================================================ + + getPreSignedURL: publicProcedure + .input(z.object({ + type: z.string(), + title: z.string(), + filename: z.string(), + })) .mutation(async ({ input }) => { - // Implementation for password hashing logic would go here - return { message: "Password hashed successfully" }; + const credentials = { + accessKeyId: env._AWS_ACCESS_KEY, + secretAccessKey: env._AWS_SECRET_KEY, + }; + + try { + const client = new S3Client({ + region: env.AWS_REGION, + credentials: credentials, + }); + + const Key = `${input.type}/${input.title}/${input.filename}`; + const ext = /^.+\.([^.]+)$/.exec(input.filename); + + const s3params = { + Bucket: env.AWS_S3_BUCKET_NAME, + Key, + ContentType: `image/${ext![1]}`, + }; + + const command = new PutObjectCommand(s3params); + const signedUrl = await getSignedUrl(client, command, { expiresIn: 120 }); + + return { uploadURL: signedUrl, key: Key }; + } catch (error) { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to generate pre-signed URL", + }); + } }), -}); \ No newline at end of file + + deleteImage: publicProcedure + .input(z.object({ + key: z.string(), + newAttachmentString: z.string(), + type: z.string(), + id: z.number(), + })) + .mutation(async ({ input }) => { + try { + const s3params = { + Bucket: env.AWS_S3_BUCKET_NAME, + Key: input.key, + }; + + const client = new S3Client({ + region: env.AWS_REGION, + }); + + const command = new DeleteObjectCommand(s3params); + const res = await client.send(command); + + const conn = ConnectionFactory(); + const query = `UPDATE ${input.type} SET attachments = ? WHERE id = ?`; + await conn.execute({ + sql: query, + args: [input.newAttachmentString, input.id], + }); + + return res; + } catch (error) { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to delete image", + }); + } + }), + + simpleDeleteImage: publicProcedure + .input(z.object({ key: z.string() })) + .mutation(async ({ input }) => { + try { + const s3params = { + Bucket: env.AWS_S3_BUCKET_NAME, + Key: input.key, + }; + + const client = new S3Client({ + region: env.AWS_REGION, + }); + + const command = new DeleteObjectCommand(s3params); + const res = await client.send(command); + + return res; + } catch (error) { + console.error(error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to delete image", + }); + } + }), + + // ============================================================ + // Password Hashing + // ============================================================ + + hashPassword: publicProcedure + .input(z.object({ password: z.string().min(8) })) + .mutation(async ({ input }) => { + try { + const saltRounds = 10; + const salt = await bcrypt.genSalt(saltRounds); + const hashedPassword = await bcrypt.hash(input.password, salt); + return { hashedPassword }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to hash password", + }); + } + }), + + checkPassword: publicProcedure + .input(z.object({ + password: z.string(), + hash: z.string(), + })) + .mutation(async ({ input }) => { + try { + const match = await bcrypt.compare(input.password, input.hash); + return { match }; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to check password", + }); + } + }), +}); diff --git a/src/server/api/utils.ts b/src/server/api/utils.ts index c082886..464fe02 100644 --- a/src/server/api/utils.ts +++ b/src/server/api/utils.ts @@ -1,6 +1,85 @@ -import { initTRPC } from "@trpc/server"; +import { initTRPC, TRPCError } from "@trpc/server"; +import type { APIEvent } from "@solidjs/start/server"; +import { getCookie, setCookie } from "vinxi/http"; +import { jwtVerify, type JWTPayload } from "jose"; +import { env } from "~/env/server"; -export const t = initTRPC.create(); +export type Context = { + event: APIEvent; + userId: string | null; + privilegeLevel: "anonymous" | "user" | "admin"; +}; + +async function createContextInner(event: APIEvent): Promise { + const userIDToken = getCookie(event.nativeEvent, "userIDToken"); + + let userId: string | null = null; + let privilegeLevel: "anonymous" | "user" | "admin" = "anonymous"; + + if (userIDToken) { + try { + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const { payload } = await jwtVerify(userIDToken, secret); + + if (payload.id && typeof payload.id === "string") { + userId = payload.id; + privilegeLevel = payload.id === env.ADMIN_ID ? "admin" : "user"; + } + } catch (err) { + console.log("Failed to authenticate token:", err); + // Clear invalid token + setCookie(event.nativeEvent, "userIDToken", "", { + maxAge: 0, + expires: new Date("2016-10-05"), + }); + } + } + + return { + event, + userId, + privilegeLevel, + }; +} + +export const createTRPCContext = (event: APIEvent) => { + return createContextInner(event); +}; + +export const t = initTRPC.context().create(); export const createTRPCRouter = t.router; export const publicProcedure = t.procedure; + +// Middleware to enforce authentication +const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { + if (!ctx.userId || ctx.privilegeLevel === "anonymous") { + throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authenticated" }); + } + return next({ + ctx: { + ...ctx, + userId: ctx.userId, // userId is non-null here + }, + }); +}); + +// Middleware to enforce admin access +const enforceUserIsAdmin = t.middleware(({ ctx, next }) => { + if (ctx.privilegeLevel !== "admin") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Admin access required" + }); + } + return next({ + ctx: { + ...ctx, + userId: ctx.userId!, // userId is non-null for admins + }, + }); +}); + +// Protected procedures +export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); +export const adminProcedure = t.procedure.use(enforceUserIsAdmin); diff --git a/src/server/utils.ts b/src/server/utils.ts index 0cdd764..daea395 100644 --- a/src/server/utils.ts +++ b/src/server/utils.ts @@ -1,38 +1,35 @@ -import jwt, { JwtPayload } from "jsonwebtoken"; -import { cookies } from "next/headers"; +import { getCookie, setCookie, type H3Event } from "vinxi/http"; +import { jwtVerify, type JWTPayload, SignJWT } from "jose"; +import { env } from "~/env/server"; +import { createClient, Row } from "@libsql/client/web"; +import { v4 as uuid } from "uuid"; +import { createClient as createAPIClient } from "@tursodatabase/api"; +import { OAuth2Client } from "google-auth-library"; +import * as bcrypt from "bcrypt"; export const LINEAGE_JWT_EXPIRY = "14d"; -export async function getPrivilegeLevel(): Promise< - "anonymous" | "admin" | "user" -> { +// Helper function to get privilege level from H3Event (for use outside tRPC) +export async function getPrivilegeLevel( + event: H3Event, +): Promise<"anonymous" | "admin" | "user"> { try { - const userIDToken = (await cookies()).get("userIDToken"); + const userIDToken = getCookie(event, "userIDToken"); if (userIDToken) { - const decoded = await new Promise((resolve) => { - jwt.verify( - userIDToken.value, - env.JWT_SECRET_KEY, - async (err, decoded) => { - if (err) { - console.log("Failed to authenticate token."); - (await cookies()).set({ - name: "userIDToken", - value: "", - maxAge: 0, - expires: new Date("2016-10-05"), - }); - resolve(undefined); - } else { - resolve(decoded as JwtPayload); - } - }, - ); - }); + try { + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const { payload } = await jwtVerify(userIDToken, secret); - if (decoded) { - return decoded.id === env.ADMIN_ID ? "admin" : "user"; + if (payload.id && typeof payload.id === "string") { + return payload.id === env.ADMIN_ID ? "admin" : "user"; + } + } catch (err) { + console.log("Failed to authenticate token."); + setCookie(event, "userIDToken", "", { + maxAge: 0, + expires: new Date("2016-10-05"), + }); } } } catch (e) { @@ -40,34 +37,26 @@ export async function getPrivilegeLevel(): Promise< } return "anonymous"; } -export async function getUserID(): Promise { + +// Helper function to get user ID from H3Event (for use outside tRPC) +export async function getUserID(event: H3Event): Promise { try { - const userIDToken = (await cookies()).get("userIDToken"); + const userIDToken = getCookie(event, "userIDToken"); if (userIDToken) { - const decoded = await new Promise((resolve) => { - jwt.verify( - userIDToken.value, - env.JWT_SECRET_KEY, - async (err, decoded) => { - if (err) { - console.log("Failed to authenticate token."); - (await cookies()).set({ - name: "userIDToken", - value: "", - maxAge: 0, - expires: new Date("2016-10-05"), - }); - resolve(undefined); - } else { - resolve(decoded as JwtPayload); - } - }, - ); - }); + try { + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const { payload } = await jwtVerify(userIDToken, secret); - if (decoded) { - return decoded.id; + if (payload.id && typeof payload.id === "string") { + return payload.id; + } + } catch (err) { + console.log("Failed to authenticate token."); + setCookie(event, "userIDToken", "", { + maxAge: 0, + expires: new Date("2016-10-05"), + }); } } } catch (e) { @@ -76,9 +65,6 @@ export async function getUserID(): Promise { return null; } -import { createClient, Row } from "@libsql/client/web"; -import { env } from "@/env.mjs"; - // Turso export function ConnectionFactory() { const config = { @@ -100,11 +86,6 @@ export function LineageConnectionFactory() { return conn; } -import { v4 as uuid } from "uuid"; -import { createClient as createAPIClient } from "@tursodatabase/api"; -import { checkPassword } from "./api/passwordHashing"; -import { OAuth2Client } from "google-auth-library"; - export async function LineageDBInit() { const turso = createAPIClient({ org: "mikefreno", @@ -220,11 +201,13 @@ export async function validateLineageRequest({ }): Promise { const { provider, email } = userRow; if (provider === "email") { - const decoded = jwt.verify( - auth_token, - env.JWT_SECRET_KEY, - ) as jwt.JwtPayload; - if (email !== decoded.email) { + try { + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const { payload } = await jwtVerify(auth_token, secret); + if (email !== payload.email) { + return false; + } + } catch (err) { return false; } } else if (provider == "apple") { @@ -233,7 +216,12 @@ export async function validateLineageRequest({ return false; } } else if (provider == "google") { - const CLIENT_ID = env.NEXT_PUBLIC_GOOGLE_CLIENT_ID_MAGIC_DELVE; + // Note: Using client env var - should be available via import.meta.env in actual runtime + const CLIENT_ID = process.env.VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE; + if (!CLIENT_ID) { + console.error("Missing VITE_GOOGLE_CLIENT_ID_MAGIC_DELVE"); + return false; + } const client = new OAuth2Client(CLIENT_ID); const ticket = await client.verifyIdToken({ idToken: auth_token, @@ -247,3 +235,110 @@ export async function validateLineageRequest({ } return true; } + +// Password hashing utilities +export async function hashPassword(password: string): Promise { + const saltRounds = 10; + const salt = await bcrypt.genSalt(saltRounds); + const hashedPassword = await bcrypt.hash(password, salt); + return hashedPassword; +} + +export async function checkPassword( + password: string, + hash: string +): Promise { + const match = await bcrypt.compare(password, hash); + return match; +} + +// Email service utilities +export async function sendEmailVerification(userEmail: string): Promise<{ + success: boolean; + messageId?: string; + message?: string; +}> { + const apiKey = env.SENDINBLUE_KEY; + const apiUrl = "https://api.brevo.com/v3/smtp/email"; + + const secret = new TextEncoder().encode(env.JWT_SECRET_KEY); + const token = await new SignJWT({ email: userEmail }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime("15m") + .sign(secret); + + const domain = env.VITE_DOMAIN || env.NEXT_PUBLIC_DOMAIN || "https://freno.me"; + + const emailPayload = { + sender: { + name: "MikeFreno", + email: "lifeandlineage_no_reply@freno.me", + }, + to: [ + { + email: userEmail, + }, + ], + htmlContent: ` + + + + +
+

Click the button below to verify email

+
+
+ + + +`, + subject: `Life and Lineage email verification`, + }; + + try { + const res = await fetch(apiUrl, { + method: "POST", + headers: { + accept: "application/json", + "api-key": apiKey, + "content-type": "application/json", + }, + body: JSON.stringify(emailPayload), + }); + + if (!res.ok) { + return { success: false, message: "Failed to send email" }; + } + + const json = await res.json() as { messageId?: string }; + if (json.messageId) { + return { success: true, messageId: json.messageId }; + } + return { success: false, message: "No messageId in response" }; + } catch (error) { + console.error("Email sending error:", error); + return { success: false, message: "Email service error" }; + } +} diff --git a/src/utils.ts b/src/utils.ts index c37bdf4..f158b83 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,6 @@ +import { createClient } from "@libsql/client/web"; +import { env } from "~/env/server"; + export function ConnectionFactory() { const config = { url: env.TURSO_DB_URL,