continued migration

This commit is contained in:
Michael Freno
2025-12-16 23:31:12 -05:00
parent 8fb748f401
commit b3df3eedd2
117 changed files with 16957 additions and 3172 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -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<string | null>(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 (
<div>
<p>{result}</p>
<button onClick={handleClick}>Call API</button>
</div>
);
}
```
## 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

View File

@@ -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"
}
}

View File

@@ -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" } },
);
}
}

View File

@@ -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" } },
);
}
}

View File

@@ -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" } },
);
}
}

View File

@@ -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" } },
);
}
}

View File

@@ -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 },
);
}

View File

@@ -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 || [] });
}

View File

@@ -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 || [] });
}

View File

@@ -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 });
}

View File

@@ -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 });
}

View File

@@ -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 });
}

View File

@@ -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 });
}

View File

@@ -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 });
}
}
}

View File

@@ -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 });
}
}
}

View File

@@ -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 };
}

View File

@@ -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 });
}
}

View File

@@ -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 });
}
}

View File

@@ -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 });
}
}

View File

@@ -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 });
}
}

View File

@@ -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<string, string> = {
"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 });
}
}

View File

@@ -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" } },
);
}

View File

@@ -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" } },
);
}

View File

@@ -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 });
}
}

View File

@@ -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 });
}
}

View File

@@ -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" } },
);
}
}

View File

@@ -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,
},
);
}
}

View File

@@ -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.",
});
}
}

View File

@@ -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 });
}
}

View File

@@ -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 });
}

View File

@@ -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`,
});
}
}
}

View File

@@ -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" } },
);
}
}

View File

@@ -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 });
}
}

View File

@@ -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: `<html>
<head>
<style>
.center {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.button {
display: inline-block;
padding: 10px 20px;
text-align: center;
text-decoration: none;
color: #ffffff;
background-color: #007BFF;
border-radius: 6px;
transition: background-color 0.3s;
}
.button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="center">
<p>Click the button below to verify email</p>
</div>
<br/>
<div class="center">
<a href=${env.NEXT_PUBLIC_DOMAIN}/api/lineage/email/verification/${userEmail}/?token=${token} class="button">Verify Email</a>
</div>
</body>
</html>
`,
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),
});
}

View File

@@ -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: `<html>
<head>
<style>
.center {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.button {
display: inline-block;
padding: 10px 20px;
text-align: center;
text-decoration: none;
color: #ffffff;
background-color: #007BFF;
border-radius: 6px;
transition: background-color 0.3s;
}
.button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="center">
<p>Click the button below to verify email</p>
</div>
<br/>
<div class="center">
<a href=${env.NEXT_PUBLIC_DOMAIN}/api/lineage/email/verification/${userEmail}/?token=${token} class="button">Verify Email</a>
</div>
</body>
</html>
`,
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),
});
}

View File

@@ -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" } },
);
}
}

View File

@@ -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" } },
);
}
}

View File

@@ -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,
});
}

View File

@@ -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,
});
}

View File

@@ -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 });
}

View File

@@ -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 });
}

View File

@@ -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,
});
}

View File

@@ -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,
});
}

View File

@@ -1,5 +0,0 @@
import { NextResponse } from "next/server";
export async function GET() {
return new NextResponse(process.env.LINEAGE_OFFLINE_SERIALIZATION_SECRET);
}

View File

@@ -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 });
}
}

View File

@@ -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 });
}
}

View File

@@ -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));
}
}

View File

@@ -1,20 +0,0 @@
import * as bcrypt from "bcrypt";
// Asynchronous function to hash a password
export async function hashPassword(password: string): Promise<string> {
// 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<boolean> {
const match = await bcrypt.compare(password, hash);
return match;
}

View File

@@ -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);
}

View File

@@ -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 });
}
}

View File

@@ -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);
}

View File

@@ -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);

73
src/env/server.ts vendored
View File

@@ -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),
),
};
};

View File

@@ -0,0 +1 @@
Do not directly modify this directory, it is synced from lineage using the script `json-sync.ts`

View File

@@ -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
}
]

View File

@@ -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
}
}
]

View File

@@ -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
}
]

View File

@@ -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"
}
}
]

View File

@@ -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
}
]

View File

@@ -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"
}
}
]

View File

@@ -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
}
}
]

View File

@@ -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
}
]

View File

@@ -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"
}
}
]

View File

@@ -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
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
]

View File

@@ -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"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
}
}
}
]

View File

@@ -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
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -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
}
]
}
]

View File

@@ -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
}
}
]

View File

@@ -0,0 +1,8 @@
[
{
"name": "golden goblet",
"icon": "Goblet",
"baseValue": 5000
}
]

View File

@@ -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
}
}
]

View File

@@ -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
}
}
]

View File

@@ -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
}
]

View File

@@ -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
}
}
]

View File

@@ -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
}
}
]

View File

@@ -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
}
]

View File

@@ -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
}
]

View File

@@ -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
}
}
]

View File

@@ -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
}
]

View File

@@ -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
}
]

View File

@@ -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
}
]

View File

@@ -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
}
}
]

View File

@@ -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
}
}
]

View File

@@ -0,0 +1,16 @@
[
{
"name": "Goblin Totem",
"baseValue": 5000,
"icon": "Goblin_Staff",
"slot": "one-hand",
"attacks": [
"bonk",
"spark"
],
"stats": {
"physicalDamage": 8.0
}
}
]

View File

@@ -0,0 +1,21 @@
[
{
"name": "the deed to the whispering raven inn",
"icon": "Scroll",
"description": "<Font>Cursive</Font>**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\n<em>But the FOOLS outside do not understand!</em>\n\nThey call me <em>monster, madman, murderer.</em> 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": "<Font>Cursive</Font>*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
}
]

View File

@@ -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
}
]

View File

@@ -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
}
}
]

View File

@@ -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
}
}
]

View File

@@ -0,0 +1,12 @@
[
{
"serviceName": "Recover Alone",
"cost": 0,
"heathRestore": 15
},
{
"serviceName": "Visit Master Priest",
"cost": 150,
"heathRestore": "fill"
}
]

View File

@@ -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}
}
]
}
]

View File

@@ -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
}
]

View File

@@ -0,0 +1,12 @@
[
{
"serviceName": "Meditate On The Elements",
"cost": 0,
"manaRestore": 25
},
{
"serviceName": "Hire Shamanic Guide",
"cost": 150,
"manaRestore": "fill"
}
]

View File

@@ -0,0 +1,12 @@
[
{
"serviceName": "Visit Witch Doctor",
"cost": 25,
"removeDebuffs": 1
},
{
"serviceName": "Seek Paladin Aid",
"cost": 150,
"removeDebuffs": 5
}
]

View File

@@ -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"
}
]

View File

@@ -0,0 +1,12 @@
[
{
"serviceName": "Pray",
"cost": 0,
"sanityRestore": 10
},
{
"serviceName": "Seek Spiritual Leader",
"cost": 200,
"sanityRestore": "fill"
}
]

Some files were not shown because too many files have changed in this diff Show More