- Implemented `findMobDependencies.ts` to identify foreign key constraints referencing the Mob table and log dependent rows. - Created `fullServerSetup.ts` for idempotent server setup, including economy items, item recipes, game areas, mobs, and optional demo mob attacks. - Developed `removeInvalidMobsWithDeps.ts` to delete invalid mobs and their dependencies, backing up affected scheduled mob attacks. - Added unit tests in `testMobUnit.ts` and `mob.test.ts` for mob functionality, including stats computation and instance retrieval. - Introduced reward modification tests in `testRewardMods.ts` and `rewardMods.unit.ts` to validate drop selection and coin multiplier behavior. - Enhanced command handling for mob deletion in `mobDelete.ts` and setup examples in `setup.ts`, ensuring proper permissions and feedback. - Created utility functions in `testHelpers.ts` for deterministic drop selection from mob definitions.
394 lines
11 KiB
TypeScript
394 lines
11 KiB
TypeScript
import { prisma } from "../src/core/database/prisma";
|
|
import { Prisma } from "@prisma/client";
|
|
|
|
/**
|
|
* fullServerSetup.ts
|
|
*
|
|
* Script idempotente para poblar UN servidor con todo lo necesario:
|
|
* - Economy items (herramientas, armas, materiales, cofres, pociones)
|
|
* - Item recipes (crafteo)
|
|
* - Item mutations (encantamientos)
|
|
* - Game areas y niveles
|
|
* - Mobs con drops
|
|
* - Opcional: programar ataques de mobs demo
|
|
*
|
|
* Uso: provee GUILD_ID como variable de entorno opcional. Si no se provee, usa el id por defecto.
|
|
* GUILD_ID=1316592320954630144 npx tsx scripts/fullServerSetup.ts
|
|
*/
|
|
|
|
const DEFAULT_GUILD = process.env.GUILD_ID ?? "1316592320954630144";
|
|
|
|
async function upsertEconomyItem(
|
|
guildId: string | null,
|
|
key: string,
|
|
data: Omit<Prisma.EconomyItemUncheckedCreateInput, "key" | "guildId">
|
|
) {
|
|
const existing = await prisma.economyItem.findFirst({
|
|
where: { key, guildId },
|
|
});
|
|
if (existing)
|
|
return prisma.economyItem.update({
|
|
where: { id: existing.id },
|
|
data: { ...data },
|
|
});
|
|
return prisma.economyItem.create({ data: { ...data, key, guildId } });
|
|
}
|
|
|
|
async function upsertGameArea(
|
|
guildId: string | null,
|
|
key: string,
|
|
data: Omit<Prisma.GameAreaUncheckedCreateInput, "key" | "guildId">
|
|
) {
|
|
const existing = await prisma.gameArea.findFirst({ where: { key, guildId } });
|
|
if (existing)
|
|
return prisma.gameArea.update({
|
|
where: { id: existing.id },
|
|
data: { ...data },
|
|
});
|
|
return prisma.gameArea.create({ data: { ...data, key, guildId } });
|
|
}
|
|
|
|
async function upsertMob(
|
|
guildId: string | null,
|
|
key: string,
|
|
data: Omit<Prisma.MobUncheckedCreateInput, "key" | "guildId">
|
|
) {
|
|
const existing = await prisma.mob.findFirst({ where: { key, guildId } });
|
|
if (existing)
|
|
return prisma.mob.update({ where: { id: existing.id }, data: { ...data } });
|
|
return prisma.mob.create({ data: { ...data, key, guildId } });
|
|
}
|
|
|
|
async function upsertItemRecipe(
|
|
guildId: string | null,
|
|
productKey: string,
|
|
ingredients: { itemKey: string; qty: number }[],
|
|
productQty = 1
|
|
) {
|
|
// Ensure product exists
|
|
const product = await prisma.economyItem.findFirst({
|
|
where: { key: productKey, OR: [{ guildId }, { guildId: null }] },
|
|
orderBy: [{ guildId: "desc" }],
|
|
});
|
|
if (!product) throw new Error(`Product item not found: ${productKey}`);
|
|
|
|
// Find existing recipe by productItemId
|
|
const existing = await prisma.itemRecipe.findUnique({
|
|
where: { productItemId: product.id },
|
|
});
|
|
if (existing) {
|
|
// Recreate ingredients set
|
|
await prisma.recipeIngredient.deleteMany({
|
|
where: { recipeId: existing.id },
|
|
});
|
|
for (const ing of ingredients) {
|
|
const it = await prisma.economyItem.findFirst({
|
|
where: { key: ing.itemKey, OR: [{ guildId }, { guildId: null }] },
|
|
orderBy: [{ guildId: "desc" }],
|
|
});
|
|
if (!it) throw new Error(`Ingredient item not found: ${ing.itemKey}`);
|
|
await prisma.recipeIngredient.create({
|
|
data: { recipeId: existing.id, itemId: it.id, quantity: ing.qty },
|
|
});
|
|
}
|
|
return existing;
|
|
}
|
|
|
|
const r = await prisma.itemRecipe.create({
|
|
data: { productItemId: product.id, productQuantity: productQty },
|
|
});
|
|
for (const ing of ingredients) {
|
|
const it = await prisma.economyItem.findFirst({
|
|
where: { key: ing.itemKey, OR: [{ guildId }, { guildId: null }] },
|
|
orderBy: [{ guildId: "desc" }],
|
|
});
|
|
if (!it) throw new Error(`Ingredient item not found: ${ing.itemKey}`);
|
|
await prisma.recipeIngredient.create({
|
|
data: { recipeId: r.id, itemId: it.id, quantity: ing.qty },
|
|
});
|
|
}
|
|
return r;
|
|
}
|
|
|
|
export async function runFullServerSetup(
|
|
guildIdArg?: string | null,
|
|
options?: { dryRun?: boolean }
|
|
) {
|
|
const guildId = guildIdArg ?? DEFAULT_GUILD;
|
|
console.log("Starting full server setup for guild=", guildId, options ?? {});
|
|
|
|
// --- Items: tools, weapons, materials ---
|
|
await upsertEconomyItem(guildId, "tool.pickaxe.basic", {
|
|
name: "Pico Básico",
|
|
stackable: false,
|
|
props: {
|
|
tool: { type: "pickaxe", tier: 1 },
|
|
breakable: { enabled: true, maxDurability: 100, durabilityPerUse: 5 },
|
|
} as unknown as Prisma.InputJsonValue,
|
|
tags: ["tool", "mine"],
|
|
});
|
|
|
|
await upsertEconomyItem(guildId, "tool.pickaxe.iron", {
|
|
name: "Pico de Hierro",
|
|
stackable: false,
|
|
props: {
|
|
tool: { type: "pickaxe", tier: 2 },
|
|
breakable: { enabled: true, maxDurability: 180, durabilityPerUse: 4 },
|
|
} as unknown as Prisma.InputJsonValue,
|
|
tags: ["tool", "mine", "tier2"],
|
|
});
|
|
|
|
await upsertEconomyItem(guildId, "weapon.sword.iron", {
|
|
name: "Espada de Hierro",
|
|
stackable: false,
|
|
props: {
|
|
damage: 10,
|
|
tool: { type: "sword", tier: 1 },
|
|
breakable: { enabled: true, maxDurability: 150, durabilityPerUse: 2 },
|
|
} as unknown as Prisma.InputJsonValue,
|
|
tags: ["weapon"],
|
|
});
|
|
|
|
await upsertEconomyItem(guildId, "armor.leather.basic", {
|
|
name: "Armadura de Cuero",
|
|
stackable: false,
|
|
props: { defense: 3 } as unknown as Prisma.InputJsonValue,
|
|
tags: ["armor"],
|
|
});
|
|
|
|
await upsertEconomyItem(guildId, "ore.iron", {
|
|
name: "Mineral de Hierro",
|
|
stackable: true,
|
|
props: { craftingOnly: true } as unknown as Prisma.InputJsonValue,
|
|
tags: ["ore", "common"],
|
|
});
|
|
await upsertEconomyItem(guildId, "ingot.iron", {
|
|
name: "Lingote de Hierro",
|
|
stackable: true,
|
|
props: {} as unknown as Prisma.InputJsonValue,
|
|
tags: ["ingot", "metal"],
|
|
});
|
|
|
|
// Consumibles y pociones
|
|
await upsertEconomyItem(guildId, "food.meat.small", {
|
|
name: "Carne Pequeña",
|
|
stackable: true,
|
|
props: {
|
|
food: { healHp: 8, cooldownSeconds: 20 },
|
|
} as unknown as Prisma.InputJsonValue,
|
|
tags: ["food"],
|
|
});
|
|
await upsertEconomyItem(guildId, "potion.energy", {
|
|
name: "Poción Energética",
|
|
stackable: true,
|
|
props: {
|
|
potion: { removeEffects: ["FATIGUE"], cooldownSeconds: 90 },
|
|
} as unknown as Prisma.InputJsonValue,
|
|
tags: ["potion", "utility"],
|
|
});
|
|
|
|
// Cofre con recompensas
|
|
await upsertEconomyItem(guildId, "chest.daily", {
|
|
name: "Cofre Diario",
|
|
stackable: true,
|
|
props: {
|
|
chest: {
|
|
enabled: true,
|
|
consumeOnOpen: true,
|
|
randomMode: "single",
|
|
rewards: [
|
|
{ type: "coins", amount: 200 },
|
|
{ type: "item", itemKey: "ingot.iron", qty: 2 },
|
|
],
|
|
},
|
|
} as unknown as Prisma.InputJsonValue,
|
|
tags: ["chest"],
|
|
});
|
|
|
|
// --- Mutations / enchants catalog ---
|
|
// Item mutations (catalog)
|
|
const existingRuby = await prisma.itemMutation.findFirst({
|
|
where: { key: "ruby_core", guildId },
|
|
});
|
|
if (existingRuby) {
|
|
await prisma.itemMutation.update({
|
|
where: { id: existingRuby.id },
|
|
data: { name: "Núcleo de Rubí", effects: { damageBonus: 15 } as any },
|
|
});
|
|
} else {
|
|
await prisma.itemMutation.create({
|
|
data: {
|
|
key: "ruby_core",
|
|
name: "Núcleo de Rubí",
|
|
guildId,
|
|
effects: { damageBonus: 15 } as any,
|
|
} as any,
|
|
});
|
|
}
|
|
|
|
const existingEmerald = await prisma.itemMutation.findFirst({
|
|
where: { key: "emerald_core", guildId },
|
|
});
|
|
if (existingEmerald) {
|
|
await prisma.itemMutation.update({
|
|
where: { id: existingEmerald.id },
|
|
data: {
|
|
name: "Núcleo de Esmeralda",
|
|
effects: { defenseBonus: 10, maxHpBonus: 20 } as any,
|
|
},
|
|
});
|
|
} else {
|
|
await prisma.itemMutation.create({
|
|
data: {
|
|
key: "emerald_core",
|
|
name: "Núcleo de Esmeralda",
|
|
guildId,
|
|
effects: { defenseBonus: 10, maxHpBonus: 20 } as any,
|
|
} as any,
|
|
});
|
|
}
|
|
|
|
// --- Recipes (crafteo): iron_ingot <- iron ore x3
|
|
// Create ingredient items if missing
|
|
await upsertEconomyItem(guildId, "ingot.iron", {
|
|
name: "Lingote de Hierro",
|
|
stackable: true,
|
|
props: {} as unknown as Prisma.InputJsonValue,
|
|
tags: ["ingot"],
|
|
});
|
|
await upsertEconomyItem(guildId, "ore.iron", {
|
|
name: "Mineral de Hierro",
|
|
stackable: true,
|
|
props: {} as unknown as Prisma.InputJsonValue,
|
|
tags: ["ore"],
|
|
});
|
|
await upsertItemRecipe(
|
|
guildId,
|
|
"ingot.iron",
|
|
[{ itemKey: "ore.iron", qty: 3 }],
|
|
1
|
|
);
|
|
|
|
// --- Areas & Levels ---
|
|
const mine = await upsertGameArea(guildId, "mine.cavern", {
|
|
name: "Mina: Caverna",
|
|
type: "MINE",
|
|
config: { cooldownSeconds: 10 } as unknown as Prisma.InputJsonValue,
|
|
});
|
|
await prisma.gameAreaLevel.upsert({
|
|
where: { areaId_level: { areaId: mine.id, level: 1 } },
|
|
update: {},
|
|
create: {
|
|
areaId: mine.id,
|
|
level: 1,
|
|
requirements: {
|
|
tool: { required: true, toolType: "pickaxe", minTier: 1 },
|
|
} as any,
|
|
rewards: {
|
|
draws: 2,
|
|
table: [
|
|
{ type: "item", itemKey: "ore.iron", qty: 2, weight: 70 },
|
|
{ type: "coins", amount: 10, weight: 30 },
|
|
],
|
|
} as any,
|
|
mobs: {
|
|
draws: 1,
|
|
table: [
|
|
{ mobKey: "slime.green", weight: 50 },
|
|
{ mobKey: "bat", weight: 50 },
|
|
],
|
|
} as any,
|
|
},
|
|
});
|
|
|
|
const lagoon = await upsertGameArea(guildId, "lagoon.shore", {
|
|
name: "Laguna: Orilla",
|
|
type: "LAGOON",
|
|
config: { cooldownSeconds: 12 } as unknown as Prisma.InputJsonValue,
|
|
});
|
|
await prisma.gameAreaLevel.upsert({
|
|
where: { areaId_level: { areaId: lagoon.id, level: 1 } },
|
|
update: {},
|
|
create: {
|
|
areaId: lagoon.id,
|
|
level: 1,
|
|
requirements: {
|
|
tool: { required: true, toolType: "rod", minTier: 1 },
|
|
} as any,
|
|
rewards: {
|
|
draws: 2,
|
|
table: [
|
|
{ type: "item", itemKey: "food.meat.small", qty: 1, weight: 70 },
|
|
{ type: "coins", amount: 10, weight: 30 },
|
|
],
|
|
} as any,
|
|
mobs: { draws: 0, table: [] } as any,
|
|
},
|
|
});
|
|
|
|
// --- Basic mobs ---
|
|
await upsertMob(guildId, "slime.green", {
|
|
name: "Slime Verde",
|
|
stats: { attack: 4, hp: 18 } as any,
|
|
drops: [{ itemKey: "ingot.iron", qty: 1, weight: 10 }] as any,
|
|
});
|
|
await upsertMob(guildId, "bat", {
|
|
name: "Murciélago",
|
|
stats: { attack: 3, hp: 10 } as any,
|
|
drops: Prisma.DbNull,
|
|
});
|
|
|
|
// Advanced mobs
|
|
await upsertMob(guildId, "goblin", {
|
|
name: "Duende",
|
|
stats: { attack: 8, hp: 30 } as any,
|
|
drops: [{ itemKey: "ore.iron", qty: 1, weight: 50 }] as any,
|
|
});
|
|
await upsertMob(guildId, "orc", {
|
|
name: "Orco",
|
|
stats: { attack: 12, hp: 50 } as any,
|
|
drops: Prisma.DbNull,
|
|
});
|
|
|
|
// Programar un par de ataques demo (opcional)
|
|
const targetUser = process.env.TARGET_USER ?? null;
|
|
if (targetUser) {
|
|
const slime = await prisma.mob.findFirst({
|
|
where: { key: "slime.green", OR: [{ guildId }, { guildId: null }] },
|
|
orderBy: [{ guildId: "desc" }],
|
|
});
|
|
if (slime) {
|
|
const now = Date.now();
|
|
await prisma.scheduledMobAttack.createMany({
|
|
data: [
|
|
{
|
|
userId: targetUser,
|
|
guildId: guildId ?? "global",
|
|
mobId: slime.id,
|
|
scheduleAt: new Date(now + 5_000),
|
|
},
|
|
{
|
|
userId: targetUser,
|
|
guildId: guildId ?? "global",
|
|
mobId: slime.id,
|
|
scheduleAt: new Date(now + 15_000),
|
|
},
|
|
],
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log("Full server setup complete.");
|
|
}
|
|
|
|
// Backwards-compatible CLI entry
|
|
if (require.main === module) {
|
|
const gid = process.env.GUILD_ID ?? DEFAULT_GUILD;
|
|
runFullServerSetup(gid)
|
|
.then(() => process.exit(0))
|
|
.catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|
|
}
|