feat: add ancient artifacts and refactor item categories

Introduce Ancient Artifacts with full CRUD and image support
Migrate item categories and usages to system-defined lists
Add display_id to items and artifacts for custom sorting
This commit is contained in:
2026-05-04 08:28:56 +08:00
parent 5ccc25b248
commit 4238be7761
25 changed files with 1857 additions and 181 deletions

View File

@@ -30,10 +30,12 @@ CREATE TABLE IF NOT EXISTS entity_translations (
'item-usages',
'acquisition-methods',
'items',
'ancient-artifacts',
'maps',
'habitats',
'daily-checklist-items',
'life-tags'
'life-tags',
'game-versions'
)
),
entity_id integer NOT NULL,
@@ -46,6 +48,30 @@ CREATE TABLE IF NOT EXISTS entity_translations (
CREATE INDEX IF NOT EXISTS entity_translations_lookup_idx
ON entity_translations (entity_type, entity_id, field_name, locale);
ALTER TABLE entity_translations
DROP CONSTRAINT IF EXISTS entity_translations_entity_type_check;
ALTER TABLE entity_translations
ADD CONSTRAINT entity_translations_entity_type_check CHECK (
entity_type IN (
'pokemon',
'pokemon-types',
'skills',
'environments',
'favorite-things',
'item-categories',
'item-usages',
'acquisition-methods',
'items',
'ancient-artifacts',
'maps',
'habitats',
'daily-checklist-items',
'life-tags',
'game-versions'
)
);
CREATE TABLE IF NOT EXISTS users (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
@@ -241,6 +267,11 @@ VALUES
('items.delete', 'Delete items', 'Delete item records.', 'Items', true),
('items.order', 'Order items', 'Reorder item records.', 'Items', true),
('items.upload', 'Upload item images', 'Upload item images.', 'Items', true),
('ancient-artifacts.create', 'Create Ancient Artifacts', 'Create Ancient Artifact records.', 'Ancient Artifacts', true),
('ancient-artifacts.update', 'Update Ancient Artifacts', 'Edit Ancient Artifact records.', 'Ancient Artifacts', true),
('ancient-artifacts.delete', 'Delete Ancient Artifacts', 'Delete Ancient Artifact records.', 'Ancient Artifacts', true),
('ancient-artifacts.order', 'Order Ancient Artifacts', 'Reorder Ancient Artifact records.', 'Ancient Artifacts', true),
('ancient-artifacts.upload', 'Upload Ancient Artifact images', 'Upload Ancient Artifact images.', 'Ancient Artifacts', true),
('recipes.create', 'Create recipes', 'Create recipe records.', 'Recipes', true),
('recipes.update', 'Update recipes', 'Edit recipe records.', 'Recipes', true),
('recipes.delete', 'Delete recipes', 'Delete recipe records.', 'Recipes', true),
@@ -327,6 +358,11 @@ JOIN permissions p ON p.key = ANY (ARRAY[
'items.delete',
'items.order',
'items.upload',
'ancient-artifacts.create',
'ancient-artifacts.update',
'ancient-artifacts.delete',
'ancient-artifacts.order',
'ancient-artifacts.upload',
'recipes.create',
'recipes.update',
'recipes.delete',
@@ -395,6 +431,10 @@ JOIN permissions p ON p.key = ANY (ARRAY[
'items.update',
'items.order',
'items.upload',
'ancient-artifacts.create',
'ancient-artifacts.update',
'ancient-artifacts.order',
'ancient-artifacts.upload',
'recipes.create',
'recipes.update',
'recipes.order',
@@ -416,6 +456,31 @@ WHERE r.key = 'editor'
)
ON CONFLICT DO NOTHING;
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
JOIN permissions p ON p.key = ANY (ARRAY[
'ancient-artifacts.create',
'ancient-artifacts.update',
'ancient-artifacts.delete',
'ancient-artifacts.order',
'ancient-artifacts.upload'
])
WHERE r.key = 'admin'
ON CONFLICT DO NOTHING;
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
JOIN permissions p ON p.key = ANY (ARRAY[
'ancient-artifacts.create',
'ancient-artifacts.update',
'ancient-artifacts.order',
'ancient-artifacts.upload'
])
WHERE r.key = 'editor'
ON CONFLICT DO NOTHING;
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
@@ -798,8 +863,12 @@ CREATE TABLE IF NOT EXISTS acquisition_methods (
CREATE TABLE IF NOT EXISTS items (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
display_id integer NOT NULL CHECK (display_id > 0),
name text NOT NULL UNIQUE,
category_id integer NOT NULL REFERENCES item_categories(id),
details text NOT NULL DEFAULT '',
category_key text NOT NULL DEFAULT 'other',
usage_key text,
category_id integer REFERENCES item_categories(id),
usage_id integer REFERENCES item_usages(id),
dyeable boolean NOT NULL DEFAULT false,
dual_dyeable boolean NOT NULL DEFAULT false,
@@ -811,6 +880,35 @@ CREATE TABLE IF NOT EXISTS items (
created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (category_key IN (
'furniture',
'misc',
'outdoor',
'utilities',
'buildings',
'blocks',
'kits',
'nature',
'food',
'materials',
'key-items',
'other'
)),
CHECK (usage_key IS NULL OR usage_key IN ('decoration', 'relaxation', 'toy', 'road'))
);
CREATE TABLE IF NOT EXISTS ancient_artifacts (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
display_id integer NOT NULL UNIQUE CHECK (display_id > 0),
name text NOT NULL UNIQUE,
details text NOT NULL DEFAULT '',
category_key text NOT NULL CHECK (category_key IN ('lost-relics-l', 'lost-relics-s', 'fossils')),
image_path text NOT NULL DEFAULT '',
sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0),
created_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
updated_by_user_id integer REFERENCES users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
@@ -842,6 +940,12 @@ CREATE TABLE IF NOT EXISTS item_favorite_things (
PRIMARY KEY (item_id, favorite_thing_id)
);
CREATE TABLE IF NOT EXISTS ancient_artifact_favorite_things (
ancient_artifact_id integer NOT NULL REFERENCES ancient_artifacts(id) ON DELETE CASCADE,
favorite_thing_id integer NOT NULL REFERENCES favorite_things(id),
PRIMARY KEY (ancient_artifact_id, favorite_thing_id)
);
CREATE TABLE IF NOT EXISTS pokemon_skill_item_drops (
pokemon_id integer NOT NULL,
skill_id integer NOT NULL,
@@ -899,6 +1003,116 @@ CREATE TABLE IF NOT EXISTS habitat_pokemon (
ALTER TABLE life_tags
ADD COLUMN IF NOT EXISTS is_default boolean NOT NULL DEFAULT false;
ALTER TABLE items
ADD COLUMN IF NOT EXISTS display_id integer,
ADD COLUMN IF NOT EXISTS details text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS category_key text,
ADD COLUMN IF NOT EXISTS usage_key text;
ALTER TABLE ancient_artifacts
ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT '';
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'items'
AND column_name = 'category_id'
AND table_schema = current_schema()
) THEN
ALTER TABLE items ALTER COLUMN category_id DROP NOT NULL;
END IF;
END $$;
UPDATE items
SET display_id = id
WHERE display_id IS NULL;
UPDATE items i
SET category_key = CASE lower(trim(c.name))
WHEN 'furniture' THEN 'furniture'
WHEN 'misc' THEN 'misc'
WHEN 'outdoor' THEN 'outdoor'
WHEN 'utilities' THEN 'utilities'
WHEN 'buildings' THEN 'buildings'
WHEN 'blocks' THEN 'blocks'
WHEN 'kits' THEN 'kits'
WHEN 'nature' THEN 'nature'
WHEN 'food' THEN 'food'
WHEN 'materials' THEN 'materials'
WHEN 'key items' THEN 'key-items'
WHEN 'key-items' THEN 'key-items'
WHEN 'other' THEN 'other'
ELSE 'other'
END
FROM item_categories c
WHERE i.category_id = c.id
AND (i.category_key IS NULL OR i.category_key = '');
UPDATE items i
SET usage_key = CASE lower(trim(u.name))
WHEN 'decoration' THEN 'decoration'
WHEN 'relaxation' THEN 'relaxation'
WHEN 'toy' THEN 'toy'
WHEN 'road' THEN 'road'
ELSE NULL
END
FROM item_usages u
WHERE i.usage_id = u.id
AND i.usage_key IS NULL;
UPDATE items
SET category_key = 'other'
WHERE category_key IS NULL
OR category_key NOT IN (
'furniture',
'misc',
'outdoor',
'utilities',
'buildings',
'blocks',
'kits',
'nature',
'food',
'materials',
'key-items',
'other'
);
UPDATE items
SET usage_key = NULL
WHERE usage_key IS NOT NULL
AND usage_key NOT IN ('decoration', 'relaxation', 'toy', 'road');
ALTER TABLE items
ALTER COLUMN display_id SET NOT NULL,
ALTER COLUMN category_key SET NOT NULL,
ALTER COLUMN category_key SET DEFAULT 'other';
ALTER TABLE items
DROP CONSTRAINT IF EXISTS items_display_id_positive,
DROP CONSTRAINT IF EXISTS items_category_key_check,
DROP CONSTRAINT IF EXISTS items_usage_key_check;
ALTER TABLE items
ADD CONSTRAINT items_display_id_positive CHECK (display_id > 0),
ADD CONSTRAINT items_category_key_check CHECK (category_key IN (
'furniture',
'misc',
'outdoor',
'utilities',
'buildings',
'blocks',
'kits',
'nature',
'food',
'materials',
'key-items',
'other'
)),
ADD CONSTRAINT items_usage_key_check CHECK (usage_key IS NULL OR usage_key IN ('decoration', 'relaxation', 'toy', 'road'));
CREATE INDEX IF NOT EXISTS environments_sort_order_idx ON environments(sort_order, id);
CREATE INDEX IF NOT EXISTS skills_sort_order_idx ON skills(sort_order, id);
CREATE INDEX IF NOT EXISTS favorite_things_sort_order_idx ON favorite_things(sort_order, id);
@@ -911,6 +1125,10 @@ CREATE INDEX IF NOT EXISTS item_categories_sort_order_idx ON item_categories(sor
CREATE INDEX IF NOT EXISTS item_usages_sort_order_idx ON item_usages(sort_order, id);
CREATE INDEX IF NOT EXISTS acquisition_methods_sort_order_idx ON acquisition_methods(sort_order, id);
CREATE INDEX IF NOT EXISTS items_sort_order_idx ON items(sort_order, id);
CREATE UNIQUE INDEX IF NOT EXISTS items_display_event_item_key ON items(display_id, is_event_item);
CREATE INDEX IF NOT EXISTS items_display_order_idx ON items(is_event_item, display_id, sort_order, id);
CREATE INDEX IF NOT EXISTS ancient_artifacts_sort_order_idx ON ancient_artifacts(sort_order, id);
CREATE INDEX IF NOT EXISTS ancient_artifacts_display_order_idx ON ancient_artifacts(display_id, sort_order, id);
CREATE INDEX IF NOT EXISTS recipes_sort_order_idx ON recipes(sort_order, id);
CREATE INDEX IF NOT EXISTS maps_sort_order_idx ON maps(sort_order, id);
CREATE INDEX IF NOT EXISTS habitats_sort_order_idx ON habitats(sort_order, id);
@@ -933,7 +1151,7 @@ CREATE INDEX IF NOT EXISTS wiki_edit_logs_user_id_idx
CREATE TABLE IF NOT EXISTS entity_image_uploads (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
entity_type text NOT NULL CHECK (entity_type IN ('pokemon', 'items', 'habitats')),
entity_type text NOT NULL CHECK (entity_type IN ('pokemon', 'items', 'habitats', 'ancient-artifacts')),
entity_id integer,
entity_name text NOT NULL,
path text NOT NULL UNIQUE,
@@ -946,6 +1164,14 @@ CREATE TABLE IF NOT EXISTS entity_image_uploads (
CHECK (path !~ '(^/|\\.\\.)')
);
ALTER TABLE entity_image_uploads
DROP CONSTRAINT IF EXISTS entity_image_uploads_entity_type_check;
ALTER TABLE entity_image_uploads
ADD CONSTRAINT entity_image_uploads_entity_type_check CHECK (
entity_type IN ('pokemon', 'items', 'habitats', 'ancient-artifacts')
);
CREATE INDEX IF NOT EXISTS entity_image_uploads_entity_idx
ON entity_image_uploads(entity_type, entity_id, created_at DESC, id DESC);
@@ -954,7 +1180,7 @@ CREATE INDEX IF NOT EXISTS entity_image_uploads_user_idx
CREATE TABLE IF NOT EXISTS entity_discussion_comments (
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
entity_type text NOT NULL CHECK (entity_type IN ('pokemon', 'items', 'recipes', 'habitats')),
entity_type text NOT NULL CHECK (entity_type IN ('pokemon', 'items', 'recipes', 'habitats', 'ancient-artifacts')),
entity_id integer NOT NULL,
parent_comment_id integer REFERENCES entity_discussion_comments(id) ON DELETE CASCADE,
body text NOT NULL CHECK (length(body) BETWEEN 1 AND 1000),
@@ -980,6 +1206,14 @@ CREATE INDEX IF NOT EXISTS entity_discussion_comments_parent_idx
CREATE INDEX IF NOT EXISTS entity_discussion_comments_user_idx
ON entity_discussion_comments(created_by_user_id);
ALTER TABLE entity_discussion_comments
DROP CONSTRAINT IF EXISTS entity_discussion_comments_entity_type_check;
ALTER TABLE entity_discussion_comments
ADD CONSTRAINT entity_discussion_comments_entity_type_check CHECK (
entity_type IN ('pokemon', 'items', 'recipes', 'habitats', 'ancient-artifacts')
);
ALTER TABLE life_tags
ADD COLUMN IF NOT EXISTS is_rateable boolean NOT NULL DEFAULT false;