piclets / src /lib /db /piclets.ts
Fraser's picture
rm spec ab stuff
0959fa0
raw
history blame
9.6 kB
import { db } from './index';
import type { PicletInstance, BattleMove } from './schema';
import { PicletType, getTypeFromConcept } from '../types/picletTypes';
import type { PicletStats } from '../types';
import type { Move as BattleEngineMove } from '../battle-engine/types';
import { generateUnlockLevels } from '../services/unlockLevels';
// Interface for generated piclet data (from PicletResult component)
interface GeneratedPicletData {
name: string;
imageUrl: string;
imageData?: string;
imageCaption: string;
concept: string;
imagePrompt: string;
stats: PicletStats;
createdAt: Date;
}
// Convert generated piclet data to a PicletInstance
export async function generatedDataToPicletInstance(data: GeneratedPicletData, level: number = 5): Promise<Omit<PicletInstance, 'id'>> {
if (!data.stats) {
throw new Error('Generated data must have stats to create PicletInstance');
}
// All generated data must now have the battle-ready format
const stats = data.stats as PicletStats;
if (!stats.baseStats || !stats.specialAbility || !stats.movepool) {
throw new Error('Generated stats must be in battle-ready format with baseStats, specialAbility, and movepool');
}
// Calculate base stats from battle-ready format
const baseHp = Math.floor(stats.baseStats.hp * 2 + 50);
const baseAttack = Math.floor(stats.baseStats.attack * 1.5 + 30);
const baseDefense = Math.floor(stats.baseStats.defense * 1.5 + 30);
const baseSpeed = Math.floor(stats.baseStats.speed * 1.5 + 30);
// Determine primary type from battle stats
const normalizedPrimaryType = stats.primaryType.toLowerCase();
const validPrimaryType = Object.values(PicletType).find(type => type === normalizedPrimaryType);
const primaryType = validPrimaryType || getTypeFromConcept(data.concept, data.imageCaption);
if (!validPrimaryType) {
console.warn(`Invalid primaryType "${stats.primaryType}" from stats, falling back to concept detection`);
}
// Handle secondary type
let secondaryType: PicletType | undefined = undefined;
if (stats.secondaryType && stats.secondaryType !== null) {
const normalizedSecondaryType = stats.secondaryType.toLowerCase();
const validSecondaryType = Object.values(PicletType).find(type => type === normalizedSecondaryType);
secondaryType = validSecondaryType;
if (!validSecondaryType) {
console.warn(`Invalid secondaryType "${stats.secondaryType}" from stats, ignoring`);
}
}
// Create moves from battle-ready format preserving all data
// Convert from PicletStats.BattleMove (loose types) to schema.BattleMove (strict types)
const baseMoves: BattleMove[] = stats.movepool.map(move => ({
name: move.name,
type: move.type as any, // Type conversion from string union to AttackType enum
power: move.power,
accuracy: move.accuracy,
pp: move.pp,
priority: move.priority,
flags: move.flags as any, // Type conversion from string[] to MoveFlag[]
effects: move.effects as any, // Type conversion between BattleEffect types
currentPp: move.pp,
unlockLevel: 1 // Temporary, will be set below
}));
// Generate unlock levels for moves and special ability
// Convert from PicletStats.SpecialAbility to battle-engine.SpecialAbility
const convertedSpecialAbility = {
name: stats.specialAbility.name,
description: `Special ability of ${stats.name}`, // Generate a generic description since it's removed from stats
effects: stats.specialAbility.effects as any,
triggers: stats.specialAbility.triggers as any
};
const { movesWithUnlocks, abilityUnlockLevel } = generateUnlockLevels(baseMoves, convertedSpecialAbility);
const moves = movesWithUnlocks;
// Field stats are variations of regular stats
const baseFieldAttack = Math.floor(baseAttack * 0.8);
const baseFieldDefense = Math.floor(baseDefense * 0.8);
// Use Pokemon-accurate stat calculations (matching levelingService)
const calculateStat = (base: number, level: number) => {
if (level === 1) {
return Math.max(1, Math.floor(base / 10) + 5);
}
return Math.floor((2 * base * level) / 100) + 5;
};
const calculateHp = (base: number, level: number) => {
if (level === 1) {
return Math.max(1, Math.floor(base / 10) + 11);
}
return Math.floor((2 * base * level) / 100) + level + 10;
};
const maxHp = calculateHp(baseHp, level);
const bst = baseHp + baseAttack + baseDefense + baseFieldAttack + baseFieldDefense + baseSpeed;
// Check if this is the first piclet (no existing piclets in database)
const existingPiclets = await db.picletInstances.count();
const isFirstPiclet = existingPiclets === 0;
return {
// Type Info
typeId: data.name.toLowerCase().replace(/\s+/g, '-'),
nickname: stats.name || data.name, // Use stats.name if available, fallback to data.name
primaryType: primaryType,
secondaryType: secondaryType,
// Current Stats
currentHp: maxHp,
maxHp,
level,
xp: 0,
attack: calculateStat(baseAttack, level),
defense: calculateStat(baseDefense, level),
fieldAttack: calculateStat(baseFieldAttack, level),
fieldDefense: calculateStat(baseFieldDefense, level),
speed: calculateStat(baseSpeed, level),
// Base Stats
baseHp,
baseAttack,
baseDefense,
baseFieldAttack,
baseFieldDefense,
baseSpeed,
// Battle
moves,
nature: stats.nature,
specialAbility: convertedSpecialAbility,
specialAbilityUnlockLevel: abilityUnlockLevel,
// Roster
isInRoster: isFirstPiclet,
rosterPosition: isFirstPiclet ? 0 : undefined,
// Metadata
caught: isFirstPiclet, // First piclet is automatically caught
caughtAt: isFirstPiclet ? new Date() : undefined,
bst,
tier: stats.tier, // Use tier from stats
role: 'balanced', // Could be enhanced based on stat distribution
variance: 0,
// Original generation data
imageUrl: data.imageUrl,
imageData: data.imageData,
imageCaption: data.imageCaption,
concept: data.concept,
description: stats.description,
imagePrompt: data.imagePrompt
};
}
// Save a new PicletInstance
export async function savePicletInstance(piclet: Omit<PicletInstance, 'id'>): Promise<number> {
return await db.picletInstances.add(piclet);
}
// Mark a Piclet as caught
export async function catchPiclet(picletId: number): Promise<void> {
await db.picletInstances.update(picletId, {
caught: true,
caughtAt: new Date()
});
}
// Get only caught Piclets (for Pictuary and battle roster)
export async function getCaughtPiclets(): Promise<PicletInstance[]> {
const allPiclets = await db.picletInstances.toArray();
return allPiclets.filter(p => p.caught === true);
}
// Get uncaught Piclets (for encounters)
export async function getUncaughtPiclets(): Promise<PicletInstance[]> {
const allPiclets = await db.picletInstances.toArray();
return allPiclets.filter(p => p.caught === false);
}
// Get all PicletInstances
export async function getAllPicletInstances(): Promise<PicletInstance[]> {
return await db.picletInstances.toArray();
}
// Get roster PicletInstances
export async function getRosterPiclets(): Promise<PicletInstance[]> {
const allPiclets = await db.picletInstances.toArray();
return allPiclets
.filter(p =>
p.caught === true && // Only caught Piclets can be in roster
p.rosterPosition !== undefined &&
p.rosterPosition !== null &&
p.rosterPosition >= 0 &&
p.rosterPosition <= 5
)
.sort((a, b) => (a.rosterPosition ?? 0) - (b.rosterPosition ?? 0));
}
// Update roster position
export async function updateRosterPosition(id: number, position: number | undefined): Promise<void> {
await db.picletInstances.update(id, {
isInRoster: position !== undefined,
rosterPosition: position
});
}
// Move piclet to roster
export async function moveToRoster(id: number, position: number): Promise<void> {
// Check if position is already occupied
const existingPiclet = await db.picletInstances
.where('rosterPosition')
.equals(position)
.and(item => item.isInRoster)
.first();
if (existingPiclet) {
// Move existing piclet to storage
await db.picletInstances.update(existingPiclet.id!, {
isInRoster: false,
rosterPosition: undefined
});
}
// Move new piclet to roster
await db.picletInstances.update(id, {
isInRoster: true,
rosterPosition: position
});
}
// Swap roster positions
export async function swapRosterPositions(id1: number, position1: number, id2: number, position2: number): Promise<void> {
await db.transaction('rw', db.picletInstances, async () => {
await db.picletInstances.update(id1, { rosterPosition: position2 });
await db.picletInstances.update(id2, { rosterPosition: position1 });
});
}
// Move piclet to storage
export async function moveToStorage(id: number): Promise<void> {
await db.picletInstances.update(id, {
isInRoster: false,
rosterPosition: undefined
});
}
// Get storage piclets
export async function getStoragePiclets(): Promise<PicletInstance[]> {
const allPiclets = await db.picletInstances.toArray();
return allPiclets.filter(p =>
p.caught === true && // Only caught Piclets can be in storage
(p.rosterPosition === undefined ||
p.rosterPosition === null ||
p.rosterPosition < 0 ||
p.rosterPosition > 5)
);
}
// Delete a PicletInstance
export async function deletePicletInstance(id: number): Promise<void> {
await db.picletInstances.delete(id);
}