feat: rewrite seed-dev-data for MariaDB with test users and campaign members
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ec75add5b7
commit
39f8220eb7
1 changed files with 159 additions and 194 deletions
|
|
@ -1,210 +1,175 @@
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import type { ResultSetHeader, RowDataPacket } from "mysql2";
|
||||||
import db from "./db.js";
|
import db from "./db.js";
|
||||||
|
import { SEED_ITEMS } from "./seed-items.js";
|
||||||
|
import { SEED_TALENTS } from "./seed-talents.js";
|
||||||
|
|
||||||
export function seedDevData() {
|
export async function seedDevData(): Promise<void> {
|
||||||
const hasCampaigns = (
|
const [userRows] = await db.execute<RowDataPacket[]>(
|
||||||
db.prepare("SELECT COUNT(*) as c FROM campaigns").get() as { c: number }
|
"SELECT COUNT(*) as c FROM users"
|
||||||
).c;
|
);
|
||||||
if (hasCampaigns > 0) return;
|
if ((userRows[0] as { c: number }).c > 0) return;
|
||||||
|
|
||||||
|
const passwordHash = await bcrypt.hash("password", 12);
|
||||||
|
|
||||||
|
// Create DM user
|
||||||
|
const [dmResult] = await db.execute<ResultSetHeader>(
|
||||||
|
"INSERT INTO users (email, username, password_hash) VALUES (?, ?, ?)",
|
||||||
|
["dm@darkwatch.test", "DungeonMaster", passwordHash]
|
||||||
|
);
|
||||||
|
const dmId = dmResult.insertId;
|
||||||
|
|
||||||
|
// Create Player user
|
||||||
|
const [playerResult] = await db.execute<ResultSetHeader>(
|
||||||
|
"INSERT INTO users (email, username, password_hash) VALUES (?, ?, ?)",
|
||||||
|
["player@darkwatch.test", "Adventurer", passwordHash]
|
||||||
|
);
|
||||||
|
const playerId = playerResult.insertId;
|
||||||
|
|
||||||
// Create campaign
|
// Create campaign
|
||||||
db.prepare(
|
const [campaignResult] = await db.execute<ResultSetHeader>(
|
||||||
"INSERT INTO campaigns (name) VALUES ('Tomb of the Serpent King')",
|
"INSERT INTO campaigns (name) VALUES (?)",
|
||||||
).run();
|
["Tomb of the Serpent King"]
|
||||||
|
);
|
||||||
|
const campaignId = campaignResult.insertId;
|
||||||
|
|
||||||
const insertChar = db.prepare(`
|
// Add DM as dm, player as player
|
||||||
INSERT INTO characters (campaign_id, name, class, ancestry, level, xp, hp_current, hp_max, ac, alignment, title, background, deity, languages, gp, sp, cp, color)
|
await db.execute(
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
"INSERT INTO campaign_members (campaign_id, user_id, role) VALUES (?, ?, 'dm')",
|
||||||
`);
|
[campaignId, dmId]
|
||||||
|
);
|
||||||
const insertStat = db.prepare(
|
await db.execute(
|
||||||
"INSERT INTO character_stats (character_id, stat_name, value) VALUES (?, ?, ?)",
|
"INSERT INTO campaign_members (campaign_id, user_id, role) VALUES (?, ?, 'player')",
|
||||||
|
[campaignId, playerId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const insertGear = db.prepare(
|
// Create invite token for testing
|
||||||
"INSERT INTO character_gear (character_id, name, type, slot_count, properties, effects, game_item_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
await db.execute(
|
||||||
|
"INSERT INTO campaign_invites (campaign_id, token, created_by) VALUES (?, ?, ?)",
|
||||||
|
[campaignId, "dev-invite-token-abc123", dmId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const insertTalent = db.prepare(
|
// Seed game_items from seed-items.ts
|
||||||
"INSERT INTO character_talents (character_id, name, description, effect, game_talent_id) VALUES (?, ?, ?, ?, ?)",
|
for (const item of SEED_ITEMS) {
|
||||||
);
|
await db.execute(
|
||||||
|
"INSERT INTO game_items (name, type, slot_count, effects, properties) VALUES (?, ?, ?, ?, ?)",
|
||||||
// --- Limpie ---
|
[
|
||||||
const limpie = insertChar.run(
|
item.name,
|
||||||
1,
|
item.type,
|
||||||
"Limpie",
|
item.slot_count,
|
||||||
"Thief",
|
JSON.stringify(item.effects),
|
||||||
"Goblin",
|
JSON.stringify(item.properties),
|
||||||
1,
|
]
|
||||||
6,
|
);
|
||||||
1,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
"Lawful",
|
|
||||||
"Footpad",
|
|
||||||
"Chirurgeo",
|
|
||||||
"Madeera the Covenant",
|
|
||||||
"Common, Goblin",
|
|
||||||
8,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
"hsl(45, 60%, 65%)",
|
|
||||||
);
|
|
||||||
const limpieId = limpie.lastInsertRowid;
|
|
||||||
|
|
||||||
// Limpie stats
|
|
||||||
const limpieStats: [string, number][] = [
|
|
||||||
["STR", 11],
|
|
||||||
["DEX", 11],
|
|
||||||
["CON", 8],
|
|
||||||
["INT", 10],
|
|
||||||
["WIS", 14],
|
|
||||||
["CHA", 10],
|
|
||||||
];
|
|
||||||
for (const [name, value] of limpieStats) {
|
|
||||||
insertStat.run(limpieId, name, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limpie gear
|
// Seed game_talents from seed-talents.ts
|
||||||
insertGear.run(
|
for (const talent of SEED_TALENTS) {
|
||||||
limpieId,
|
await db.execute(
|
||||||
"Shortsword",
|
"INSERT INTO game_talents (name, source, description, effect) VALUES (?, ?, ?, ?)",
|
||||||
"weapon",
|
[
|
||||||
1,
|
talent.name,
|
||||||
"{}",
|
talent.source,
|
||||||
'{"damage":"1d6","melee":true,"stat":"STR"}',
|
talent.description,
|
||||||
null,
|
JSON.stringify(talent.effect),
|
||||||
);
|
]
|
||||||
insertGear.run(
|
);
|
||||||
limpieId,
|
|
||||||
"Shortbow",
|
|
||||||
"weapon",
|
|
||||||
1,
|
|
||||||
"{}",
|
|
||||||
'{"damage":"1d4","ranged":true,"stat":"DEX","two_handed":true}',
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
insertGear.run(
|
|
||||||
limpieId,
|
|
||||||
"Leather Armor",
|
|
||||||
"armor",
|
|
||||||
1,
|
|
||||||
"{}",
|
|
||||||
'{"ac_base":11,"ac_dex":true}',
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
insertGear.run(limpieId, "Shield", "armor", 1, "{}", '{"ac_bonus":2}', null);
|
|
||||||
insertGear.run(limpieId, "Arrows (20)", "gear", 1, "{}", "{}", null);
|
|
||||||
insertGear.run(limpieId, "Torch", "gear", 1, "{}", "{}", null);
|
|
||||||
insertGear.run(limpieId, "Rations", "gear", 1, "{}", "{}", null);
|
|
||||||
insertGear.run(limpieId, "Rope (60ft)", "gear", 1, "{}", "{}", null);
|
|
||||||
insertGear.run(limpieId, "Thieves' Tools", "gear", 1, "{}", "{}", null);
|
|
||||||
|
|
||||||
// Limpie talents
|
|
||||||
insertTalent.run(
|
|
||||||
limpieId,
|
|
||||||
"Backstab",
|
|
||||||
"Extra 1 + half level (round down) weapon dice of damage with surprise attacks",
|
|
||||||
'{"damage_bonus_surprise":true}',
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
insertTalent.run(
|
|
||||||
limpieId,
|
|
||||||
"Thievery",
|
|
||||||
"Trained in climbing, sneaking, hiding, disguise, finding & disabling traps, delicate tasks",
|
|
||||||
"{}",
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
insertTalent.run(
|
|
||||||
limpieId,
|
|
||||||
"Keen Senses",
|
|
||||||
"Can't be surprised",
|
|
||||||
'{"immune_surprise":true}',
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Brynn ---
|
|
||||||
const brynn = insertChar.run(
|
|
||||||
1,
|
|
||||||
"Brynn",
|
|
||||||
"Fighter",
|
|
||||||
"Human",
|
|
||||||
2,
|
|
||||||
15,
|
|
||||||
8,
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
"Neutral",
|
|
||||||
"the Bold",
|
|
||||||
"Soldier",
|
|
||||||
"Gede",
|
|
||||||
"Common",
|
|
||||||
25,
|
|
||||||
5,
|
|
||||||
10,
|
|
||||||
"hsl(200, 60%, 65%)",
|
|
||||||
);
|
|
||||||
const brynnId = brynn.lastInsertRowid;
|
|
||||||
|
|
||||||
// Brynn stats
|
|
||||||
const brynnStats: [string, number][] = [
|
|
||||||
["STR", 16],
|
|
||||||
["DEX", 12],
|
|
||||||
["CON", 14],
|
|
||||||
["INT", 8],
|
|
||||||
["WIS", 10],
|
|
||||||
["CHA", 11],
|
|
||||||
];
|
|
||||||
for (const [name, value] of brynnStats) {
|
|
||||||
insertStat.run(brynnId, name, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Brynn gear
|
async function createCharacter(
|
||||||
insertGear.run(
|
charCampaignId: number,
|
||||||
brynnId,
|
userId: number,
|
||||||
"Longsword",
|
data: {
|
||||||
"weapon",
|
name: string; class: string; ancestry: string; level: number; xp: number;
|
||||||
1,
|
hp_current: number; hp_max: number; ac: number; alignment: string;
|
||||||
"{}",
|
title: string; background: string; deity: string; languages: string;
|
||||||
'{"damage":"1d8","melee":true,"stat":"STR"}',
|
gp: number; sp: number; cp: number; color: string;
|
||||||
null,
|
}
|
||||||
);
|
): Promise<number> {
|
||||||
insertGear.run(
|
const [r] = await db.execute<ResultSetHeader>(
|
||||||
brynnId,
|
`INSERT INTO characters
|
||||||
"Chainmail",
|
(campaign_id, user_id, name, class, ancestry, level, xp, hp_current, hp_max,
|
||||||
"armor",
|
ac, alignment, title, background, deity, languages, gp, sp, cp, color)
|
||||||
1,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
"{}",
|
[charCampaignId, userId, data.name, data.class, data.ancestry, data.level, data.xp,
|
||||||
'{"ac_base":13,"ac_dex":true}',
|
data.hp_current, data.hp_max, data.ac, data.alignment, data.title,
|
||||||
null,
|
data.background, data.deity, data.languages, data.gp, data.sp, data.cp, data.color]
|
||||||
);
|
);
|
||||||
insertGear.run(brynnId, "Shield", "armor", 1, "{}", '{"ac_bonus":2}', null);
|
return r.insertId;
|
||||||
insertGear.run(
|
}
|
||||||
brynnId,
|
|
||||||
"Javelin",
|
|
||||||
"weapon",
|
|
||||||
1,
|
|
||||||
"{}",
|
|
||||||
'{"damage":"1d4","melee":true,"stat":"STR","thrown":true,"range":"far"}',
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
insertGear.run(brynnId, "Torch", "gear", 1, "{}", "{}", null);
|
|
||||||
insertGear.run(brynnId, "Rations", "gear", 1, "{}", "{}", null);
|
|
||||||
|
|
||||||
// Brynn talents
|
async function addStats(charId: number, stats: [string, number][]) {
|
||||||
insertTalent.run(
|
for (const [name, value] of stats) {
|
||||||
brynnId,
|
await db.execute(
|
||||||
"Weapon Mastery",
|
"INSERT INTO character_stats (character_id, stat_name, value) VALUES (?, ?, ?)",
|
||||||
"+1 to attack and damage with longswords",
|
[charId, name, value]
|
||||||
'{"attack_bonus":1,"damage_bonus":1}',
|
);
|
||||||
null,
|
}
|
||||||
);
|
}
|
||||||
insertTalent.run(
|
|
||||||
brynnId,
|
|
||||||
"Grit",
|
|
||||||
"+2 HP and +1 HP each level",
|
|
||||||
'{"hp_bonus":2,"hp_per_level":1}',
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("Dev data seeded: campaign + 2 characters (Limpie & Brynn)");
|
async function addGear(charId: number, items: { name: string; type: string; slot_count: number; properties: string; effects: string }[]) {
|
||||||
|
for (const item of items) {
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO character_gear (character_id, name, type, slot_count, properties, effects) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
[charId, item.name, item.type, item.slot_count, item.properties, item.effects]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addTalents(charId: number, talents: { name: string; description: string; effect: string }[]) {
|
||||||
|
for (const t of talents) {
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO character_talents (character_id, name, description, effect) VALUES (?, ?, ?, ?)",
|
||||||
|
[charId, t.name, t.description, t.effect]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Limpie (player's character 1) ---
|
||||||
|
const limpieId = await createCharacter(campaignId, playerId, {
|
||||||
|
name: "Limpie", class: "Thief", ancestry: "Goblin", level: 1, xp: 6,
|
||||||
|
hp_current: 1, hp_max: 1, ac: 10, alignment: "Lawful", title: "Footpad",
|
||||||
|
background: "Chirurgeo", deity: "Madeera the Covenant",
|
||||||
|
languages: "Common, Goblin", gp: 8, sp: 0, cp: 0, color: "hsl(45, 60%, 65%)",
|
||||||
|
});
|
||||||
|
await addStats(limpieId, [["STR",11],["DEX",11],["CON",8],["INT",10],["WIS",14],["CHA",10]]);
|
||||||
|
await addGear(limpieId, [
|
||||||
|
{ name: "Shortsword", type: "weapon", slot_count: 1, properties: "{}", effects: '{"damage":"1d6","melee":true,"stat":"STR"}' },
|
||||||
|
{ name: "Shortbow", type: "weapon", slot_count: 1, properties: "{}", effects: '{"damage":"1d4","ranged":true,"stat":"DEX","two_handed":true}' },
|
||||||
|
{ name: "Leather Armor", type: "armor", slot_count: 1, properties: "{}", effects: '{"ac_base":11,"ac_dex":true}' },
|
||||||
|
{ name: "Shield", type: "armor", slot_count: 1, properties: "{}", effects: '{"ac_bonus":2}' },
|
||||||
|
{ name: "Arrows (20)", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
|
||||||
|
{ name: "Torch", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
|
||||||
|
{ name: "Rations", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
|
||||||
|
{ name: "Rope (60ft)", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
|
||||||
|
{ name: "Thieves' Tools", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
|
||||||
|
]);
|
||||||
|
await addTalents(limpieId, [
|
||||||
|
{ name: "Backstab", description: "Extra 1 + half level (round down) weapon dice of damage with surprise attacks", effect: '{"damage_bonus_surprise":true}' },
|
||||||
|
{ name: "Thievery", description: "Trained in climbing, sneaking, hiding, disguise, finding & disabling traps, delicate tasks", effect: "{}" },
|
||||||
|
{ name: "Keen Senses", description: "Can't be surprised", effect: '{"immune_surprise":true}' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// --- Brynn (player's character 2) ---
|
||||||
|
const brynnId = await createCharacter(campaignId, playerId, {
|
||||||
|
name: "Brynn", class: "Fighter", ancestry: "Human", level: 2, xp: 15,
|
||||||
|
hp_current: 8, hp_max: 10, ac: 10, alignment: "Neutral", title: "the Bold",
|
||||||
|
background: "Soldier", deity: "Gede", languages: "Common",
|
||||||
|
gp: 25, sp: 5, cp: 10, color: "hsl(200, 60%, 65%)",
|
||||||
|
});
|
||||||
|
await addStats(brynnId, [["STR",16],["DEX",12],["CON",14],["INT",8],["WIS",10],["CHA",11]]);
|
||||||
|
await addGear(brynnId, [
|
||||||
|
{ name: "Longsword", type: "weapon", slot_count: 1, properties: "{}", effects: '{"damage":"1d8","melee":true,"stat":"STR"}' },
|
||||||
|
{ name: "Chainmail", type: "armor", slot_count: 1, properties: "{}", effects: '{"ac_base":13,"ac_dex":true}' },
|
||||||
|
{ name: "Shield", type: "armor", slot_count: 1, properties: "{}", effects: '{"ac_bonus":2}' },
|
||||||
|
{ name: "Javelin", type: "weapon", slot_count: 1, properties: "{}", effects: '{"damage":"1d4","melee":true,"stat":"STR","thrown":true,"range":"far"}' },
|
||||||
|
{ name: "Torch", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
|
||||||
|
{ name: "Rations", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
|
||||||
|
]);
|
||||||
|
await addTalents(brynnId, [
|
||||||
|
{ name: "Weapon Mastery", description: "+1 to attack and damage with longswords", effect: '{"attack_bonus":1,"damage_bonus":1}' },
|
||||||
|
{ name: "Grit", description: "+2 HP and +1 HP each level", effect: '{"hp_bonus":2,"hp_per_level":1}' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log("Dev data seeded: 2 users, 1 campaign, 2 characters (Limpie & Brynn)");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue