[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7826EFWCPCZBK8GSFJCZSAF":3,"skill-same:01M7826EFWCPCZBK8GSFJCZSAF":56},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":33,"commitSha":34,"skillDirectory":35,"forksCount":36,"repositoryHtmlUrl":37,"repositoryHomepageUrl":38,"ownerHtmlUrl":39,"ownerBlogUrl":6,"ownerDescription":40,"updatedAt":41,"install":42,"resources":55},"01M7826EFWCPCZBK8GSFJCZSAF","acgti-anime-persona-quiz",null,"ACG Type Indicator — MBTI-inspired anime character persona quiz built with Vue 3, TypeScript, and Vite","Lord1Egypt\u002Fawesome-skill-forge","Lord1Egypt","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F99746359?v=4","01M7592YDNT0XK9WJ0T2MQVPDN","Mohamed Mounir Ali AL Keshita","01M75GRFNHC8VZNWEB8DKDE433",3,"MIT","https:\u002F\u002Fgithub.com\u002FLord1Egypt\u002Fawesome-skill-forge\u002Fblob\u002Fmaster\u002Fcommunity\u002Fskills_sh\u002Fs\u002Fskills-sh_aradotso_trending-skills_acgti-anime-persona-quiz\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: acgti-anime-persona-quiz\ndescription: ACG Type Indicator — MBTI-inspired anime character persona quiz built with Vue 3, TypeScript, and Vite\ntriggers:\n  - add a new character to ACGTI\n  - add quiz questions to the anime personality test\n  - how do I extend the ACGTI character database\n  - modify ACGTI archetype definitions\n  - customize the ACGTI quiz scoring engine\n  - deploy ACGTI to Cloudflare Pages\n  - how does ACGTI calculate quiz results\n  - set up ACGTI locally for development\n---\n\n# ACGTI Anime Persona Quiz\n\n> Skill by [ara.so](https:\u002F\u002Fara.so) — Daily 2026 Skills collection.\n\nACGTI (ACG Type Indicator) is a purely client-side Vue 3 + TypeScript quiz that maps 39 seven-point Likert-scale questions onto four MBTI dimensions (E\u002FI, S\u002FN, T\u002FF, J\u002FP), matches the result to one of 8 anime archetypes, and then selects a specific anime character from a 40+ entry database. No backend, no user data collection — everything runs in the browser.\n\n---\n\n## Installation & Local Development\n\n```bash\n# Clone the repo\ngit clone https:\u002F\u002Fgithub.com\u002Ftianxingleo\u002FACGTI.git\ncd ACGTI\n\n# Install dependencies (Node 18+ recommended)\nnpm install\n\n# Start dev server (Vite, hot-reload)\nnpm run dev\n\n# Type-check\nnpx tsc --noEmit\n\n# Production build → dist\u002F\nnpm run build\n\n# Preview production build locally\nnpm run preview\n```\n\nThe `dist\u002F` folder uses `base: '.\u002F'` (relative paths), so it deploys directly to any static host.\n\n---\n\n## Project Architecture\n\n```\nsrc\u002F\n├── components\u002F          # Reusable UI (QuestionCard, ResultSummary, SharePoster …)\n├── composables\u002F\n│   ├── useQuiz.ts       # Quiz state machine & answer logic\n│   └── useShare.ts      # PNG poster export\n├── data\u002F                # ALL content lives here as JSON\n│   ├── questions.json\n│   ├── archetypes.json\n│   ├── characters.json\n│   ├── characterVisuals.json\n│   └── characterProbabilities.json\n├── pages\u002F               # Vue route-level components\n├── types\u002Fquiz.ts        # Shared TypeScript types\n├── utils\u002F\n│   ├── quizEngine.ts    # Score → archetype → character pipeline\n│   ├── characterVisuals.ts\n│   ├── characterProbability.ts\n│   └── storage.ts       # localStorage helpers\n└── router\u002Findex.ts\n```\n\n---\n\n## Core Types (`src\u002Ftypes\u002Fquiz.ts`)\n\nUnderstanding these types is essential before touching any data file or engine logic.\n\n```typescript\n\u002F\u002F MBTI dimension keys\nexport type Dimension = 'EI' | 'SN' | 'TF' | 'JP';\n\n\u002F\u002F One question entry\nexport interface Question {\n  id: number;\n  text: string;\n  dimension: Dimension;\n  archetypeWeights: Record\u003Cstring, number>; \u002F\u002F archetype id → weight (-3..+3)\n  tags?: string[];\n}\n\n\u002F\u002F One of 8 archetypes\nexport interface Archetype {\n  id: string;           \u002F\u002F e.g. \"glowing-protagonist\"\n  name: string;\n  mbtiTypes: string[];  \u002F\u002F e.g. [\"ENFJ\",\"ENFP\"]\n  description: string;\n  strengths: string[];\n  weaknesses: string[];\n  color: string;        \u002F\u002F hex\n}\n\n\u002F\u002F Anime character entry\nexport interface Character {\n  id: string;           \u002F\u002F unique slug, becomes the \"character code\"\n  name: string;\n  series: string;\n  mbtiType: string;     \u002F\u002F e.g. \"ENFJ\"\n  archetypeId: string;\n  tags: string[];\n  stats: {              \u002F\u002F 0–100 six-axis radar\n    energy: number;\n    intuition: number;\n    empathy: number;\n    logic: number;\n    order: number;\n    chaos: number;\n  };\n}\n\n\u002F\u002F Visual theming per character\nexport interface CharacterVisual {\n  characterId: string;\n  portraitUrl: string;\n  backgroundUrl: string;\n  primaryColor: string;\n  accentColor: string;\n}\n\n\u002F\u002F Final computed result passed to ResultPage\nexport interface QuizResult {\n  mbtiType: string;               \u002F\u002F e.g. \"INFP\"\n  dimensionScores: Record\u003CDimension, number>; \u002F\u002F 50–100, direction-normalised\n  archetypeId: string;\n  characterId: string;\n}\n```\n\n---\n\n## Scoring Engine (`src\u002Futils\u002FquizEngine.ts`)\n\nThe engine is a pure function pipeline — ideal extension point.\n\n```typescript\nimport questions from '@\u002Fdata\u002Fquestions.json';\nimport archetypes from '@\u002Fdata\u002Farchetypes.json';\nimport characters from '@\u002Fdata\u002Fcharacters.json';\nimport type { Dimension, QuizResult } from '@\u002Ftypes\u002Fquiz';\n\ntype Answers = Record\u003Cnumber, number>; \u002F\u002F questionId → -3..+3\n\n\u002F** Step 1: Sum raw signed scores per MBTI dimension *\u002F\nfunction calcDimensionRaw(answers: Answers): Record\u003CDimension, number> {\n  const raw: Record\u003CDimension, number> = { EI: 0, SN: 0, TF: 0, JP: 0 };\n  for (const q of questions) {\n    const val = answers[q.id] ?? 0;\n    raw[q.dimension as Dimension] += val;\n  }\n  return raw;\n}\n\n\u002F** Step 2: Normalise to 50–100 (50 = perfectly balanced) *\u002F\nfunction normaliseDimension(raw: number, questionCount: number): number {\n  const max = questionCount * 3;           \u002F\u002F maximum possible absolute value\n  const clamped = Math.max(-max, Math.min(max, raw));\n  return Math.round(50 + (Math.abs(clamped) \u002F max) * 50);\n}\n\n\u002F** Step 3: Derive MBTI letter for one dimension *\u002F\nfunction mbtiLetter(dimension: Dimension, raw: number): string {\n  const positive: Record\u003CDimension, string> = { EI: 'E', SN: 'N', TF: 'T', JP: 'J' };\n  const negative: Record\u003CDimension, string> = { EI: 'I', SN: 'S', TF: 'F', JP: 'P' };\n  return raw >= 0 ? positive[dimension] : negative[dimension];\n}\n\n\u002F** Full pipeline *\u002F\nexport function computeResult(answers: Answers): QuizResult {\n  const dims: Dimension[] = ['EI', 'SN', 'TF', 'JP'];\n  const raw = calcDimensionRaw(answers);\n\n  \u002F\u002F Count questions per dimension for normalisation\n  const countPerDim = dims.reduce((acc, d) => {\n    acc[d] = questions.filter(q => q.dimension === d).length;\n    return acc;\n  }, {} as Record\u003CDimension, number>);\n\n  const dimensionScores = dims.reduce((acc, d) => {\n    acc[d] = normaliseDimension(raw[d], countPerDim[d]);\n    return acc;\n  }, {} as Record\u003CDimension, number>);\n\n  const mbtiType = dims.map(d => mbtiLetter(d, raw[d])).join('');\n\n  \u002F\u002F Match archetype (archetypes list mbtiTypes they cover)\n  const archetype = archetypes.find(a => a.mbtiTypes.includes(mbtiType))\n    ?? archetypes[0];\n\n  \u002F\u002F Pick best-fit character within archetype\n  const candidates = characters.filter(c => c.archetypeId === archetype.id);\n  \u002F\u002F Default: first match; extendable with probability weighting\n  const character = candidates[0];\n\n  return {\n    mbtiType,\n    dimensionScores,\n    archetypeId: archetype.id,\n    characterId: character.id,\n  };\n}\n```\n\n---\n\n## Adding a New Character\n\nEdit `src\u002Fdata\u002Fcharacters.json` — append one object following the schema:\n\n```json\n{\n  \"id\": \"hatsune-miku\",\n  \"name\": \"初音ミク\",\n  \"series\": \"VOCALOID\",\n  \"mbtiType\": \"ENFP\",\n  \"archetypeId\": \"chaotic-spark\",\n  \"tags\": [\"vocaloid\", \"energetic\", \"creative\"],\n  \"stats\": {\n    \"energy\": 90,\n    \"intuition\": 85,\n    \"empathy\": 75,\n    \"logic\": 50,\n    \"order\": 40,\n    \"chaos\": 80\n  }\n}\n```\n\nThen add the matching visual entry to `src\u002Fdata\u002FcharacterVisuals.json`:\n\n```json\n{\n  \"characterId\": \"hatsune-miku\",\n  \"portraitUrl\": \"https:\u002F\u002Fyour-cdn.example.com\u002Fmiku-portrait.webp\",\n  \"backgroundUrl\": \"https:\u002F\u002Fyour-cdn.example.com\u002Fmiku-bg.webp\",\n  \"primaryColor\": \"#39C5BB\",\n  \"accentColor\": \"#86EFDF\"\n}\n```\n\nAnd an optional prior probability in `src\u002Fdata\u002FcharacterProbabilities.json`:\n\n```json\n{\n  \"characterId\": \"hatsune-miku\",\n  \"baseProbability\": 0.15\n}\n```\n\n> **Rules:**  \n> • `id` must be unique and kebab-case.  \n> • `mbtiType` must be one of the 16 standard types.  \n> • `archetypeId` must match an `id` in `archetypes.json`.  \n> • `stats` values are integers 0–100.\n\n---\n\n## Adding New Quiz Questions\n\nEdit `src\u002Fdata\u002Fquestions.json` — append to the array:\n\n```json\n{\n  \"id\": 40,\n  \"text\": \"在一个陌生的聚会上，你更倾向于主动找人搭话还是等别人来找你？\",\n  \"dimension\": \"EI\",\n  \"archetypeWeights\": {\n    \"glowing-protagonist\": 2,\n    \"ice-observer\": -2,\n    \"oath-captain\": 1,\n    \"agile-spinner\": 1,\n    \"gentle-healer\": 0,\n    \"shadow-strategist\": -1,\n    \"chaotic-spark\": 2,\n    \"moonlit-guardian\": -1\n  },\n  \"tags\": [\"social\", \"introvert-extrovert\"]\n}\n```\n\n**Guidelines:**\n- `id` must be unique and increment sequentially.\n- `dimension` is one of `\"EI\" | \"SN\" | \"TF\" | \"JP\"`.\n- `archetypeWeights` keys must match all 8 archetype `id` values; weights range **-3 to +3**.\n- Positive weight = answer \"strongly agree\" nudges toward that archetype.\n- Keep question text in Chinese (Simplified) to match existing copy.\n\n---\n\n## Modifying Archetypes (`src\u002Fdata\u002Farchetypes.json`)\n\n```json\n{\n  \"id\": \"glowing-protagonist\",\n  \"name\": \"发光主角位\",\n  \"mbtiTypes\": [\"ENFJ\", \"ENFP\"],\n  \"description\": \"天生的领袖与感召者，能点燃周围人的热情。\",\n  \"strengths\": [\"感召力强\", \"共情深刻\", \"行动力高\"],\n  \"weaknesses\": [\"容易过度承担\", \"情绪波动大\"],\n  \"color\": \"#FF6B6B\"\n}\n```\n\n> Each MBTI type (16 total) should appear in **exactly one** archetype's `mbtiTypes` array. The engine uses a first-match lookup — gaps cause a fallback to `archetypes[0]`.\n\n---\n\n## `useQuiz` Composable (state management)\n\n```typescript\n\u002F\u002F src\u002Fcomposables\u002FuseQuiz.ts — typical usage from a page component\nimport { useQuiz } from '@\u002Fcomposables\u002FuseQuiz';\n\nconst {\n  currentQuestion,   \u002F\u002F Ref\u003CQuestion>\n  currentIndex,      \u002F\u002F Ref\u003Cnumber>\n  totalQuestions,    \u002F\u002F number (39)\n  progress,          \u002F\u002F ComputedRef\u003Cnumber> 0–100\n  answer,            \u002F\u002F (value: number) => void  — records -3..+3 and advances\n  goBack,            \u002F\u002F () => void\n  result,            \u002F\u002F Ref\u003CQuizResult | null>\n  isComplete,        \u002F\u002F ComputedRef\u003Cboolean>\n  resetQuiz,         \u002F\u002F () => void\n} = useQuiz();\n```\n\n---\n\n## Share \u002F Export Poster (`useShare`)\n\n```typescript\nimport { useShare } from '@\u002Fcomposables\u002FuseShare';\n\nconst { exportPNG, shareNative } = useShare();\n\n\u002F\u002F exportPNG wraps html2canvas on the #share-poster element\nawait exportPNG('#share-poster', 'my-acgti-result.png');\n\n\u002F\u002F shareNative uses Web Share API with fallback to clipboard copy\nawait shareNative({\n  title: 'My ACGTI Result',\n  text: `I got ${result.value?.characterId}!`,\n  url: 'https:\u002F\u002Facgti.tianxingleo.top',\n});\n```\n\n---\n\n## Routing (`src\u002Frouter\u002Findex.ts`)\n\n```typescript\n\u002F\u002F Five named routes\nconst routes = [\n  { path: '\u002F',          name: 'home',       component: HomePage },\n  { path: '\u002Fintro',     name: 'intro',      component: IntroPage },\n  { path: '\u002Fquiz',      name: 'quiz',       component: QuizPage },\n  { path: '\u002Fresult',    name: 'result',     component: ResultPage },\n  { path: '\u002Fcharacters',name: 'characters', component: CharactersPage },\n  { path: '\u002Fabout',     name: 'about',      component: AboutPage },\n];\n```\n\nNavigate programmatically after quiz completion:\n\n```typescript\nimport { useRouter } from 'vue-router';\nconst router = useRouter();\nrouter.push({ name: 'result' });\n```\n\n---\n\n## localStorage Utilities (`src\u002Futils\u002Fstorage.ts`)\n\n```typescript\nimport { saveResult, loadResult, clearResult } from '@\u002Futils\u002Fstorage';\nimport type { QuizResult } from '@\u002Ftypes\u002Fquiz';\n\n\u002F\u002F Persist result across page refreshes\nsaveResult(result);\n\n\u002F\u002F Restore on ResultPage mount\nconst saved: QuizResult | null = loadResult();\n\n\u002F\u002F Reset for retake\nclearResult();\n```\n\n---\n\n## Deployment\n\n### Cloudflare Pages (recommended)\n\n1. Connect GitHub repo → Cloudflare Pages dashboard.\n2. Build command: `npm run build`\n3. Build output directory: `dist`\n4. No environment variables required (pure frontend).\n\n### GitHub Actions CI\n\nThe repo includes a workflow that runs on every push to `main`\u002F`dev` and on PRs:\n\n```yaml\n# .github\u002Fworkflows\u002Fci.yml (existing)\n- run: npm ci\n- run: npm run build\n```\n\n### Release a version\n\n```bash\ngit tag v1.2.0\ngit push origin v1.2.0\n# GitHub Actions auto-builds dist\u002F, zips it, creates a Release\n```\n\n---\n\n## Common Patterns & Tips\n\n### Filtering characters by archetype in a component\n\n```typescript\nimport characters from '@\u002Fdata\u002Fcharacters.json';\nimport type { Character } from '@\u002Ftypes\u002Fquiz';\n\nconst archetypeId = 'glowing-protagonist';\nconst subset: Character[] = characters.filter(\n  (c) => c.archetypeId === archetypeId\n);\n```\n\n### Accessing visuals by character ID\n\n```typescript\nimport visuals from '@\u002Fdata\u002FcharacterVisuals.json';\nimport { enrichCharacterVisuals } from '@\u002Futils\u002FcharacterVisuals';\n\nconst enriched = enrichCharacterVisuals(characters, visuals);\n\u002F\u002F enriched[i] = { ...Character, ...CharacterVisual }\n```\n\n### Reactive dimension label (E vs I, etc.)\n\n```typescript\nfunction dimensionLabel(dim: Dimension, score: number): string {\n  const labels: Record\u003CDimension, [string, string]> = {\n    EI: ['E 外向', 'I 内向'],\n    SN: ['N 直觉', 'S 实感'],\n    TF: ['T 思考', 'F 情感'],\n    JP: ['J 判断', 'P 知觉'],\n  };\n  \u002F\u002F score > 50 means positive pole; score === 50 means balanced (show both)\n  return score >= 50 ? labels[dim][0] : labels[dim][1];\n}\n```\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| `npm run build` fails with type errors | New JSON data doesn't match types | Run `npx tsc --noEmit` and fix mismatches in `src\u002Ftypes\u002Fquiz.ts` |\n| Character not appearing in results | `archetypeId` mismatch between `characters.json` and `archetypes.json` | Ensure `archetypeId` exactly matches an archetype `id` |\n| New question not affecting scores | `dimension` key is wrong | Must be exactly `\"EI\"`, `\"SN\"`, `\"TF\"`, or `\"JP\"` |\n| Poster export is blank | `html2canvas` can't load cross-origin images | Host character images on a CORS-enabled CDN or use base64 data URIs |\n| Route returns 404 on Cloudflare Pages | SPA fallback not configured | Add `_redirects` file: `\u002F* \u002Findex.html 200` in `public\u002F` |\n| Dev server errors on `@\u002F` imports | Vite alias not resolving | Check `vite.config.ts` has `resolve: { alias: { '@': '\u002Fsrc' } }` |\n","# ACGTI Anime Persona Quiz\n\n> Skill by [ara.so](https:\u002F\u002Fara.so) — Daily 2026 Skills collection.\n\nACGTI (ACG Type Indicator) is a purely client-side Vue 3 + TypeScript quiz that maps 39 seven-point Likert-scale questions onto four MBTI dimensions (E\u002FI, S\u002FN, T\u002FF, J\u002FP), matches the result to one of 8 anime archetypes, and then selects a specific anime character from a 40+ entry database. No backend, no user data collection — everything runs in the browser.\n\n---\n\n## Installation & Local Development\n\n```bash\n# Clone the repo\ngit clone https:\u002F\u002Fgithub.com\u002Ftianxingleo\u002FACGTI.git\ncd ACGTI\n\n# Install dependencies (Node 18+ recommended)\nnpm install\n\n# Start dev server (Vite, hot-reload)\nnpm run dev\n\n# Type-check\nnpx tsc --noEmit\n\n# Production build → dist\u002F\nnpm run build\n\n# Preview production build locally\nnpm run preview\n```\n\nThe `dist\u002F` folder uses `base: '.\u002F'` (relative paths), so it deploys directly to any static host.\n\n---\n\n## Project Architecture\n\n```\nsrc\u002F\n├── components\u002F          # Reusable UI (QuestionCard, ResultSummary, SharePoster …)\n├── composables\u002F\n│   ├── useQuiz.ts       # Quiz state machine & answer logic\n│   └── useShare.ts      # PNG poster export\n├── data\u002F                # ALL content lives here as JSON\n│   ├── questions.json\n│   ├── archetypes.json\n│   ├── characters.json\n│   ├── characterVisuals.json\n│   └── characterProbabilities.json\n├── pages\u002F               # Vue route-level components\n├── types\u002Fquiz.ts        # Shared TypeScript types\n├── utils\u002F\n│   ├── quizEngine.ts    # Score → archetype → character pipeline\n│   ├── characterVisuals.ts\n│   ├── characterProbability.ts\n│   └── storage.ts       # localStorage helpers\n└── router\u002Findex.ts\n```\n\n---\n\n## Core Types (`src\u002Ftypes\u002Fquiz.ts`)\n\nUnderstanding these types is essential before touching any data file or engine logic.\n\n```typescript\n\u002F\u002F MBTI dimension keys\nexport type Dimension = 'EI' | 'SN' | 'TF' | 'JP';\n\n\u002F\u002F One question entry\nexport interface Question {\n  id: number;\n  text: string;\n  dimension: Dimension;\n  archetypeWeights: Record\u003Cstring, number>; \u002F\u002F archetype id → weight (-3..+3)\n  tags?: string[];\n}\n\n\u002F\u002F One of 8 archetypes\nexport interface Archetype {\n  id: string;           \u002F\u002F e.g. \"glowing-protagonist\"\n  name: string;\n  mbtiTypes: string[];  \u002F\u002F e.g. [\"ENFJ\",\"ENFP\"]\n  description: string;\n  strengths: string[];\n  weaknesses: string[];\n  color: string;        \u002F\u002F hex\n}\n\n\u002F\u002F Anime character entry\nexport interface Character {\n  id: string;           \u002F\u002F unique slug, becomes the \"character code\"\n  name: string;\n  series: string;\n  mbtiType: string;     \u002F\u002F e.g. \"ENFJ\"\n  archetypeId: string;\n  tags: string[];\n  stats: {              \u002F\u002F 0–100 six-axis radar\n    energy: number;\n    intuition: number;\n    empathy: number;\n    logic: number;\n    order: number;\n    chaos: number;\n  };\n}\n\n\u002F\u002F Visual theming per character\nexport interface CharacterVisual {\n  characterId: string;\n  portraitUrl: string;\n  backgroundUrl: string;\n  primaryColor: string;\n  accentColor: string;\n}\n\n\u002F\u002F Final computed result passed to ResultPage\nexport interface QuizResult {\n  mbtiType: string;               \u002F\u002F e.g. \"INFP\"\n  dimensionScores: Record\u003CDimension, number>; \u002F\u002F 50–100, direction-normalised\n  archetypeId: string;\n  characterId: string;\n}\n```\n\n---\n\n## Scoring Engine (`src\u002Futils\u002FquizEngine.ts`)\n\nThe engine is a pure function pipeline — ideal extension point.\n\n```typescript\nimport questions from '@\u002Fdata\u002Fquestions.json';\nimport archetypes from '@\u002Fdata\u002Farchetypes.json';\nimport characters from '@\u002Fdata\u002Fcharacters.json';\nimport type { Dimension, QuizResult } from '@\u002Ftypes\u002Fquiz';\n\ntype Answers = Record\u003Cnumber, number>; \u002F\u002F questionId → -3..+3\n\n\u002F** Step 1: Sum raw signed scores per MBTI dimension *\u002F\nfunction calcDimensionRaw(answers: Answers): Record\u003CDimension, number> {\n  const raw: Record\u003CDimension, number> = { EI: 0, SN: 0, TF: 0, JP: 0 };\n  for (const q of questions) {\n    const val = answers[q.id] ?? 0;\n    raw[q.dimension as Dimension] += val;\n  }\n  return raw;\n}\n\n\u002F** Step 2: Normalise to 50–100 (50 = perfectly balanced) *\u002F\nfunction normaliseDimension(raw: number, questionCount: number): number {\n  const max = questionCount * 3;           \u002F\u002F maximum possible absolute value\n  const clamped = Math.max(-max, Math.min(max, raw));\n  return Math.round(50 + (Math.abs(clamped) \u002F max) * 50);\n}\n\n\u002F** Step 3: Derive MBTI letter for one dimension *\u002F\nfunction mbtiLetter(dimension: Dimension, raw: number): string {\n  const positive: Record\u003CDimension, string> = { EI: 'E', SN: 'N', TF: 'T', JP: 'J' };\n  const negative: Record\u003CDimension, string> = { EI: 'I', SN: 'S', TF: 'F', JP: 'P' };\n  return raw >= 0 ? positive[dimension] : negative[dimension];\n}\n\n\u002F** Full pipeline *\u002F\nexport function computeResult(answers: Answers): QuizResult {\n  const dims: Dimension[] = ['EI', 'SN', 'TF', 'JP'];\n  const raw = calcDimensionRaw(answers);\n\n  \u002F\u002F Count questions per dimension for normalisation\n  const countPerDim = dims.reduce((acc, d) => {\n    acc[d] = questions.filter(q => q.dimension === d).length;\n    return acc;\n  }, {} as Record\u003CDimension, number>);\n\n  const dimensionScores = dims.reduce((acc, d) => {\n    acc[d] = normaliseDimension(raw[d], countPerDim[d]);\n    return acc;\n  }, {} as Record\u003CDimension, number>);\n\n  const mbtiType = dims.map(d => mbtiLetter(d, raw[d])).join('');\n\n  \u002F\u002F Match archetype (archetypes list mbtiTypes they cover)\n  const archetype = archetypes.find(a => a.mbtiTypes.includes(mbtiType))\n    ?? archetypes[0];\n\n  \u002F\u002F Pick best-fit character within archetype\n  const candidates = characters.filter(c => c.archetypeId === archetype.id);\n  \u002F\u002F Default: first match; extendable with probability weighting\n  const character = candidates[0];\n\n  return {\n    mbtiType,\n    dimensionScores,\n    archetypeId: archetype.id,\n    characterId: character.id,\n  };\n}\n```\n\n---\n\n## Adding a New Character\n\nEdit `src\u002Fdata\u002Fcharacters.json` — append one object following the schema:\n\n```json\n{\n  \"id\": \"hatsune-miku\",\n  \"name\": \"初音ミク\",\n  \"series\": \"VOCALOID\",\n  \"mbtiType\": \"ENFP\",\n  \"archetypeId\": \"chaotic-spark\",\n  \"tags\": [\"vocaloid\", \"energetic\", \"creative\"],\n  \"stats\": {\n    \"energy\": 90,\n    \"intuition\": 85,\n    \"empathy\": 75,\n    \"logic\": 50,\n    \"order\": 40,\n    \"chaos\": 80\n  }\n}\n```\n\nThen add the matching visual entry to `src\u002Fdata\u002FcharacterVisuals.json`:\n\n```json\n{\n  \"characterId\": \"hatsune-miku\",\n  \"portraitUrl\": \"https:\u002F\u002Fyour-cdn.example.com\u002Fmiku-portrait.webp\",\n  \"backgroundUrl\": \"https:\u002F\u002Fyour-cdn.example.com\u002Fmiku-bg.webp\",\n  \"primaryColor\": \"#39C5BB\",\n  \"accentColor\": \"#86EFDF\"\n}\n```\n\nAnd an optional prior probability in `src\u002Fdata\u002FcharacterProbabilities.json`:\n\n```json\n{\n  \"characterId\": \"hatsune-miku\",\n  \"baseProbability\": 0.15\n}\n```\n\n> **Rules:**  \n> • `id` must be unique and kebab-case.  \n> • `mbtiType` must be one of the 16 standard types.  \n> • `archetypeId` must match an `id` in `archetypes.json`.  \n> • `stats` values are integers 0–100.\n\n---\n\n## Adding New Quiz Questions\n\nEdit `src\u002Fdata\u002Fquestions.json` — append to the array:\n\n```json\n{\n  \"id\": 40,\n  \"text\": \"在一个陌生的聚会上，你更倾向于主动找人搭话还是等别人来找你？\",\n  \"dimension\": \"EI\",\n  \"archetypeWeights\": {\n    \"glowing-protagonist\": 2,\n    \"ice-observer\": -2,\n    \"oath-captain\": 1,\n    \"agile-spinner\": 1,\n    \"gentle-healer\": 0,\n    \"shadow-strategist\": -1,\n    \"chaotic-spark\": 2,\n    \"moonlit-guardian\": -1\n  },\n  \"tags\": [\"social\", \"introvert-extrovert\"]\n}\n```\n\n**Guidelines:**\n- `id` must be unique and increment sequentially.\n- `dimension` is one of `\"EI\" | \"SN\" | \"TF\" | \"JP\"`.\n- `archetypeWeights` keys must match all 8 archetype `id` values; weights range **-3 to +3**.\n- Positive weight = answer \"strongly agree\" nudges toward that archetype.\n- Keep question text in Chinese (Simplified) to match existing copy.\n\n---\n\n## Modifying Archetypes (`src\u002Fdata\u002Farchetypes.json`)\n\n```json\n{\n  \"id\": \"glowing-protagonist\",\n  \"name\": \"发光主角位\",\n  \"mbtiTypes\": [\"ENFJ\", \"ENFP\"],\n  \"description\": \"天生的领袖与感召者，能点燃周围人的热情。\",\n  \"strengths\": [\"感召力强\", \"共情深刻\", \"行动力高\"],\n  \"weaknesses\": [\"容易过度承担\", \"情绪波动大\"],\n  \"color\": \"#FF6B6B\"\n}\n```\n\n> Each MBTI type (16 total) should appear in **exactly one** archetype's `mbtiTypes` array. The engine uses a first-match lookup — gaps cause a fallback to `archetypes[0]`.\n\n---\n\n## `useQuiz` Composable (state management)\n\n```typescript\n\u002F\u002F src\u002Fcomposables\u002FuseQuiz.ts — typical usage from a page component\nimport { useQuiz } from '@\u002Fcomposables\u002FuseQuiz';\n\nconst {\n  currentQuestion,   \u002F\u002F Ref\u003CQuestion>\n  currentIndex,      \u002F\u002F Ref\u003Cnumber>\n  totalQuestions,    \u002F\u002F number (39)\n  progress,          \u002F\u002F ComputedRef\u003Cnumber> 0–100\n  answer,            \u002F\u002F (value: number) => void  — records -3..+3 and advances\n  goBack,            \u002F\u002F () => void\n  result,            \u002F\u002F Ref\u003CQuizResult | null>\n  isComplete,        \u002F\u002F ComputedRef\u003Cboolean>\n  resetQuiz,         \u002F\u002F () => void\n} = useQuiz();\n```\n\n---\n\n## Share \u002F Export Poster (`useShare`)\n\n```typescript\nimport { useShare } from '@\u002Fcomposables\u002FuseShare';\n\nconst { exportPNG, shareNative } = useShare();\n\n\u002F\u002F exportPNG wraps html2canvas on the #share-poster element\nawait exportPNG('#share-poster', 'my-acgti-result.png');\n\n\u002F\u002F shareNative uses Web Share API with fallback to clipboard copy\nawait shareNative({\n  title: 'My ACGTI Result',\n  text: `I got ${result.value?.characterId}!`,\n  url: 'https:\u002F\u002Facgti.tianxingleo.top',\n});\n```\n\n---\n\n## Routing (`src\u002Frouter\u002Findex.ts`)\n\n```typescript\n\u002F\u002F Five named routes\nconst routes = [\n  { path: '\u002F',          name: 'home',       component: HomePage },\n  { path: '\u002Fintro',     name: 'intro',      component: IntroPage },\n  { path: '\u002Fquiz',      name: 'quiz',       component: QuizPage },\n  { path: '\u002Fresult',    name: 'result',     component: ResultPage },\n  { path: '\u002Fcharacters',name: 'characters', component: CharactersPage },\n  { path: '\u002Fabout',     name: 'about',      component: AboutPage },\n];\n```\n\nNavigate programmatically after quiz completion:\n\n```typescript\nimport { useRouter } from 'vue-router';\nconst router = useRouter();\nrouter.push({ name: 'result' });\n```\n\n---\n\n## localStorage Utilities (`src\u002Futils\u002Fstorage.ts`)\n\n```typescript\nimport { saveResult, loadResult, clearResult } from '@\u002Futils\u002Fstorage';\nimport type { QuizResult } from '@\u002Ftypes\u002Fquiz';\n\n\u002F\u002F Persist result across page refreshes\nsaveResult(result);\n\n\u002F\u002F Restore on ResultPage mount\nconst saved: QuizResult | null = loadResult();\n\n\u002F\u002F Reset for retake\nclearResult();\n```\n\n---\n\n## Deployment\n\n### Cloudflare Pages (recommended)\n\n1. Connect GitHub repo → Cloudflare Pages dashboard.\n2. Build command: `npm run build`\n3. Build output directory: `dist`\n4. No environment variables required (pure frontend).\n\n### GitHub Actions CI\n\nThe repo includes a workflow that runs on every push to `main`\u002F`dev` and on PRs:\n\n```yaml\n# .github\u002Fworkflows\u002Fci.yml (existing)\n- run: npm ci\n- run: npm run build\n```\n\n### Release a version\n\n```bash\ngit tag v1.2.0\ngit push origin v1.2.0\n# GitHub Actions auto-builds dist\u002F, zips it, creates a Release\n```\n\n---\n\n## Common Patterns & Tips\n\n### Filtering characters by archetype in a component\n\n```typescript\nimport characters from '@\u002Fdata\u002Fcharacters.json';\nimport type { Character } from '@\u002Ftypes\u002Fquiz';\n\nconst archetypeId = 'glowing-protagonist';\nconst subset: Character[] = characters.filter(\n  (c) => c.archetypeId === archetypeId\n);\n```\n\n### Accessing visuals by character ID\n\n```typescript\nimport visuals from '@\u002Fdata\u002FcharacterVisuals.json';\nimport { enrichCharacterVisuals } from '@\u002Futils\u002FcharacterVisuals';\n\nconst enriched = enrichCharacterVisuals(characters, visuals);\n\u002F\u002F enriched[i] = { ...Character, ...CharacterVisual }\n```\n\n### Reactive dimension label (E vs I, etc.)\n\n```typescript\nfunction dimensionLabel(dim: Dimension, score: number): string {\n  const labels: Record\u003CDimension, [string, string]> = {\n    EI: ['E 外向', 'I 内向'],\n    SN: ['N 直觉', 'S 实感'],\n    TF: ['T 思考', 'F 情感'],\n    JP: ['J 判断', 'P 知觉'],\n  };\n  \u002F\u002F score > 50 means positive pole; score === 50 means balanced (show both)\n  return score >= 50 ? labels[dim][0] : labels[dim][1];\n}\n```\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| `npm run build` fails with type errors | New JSON data doesn't match types | Run `npx tsc --noEmit` and fix mismatches in `src\u002Ftypes\u002Fquiz.ts` |\n| Character not appearing in results | `archetypeId` mismatch between `characters.json` and `archetypes.json` | Ensure `archetypeId` exactly matches an archetype `id` |\n| New question not affecting scores | `dimension` key is wrong | Must be exactly `\"EI\"`, `\"SN\"`, `\"TF\"`, or `\"JP\"` |\n| Poster export is blank | `html2canvas` can't load cross-origin images | Host character images on a CORS-enabled CDN or use base64 data URIs |\n| Route returns 404 on Cloudflare Pages | SPA fallback not configured | Add `_redirects` file: `\u002F* \u002Findex.html 200` in `public\u002F` |\n| Dev server errors on `@\u002F` imports | Vite alias not resolving | Check `vite.config.ts` has `resolve: { alias: { '@': '\u002Fsrc' } }` |\n",{"name":5,"triggers":24,"description":7},[25,26,27,28,29,30,31,32],"add a new character to ACGTI","add quiz questions to the anime personality test","how do I extend the ACGTI character database","modify ACGTI archetype definitions","customize the ACGTI quiz scoring engine","deploy ACGTI to Cloudflare Pages","how does ACGTI calculate quiz results","set up ACGTI locally for development","master","4aa004da461d820f4ea5499e58eb8d196cf99016","community\u002Fskills_sh\u002Fs\u002Fskills-sh_aradotso_trending-skills_acgti-anime-persona-quiz",0,"https:\u002F\u002Fgithub.com\u002FLord1Egypt\u002Fawesome-skill-forge","https:\u002F\u002Fskill-explorer-rho.vercel.app","https:\u002F\u002Fgithub.com\u002FLord1Egypt","Unbreakable. Unstoppable. Unmatched⚡.\r\n Speaker of machine tongues 🤖  | World-builder of decentralized dreams 🌐 | Defender of the digital realm🛡️.","2026-06-10 08:21:01",{"source":8,"skillName":5,"commands":43,"prompt":53,"zipAvailable":54},[44,47,50],{"tool":45,"command":46},"npx","npx skills add Lord1Egypt\u002Fawesome-skill-forge --skill acgti-anime-persona-quiz",{"tool":48,"command":49},"bunx","bunx skills add Lord1Egypt\u002Fawesome-skill-forge --skill acgti-anime-persona-quiz",{"tool":51,"command":52},"pnpm","pnpm dlx skills add Lord1Egypt\u002Fawesome-skill-forge --skill acgti-anime-persona-quiz","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FLord1Egypt\u002Fawesome-skill-forge\u002Fblob\u002Fmaster\u002Fcommunity\u002Fskills_sh\u002Fs\u002Fskills-sh_aradotso_trending-skills_acgti-anime-persona-quiz\u002FSKILL.md",true,[],[57,64,71,78,85,92],{"skillNo":58,"name":59,"nameZh":6,"description":60,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":61,"categories":62,"keywords":63,"publishedAt":19},"01M785WKJB99G3STR1NF9NRTJW","youtube-content","YouTube transcripts to summaries, threads, blogs.","https:\u002F\u002Fgithub.com\u002FLord1Egypt\u002Fawesome-skill-forge\u002Fblob\u002Fmaster\u002Fskills\u002Fyoutube-content\u002FSKILL.md",[],[],{"skillNo":65,"name":66,"nameZh":6,"description":67,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":68,"categories":69,"keywords":70,"publishedAt":19},"01M785WEFHB0G2S9YA6EPR3H0E","xurl","X\u002FTwitter via xurl CLI: post, search, DM, media, v2 API.","https:\u002F\u002Fgithub.com\u002FLord1Egypt\u002Fawesome-skill-forge\u002Fblob\u002Fmaster\u002Fskills\u002Fxurl\u002FSKILL.md",[],[],{"skillNo":72,"name":73,"nameZh":6,"description":74,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":75,"categories":76,"keywords":77,"publishedAt":19},"01M785W98EDN4EFJXJPMQEHCRC","touchdesigner-mcp","Control a running TouchDesigner instance via twozero MCP — create operators, set parameters, wire connections, execute Python, build real-time visuals. 36 native tools.","https:\u002F\u002Fgithub.com\u002FLord1Egypt\u002Fawesome-skill-forge\u002Fblob\u002Fmaster\u002Fskills\u002Ftouchdesigner-mcp\u002FSKILL.md",[],[],{"skillNo":79,"name":80,"nameZh":6,"description":81,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":82,"categories":83,"keywords":84,"publishedAt":19},"01M785W5S9FP3QBH2V191E025P","test-driven-development","TDD: enforce RED-GREEN-REFACTOR, tests before code.","https:\u002F\u002Fgithub.com\u002FLord1Egypt\u002Fawesome-skill-forge\u002Fblob\u002Fmaster\u002Fskills\u002Ftest-driven-development\u002FSKILL.md",[],[],{"skillNo":86,"name":87,"nameZh":6,"description":88,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":89,"categories":90,"keywords":91,"publishedAt":19},"01M785W2AS08NSSH9YATCV6FJB","teams-meeting-pipeline","Operate the Teams meeting summary pipeline via Hermes CLI — summarize meetings, inspect pipeline status, replay jobs, manage Microsoft Graph subscriptions.","https:\u002F\u002Fgithub.com\u002FLord1Egypt\u002Fawesome-skill-forge\u002Fblob\u002Fmaster\u002Fskills\u002Fteams-meeting-pipeline\u002FSKILL.md",[],[],{"skillNo":93,"name":94,"nameZh":6,"description":95,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":98,"publishedAt":19},"01M785VXKW4T34BTP1SCHW1R7F","systematic-debugging","4-phase root cause debugging: understand bugs before fixing.","https:\u002F\u002Fgithub.com\u002FLord1Egypt\u002Fawesome-skill-forge\u002Fblob\u002Fmaster\u002Fskills\u002Fsystematic-debugging\u002FSKILL.md",[],[]]