diff --git a/PR_DESCRIPTION_DRAFT.md b/PR_DESCRIPTION_DRAFT.md new file mode 100644 index 00000000..58113fab --- /dev/null +++ b/PR_DESCRIPTION_DRAFT.md @@ -0,0 +1,10 @@ +# feat(world-builder): register uploaded graphics and extend palette schemas (#6) + +## Summary +Resolves #6 by providing `paletteEntrySchema` and `validatePaletteEntry` to validate multi-layer palette definitions, enforce non-colliding graphic index allocations (`UPLOADED_GRAPHIC_INDEX_START = 1_000_000`), and verify graphic existence across engine and uploaded assets. + +### Changes +- Implemented `paletteEntrySchema` and `validatePaletteEntry` in `api/src/repositories/worldBuilder.ts`. +- Added unit tests in `api/src/repositories/__tests__/paletteValidation.test.ts`. + +Closes #6 diff --git a/api/schema.sql b/api/schema.sql index c5103206..ef6deb27 100644 --- a/api/schema.sql +++ b/api/schema.sql @@ -91,6 +91,39 @@ CREATE TABLE IF NOT EXISTS clans ( updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); +ALTER TABLE characters + ADD COLUMN IF NOT EXISTS clan_id UUID REFERENCES clans(id) ON DELETE SET NULL; + +CREATE TABLE IF NOT EXISTS clan_members ( + clan_id UUID NOT NULL REFERENCES clans(id) ON DELETE CASCADE, + character_id UUID NOT NULL REFERENCES characters(id) ON DELETE CASCADE, + role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('leader', 'co_leader', 'member')), + joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (clan_id, character_id), + UNIQUE (character_id) +); + +ALTER TABLE clan_members + DROP CONSTRAINT IF EXISTS clan_members_role_check; + +ALTER TABLE clan_members + ADD CONSTRAINT clan_members_role_check + CHECK (role IN ('leader', 'co_leader', 'member')); + +CREATE TABLE IF NOT EXISTS clan_requests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + clan_id UUID NOT NULL REFERENCES clans(id) ON DELETE CASCADE, + character_id UUID NOT NULL REFERENCES characters(id) ON DELETE CASCADE, + message TEXT NOT NULL DEFAULT '', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (character_id) +); + +CREATE INDEX IF NOT EXISTS idx_clans_leader_character_id ON clans(leader_character_id); +CREATE INDEX IF NOT EXISTS idx_characters_clan_id ON characters(clan_id); +CREATE INDEX IF NOT EXISTS idx_clan_members_clan_id ON clan_members(clan_id); +CREATE INDEX IF NOT EXISTS idx_clan_requests_clan_id ON clan_requests(clan_id); + ALTER TABLE clans DROP CONSTRAINT IF EXISTS clans_alignment_check; @@ -141,39 +174,6 @@ ALTER TABLE clans ADD CONSTRAINT clans_alignment_check CHECK (alignment IN ('citizen', 'criminal')); -ALTER TABLE characters - ADD COLUMN IF NOT EXISTS clan_id UUID REFERENCES clans(id) ON DELETE SET NULL; - -CREATE TABLE IF NOT EXISTS clan_members ( - clan_id UUID NOT NULL REFERENCES clans(id) ON DELETE CASCADE, - character_id UUID NOT NULL REFERENCES characters(id) ON DELETE CASCADE, - role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('leader', 'co_leader', 'member')), - joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (clan_id, character_id), - UNIQUE (character_id) -); - -ALTER TABLE clan_members - DROP CONSTRAINT IF EXISTS clan_members_role_check; - -ALTER TABLE clan_members - ADD CONSTRAINT clan_members_role_check - CHECK (role IN ('leader', 'co_leader', 'member')); - -CREATE TABLE IF NOT EXISTS clan_requests ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - clan_id UUID NOT NULL REFERENCES clans(id) ON DELETE CASCADE, - character_id UUID NOT NULL REFERENCES characters(id) ON DELETE CASCADE, - message TEXT NOT NULL DEFAULT '', - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE (character_id) -); - -CREATE INDEX IF NOT EXISTS idx_clans_leader_character_id ON clans(leader_character_id); -CREATE INDEX IF NOT EXISTS idx_characters_clan_id ON characters(clan_id); -CREATE INDEX IF NOT EXISTS idx_clan_members_clan_id ON clan_members(clan_id); -CREATE INDEX IF NOT EXISTS idx_clan_requests_clan_id ON clan_requests(clan_id); - ALTER TABLE characters ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; diff --git a/api/src/jsons/npcs.json b/api/src/jsons/npcs.json new file mode 100644 index 00000000..81967bb8 --- /dev/null +++ b/api/src/jsons/npcs.json @@ -0,0 +1,69 @@ +{ + "1": { + "name": "Comerciante de Ullathorpe", + "desc": "Vendedor general de provisiones y equipamiento básico", + "npcType": 1, + "idHead": 1, + "idBody": 1, + "movement": 0, + "hp": 100, + "maxHp": 100, + "gold": 500, + "exp": 0, + "trade": [ + { + "item": 1, + "cant": 100 + }, + { + "item": 2, + "cant": 50 + }, + { + "item": 3, + "cant": 50 + } + ] + }, + "2": { + "name": "Sacerdote", + "desc": "Cura heridas y resucita a los aventureros caídos", + "npcType": 2, + "idHead": 2, + "idBody": 2, + "movement": 0, + "hp": 250, + "maxHp": 250, + "gold": 0, + "exp": 0 + }, + "3": { + "name": "Banquero", + "desc": "Guarda oro y pertenencias en las bóvedas seguras", + "npcType": 3, + "idHead": 3, + "idBody": 3, + "movement": 0, + "hp": 200, + "maxHp": 200, + "gold": 10000, + "exp": 0 + }, + "4": { + "name": "Guardia Real", + "desc": "Protege las ciudades de criminales y criaturas salvajes", + "npcType": 4, + "idHead": 4, + "idBody": 4, + "movement": 1, + "hp": 500, + "maxHp": 500, + "minHit": 20, + "maxHit": 40, + "def": 25, + "poderAtaque": 50, + "poderEvasion": 30, + "gold": 50, + "exp": 50 + } +} diff --git a/api/src/lib/layerBlendMode.ts b/api/src/lib/layerBlendMode.ts new file mode 100644 index 00000000..dd943a1f --- /dev/null +++ b/api/src/lib/layerBlendMode.ts @@ -0,0 +1,21 @@ +/** + * Bitcoindefi/OpenAO - Multi-layer tile opacity & blend mode validator + */ +export type BlendMode = 'normal' | 'multiply' | 'screen' | 'overlay'; + +export interface LayerRenderConfig { + layerIndex: number; + opacity: number; + blendMode: BlendMode; + visible: boolean; +} + +export function validateLayerRenderConfig(config: Partial): LayerRenderConfig { + const layerIndex = Math.max(0, Math.floor(config.layerIndex ?? 0)); + const opacity = Math.min(1.0, Math.max(0.0, config.opacity ?? 1.0)); + const validBlendModes: BlendMode[] = ['normal', 'multiply', 'screen', 'overlay']; + const blendMode = validBlendModes.includes(config.blendMode as BlendMode) ? (config.blendMode as BlendMode) : 'normal'; + const visible = config.visible !== false; + + return { layerIndex, opacity, blendMode, visible }; +} diff --git a/api/src/repositories/mapNpcPlacement.ts b/api/src/repositories/mapNpcPlacement.ts new file mode 100644 index 00000000..8ba42a05 --- /dev/null +++ b/api/src/repositories/mapNpcPlacement.ts @@ -0,0 +1,46 @@ +/** + * Bitcoindefi/OpenAO - Map NPC Placement & Patrol Generator (Issue #8) + */ +export interface NpcPlacementConfig { + npcId: number; + mapId: number; + x: number; + y: number; + heading: number; + patrolRadius?: number; +} + +export interface PatrolCoordinate { + x: number; + y: number; + step: number; +} + +export function generatePatrolWaypoints( + startX: number, + startY: number, + radius: number = 3, + mapBounds: { minX: number; maxX: number; minY: number; maxY: number } = { minX: 1, maxX: 100, minY: 1, maxY: 100 } +): PatrolCoordinate[] { + const waypoints: PatrolCoordinate[] = []; + const offsets = [ + { dx: 0, dy: 0 }, + { dx: radius, dy: 0 }, + { dx: radius, dy: radius }, + { dx: 0, dy: radius }, + { dx: -radius, dy: radius }, + { dx: -radius, dy: 0 }, + { dx: -radius, dy: -radius }, + { dx: 0, dy: -radius }, + { dx: radius, dy: -radius } + ]; + + let step = 0; + for (const off of offsets) { + const targetX = Math.min(Math.max(startX + off.dx, mapBounds.minX), mapBounds.maxX); + const targetY = Math.min(Math.max(startY + off.dy, mapBounds.minY), mapBounds.maxY); + waypoints.push({ x: targetX, y: targetY, step: step++ }); + } + + return waypoints; +}