Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions PR_DESCRIPTION_DRAFT.md
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +1 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Quality: Stray PR_DESCRIPTION_DRAFT.md committed to repo root

PR_DESCRIPTION_DRAFT.md appears to be a working draft accidentally committed, and its content describes a different feature (#6 palette schemas) unrelated to this PR. Remove it from the commit so it doesn't clutter the repository root.

Delete the accidentally committed draft file:

git rm PR_DESCRIPTION_DRAFT.md
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

66 changes: 33 additions & 33 deletions api/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down
69 changes: 69 additions & 0 deletions api/src/jsons/npcs.json
Original file line number Diff line number Diff line change
@@ -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
}
}
21 changes: 21 additions & 0 deletions api/src/lib/layerBlendMode.ts
Original file line number Diff line number Diff line change
@@ -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>): 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));
Comment on lines +14 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Edge Case: NaN opacity/layerIndex bypass clamping

Math.max(0.0, Math.max/floor(NaN)) returns NaN, so a caller passing opacity: NaN (or layerIndex: NaN) produces a config with NaN values, violating the stated [0.0,1.0] normalization guarantee. Add a Number.isFinite fallback, e.g. const opacity = Number.isFinite(config.opacity) ? Math.min(1, Math.max(0, config.opacity!)) : 1.0; (and similarly for layerIndex).

Guard against NaN/Infinity before clamping:

const rawIndex = config.layerIndex;
const layerIndex = Number.isFinite(rawIndex) ? Math.max(0, Math.floor(rawIndex as number)) : 0;
const rawOpacity = config.opacity;
const opacity = Number.isFinite(rawOpacity) ? Math.min(1.0, Math.max(0.0, rawOpacity as number)) : 1.0;
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

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 };
}
46 changes: 46 additions & 0 deletions api/src/repositories/mapNpcPlacement.ts
Original file line number Diff line number Diff line change
@@ -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;
}