| name | acgti-anime-persona-quiz |
| description | ACG Type Indicator — MBTI-inspired anime character persona quiz built with Vue 3, TypeScript, and Vite |
| triggers | ["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"] |
ACGTI Anime Persona Quiz
Skill by ara.so — Daily 2026 Skills collection.
ACGTI (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/I, S/N, T/F, J/P), 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.
Installation & Local Development
git clone https://github.com/tianxingleo/ACGTI.git
cd ACGTI
npm install
npm run dev
npx tsc --noEmit
npm run build
npm run preview
The dist/ folder uses base: './' (relative paths), so it deploys directly to any static host.
Project Architecture
src/
├── components/ # Reusable UI (QuestionCard, ResultSummary, SharePoster …)
├── composables/
│ ├── useQuiz.ts # Quiz state machine & answer logic
│ └── useShare.ts # PNG poster export
├── data/ # ALL content lives here as JSON
│ ├── questions.json
│ ├── archetypes.json
│ ├── characters.json
│ ├── characterVisuals.json
│ └── characterProbabilities.json
├── pages/ # Vue route-level components
├── types/quiz.ts # Shared TypeScript types
├── utils/
│ ├── quizEngine.ts # Score → archetype → character pipeline
│ ├── characterVisuals.ts
│ ├── characterProbability.ts
│ └── storage.ts # localStorage helpers
└── router/index.ts
Core Types (src/types/quiz.ts)
Understanding these types is essential before touching any data file or engine logic.
export type Dimension = 'EI' | 'SN' | 'TF' | 'JP';
export interface Question {
id: number;
text: string;
dimension: Dimension;
archetypeWeights: Record<string, number>;
tags?: string[];
}
export interface Archetype {
id: string;
name: string;
mbtiTypes: string[];
description: string;
strengths: string[];
weaknesses: string[];
color: string;
}
export interface Character {
id: string;
: ;
: ;
: ;
: ;
: [];
: {
: ;
: ;
: ;
: ;
: ;
: ;
};
}
{
: ;
: ;
: ;
: ;
: ;
}
{
: ;
: <, >;
: ;
: ;
}
Scoring Engine (src/utils/quizEngine.ts)
The engine is a pure function pipeline — ideal extension point.
import questions from '@/data/questions.json';
import archetypes from '@/data/archetypes.json';
import characters from '@/data/characters.json';
import type { Dimension, QuizResult } from '@/types/quiz';
type Answers = Record<number, number>;
function calcDimensionRaw(answers: Answers): Record<Dimension, number> {
const raw: Record<Dimension, number> = { EI: 0, SN: 0, TF: 0, JP: 0 };
for (const q of questions) {
const val = answers[q.id] ?? 0;
raw[q.dimension as Dimension] += val;
}
return raw;
}
(): {
max = questionCount * ;
clamped = .(-max, .(max, raw));
.( + (.(clamped) / max) * );
}
(): {
: <, > = { : , : , : , : };
: <, > = { : , : , : , : };
raw >= ? positive[dimension] : negative[dimension];
}
(): {
: [] = [, , , ];
raw = (answers);
countPerDim = dims.( {
acc[d] = questions.( q. === d).;
acc;
}, {} <, >);
dimensionScores = dims.( {
acc[d] = (raw[d], countPerDim[d]);
acc;
}, {} <, >);
mbtiType = dims.( (d, raw[d])).();
archetype = archetypes.( a..(mbtiType))
?? archetypes[];
candidates = characters.( c. === archetype.);
character = candidates[];
{
mbtiType,
dimensionScores,
: archetype.,
: character.,
};
}
Adding a New Character
Edit src/data/characters.json — append one object following the schema:
{
"id": "hatsune-miku",
"name": "初音ミク",
"series": "VOCALOID",
"mbtiType": "ENFP",
"archetypeId": "chaotic-spark",
"tags": ["vocaloid", "energetic", "creative"],
"stats": {
"energy": 90,
"intuition": 85,
"empathy": 75,
"logic": 50,
"order": 40,
"chaos": 80
}
}
Then add the matching visual entry to src/data/characterVisuals.json:
{
"characterId": "hatsune-miku",
"portraitUrl": "https://your-cdn.example.com/miku-portrait.webp",
"backgroundUrl": "https://your-cdn.example.com/miku-bg.webp",
"primaryColor": "#39C5BB",
"accentColor": "#86EFDF"
}
And an optional prior probability in src/data/characterProbabilities.json:
{
"characterId": "hatsune-miku",
"baseProbability": 0.15
}
Rules:
• id must be unique and kebab-case.
• mbtiType must be one of the 16 standard types.
• archetypeId must match an id in archetypes.json.
• stats values are integers 0–100.
Adding New Quiz Questions
Edit src/data/questions.json — append to the array:
{
"id": 40,
"text": "在一个陌生的聚会上,你更倾向于主动找人搭话还是等别人来找你?",
"dimension": "EI",
"archetypeWeights": {
"glowing-protagonist": 2,
"ice-observer": -2,
"oath-captain": 1,
"agile-spinner": 1,
"gentle-healer": 0,
"shadow-strategist": -1,
"chaotic-spark": 2,
"moonlit-guardian": -1
},
"tags": ["social", "introvert-extrovert"]
}
Guidelines:
id must be unique and increment sequentially.
dimension is one of "EI" | "SN" | "TF" | "JP".
archetypeWeights keys must match all 8 archetype id values; weights range -3 to +3.
- Positive weight = answer "strongly agree" nudges toward that archetype.
- Keep question text in Chinese (Simplified) to match existing copy.
Modifying Archetypes (src/data/archetypes.json)
{
"id": "glowing-protagonist",
"name": "发光主角位",
"mbtiTypes": ["ENFJ", "ENFP"],
"description": "天生的领袖与感召者,能点燃周围人的热情。",
"strengths": ["感召力强", "共情深刻", "行动力高"],
"weaknesses": ["容易过度承担", "情绪波动大"],
"color": "#FF6B6B"
}
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].
useQuiz Composable (state management)
import { useQuiz } from '@/composables/useQuiz';
const {
currentQuestion,
currentIndex,
totalQuestions,
progress,
answer,
goBack,
result,
isComplete,
resetQuiz,
} = useQuiz();
Share / Export Poster (useShare)
import { useShare } from '@/composables/useShare';
const { exportPNG, shareNative } = useShare();
await exportPNG('#share-poster', 'my-acgti-result.png');
await shareNative({
title: 'My ACGTI Result',
text: `I got ${result.value?.characterId}!`,
url: 'https://acgti.tianxingleo.top',
});
Routing (src/router/index.ts)
const routes = [
{ path: '/', name: 'home', component: HomePage },
{ path: '/intro', name: 'intro', component: IntroPage },
{ path: '/quiz', name: 'quiz', component: QuizPage },
{ path: '/result', name: 'result', component: ResultPage },
{ path: '/characters',name: 'characters', component: CharactersPage },
{ path: '/about', name: 'about', component: AboutPage },
];
Navigate programmatically after quiz completion:
import { useRouter } from 'vue-router';
const router = useRouter();
router.push({ name: 'result' });
localStorage Utilities (src/utils/storage.ts)
import { saveResult, loadResult, clearResult } from '@/utils/storage';
import type { QuizResult } from '@/types/quiz';
saveResult(result);
const saved: QuizResult | null = loadResult();
clearResult();
Deployment
Cloudflare Pages (recommended)
- Connect GitHub repo → Cloudflare Pages dashboard.
- Build command:
npm run build
- Build output directory:
dist
- No environment variables required (pure frontend).
GitHub Actions CI
The repo includes a workflow that runs on every push to main/dev and on PRs:
- run: npm ci
- run: npm run build
Release a version
git tag v1.2.0
git push origin v1.2.0
Common Patterns & Tips
Filtering characters by archetype in a component
import characters from '@/data/characters.json';
import type { Character } from '@/types/quiz';
const archetypeId = 'glowing-protagonist';
const subset: Character[] = characters.filter(
(c) => c.archetypeId === archetypeId
);
Accessing visuals by character ID
import visuals from '@/data/characterVisuals.json';
import { enrichCharacterVisuals } from '@/utils/characterVisuals';
const enriched = enrichCharacterVisuals(characters, visuals);
Reactive dimension label (E vs I, etc.)
function dimensionLabel(dim: Dimension, score: number): string {
const labels: Record<Dimension, [string, string]> = {
EI: ['E 外向', 'I 内向'],
SN: ['N 直觉', 'S 实感'],
TF: ['T 思考', 'F 情感'],
JP: ['J 判断', 'P 知觉'],
};
return score >= 50 ? labels[dim][0] : labels[dim][1];
}
Troubleshooting
| Symptom | Likely cause | Fix |
|---|
npm run build fails with type errors | New JSON data doesn't match types | Run npx tsc --noEmit and fix mismatches in src/types/quiz.ts |
| Character not appearing in results | archetypeId mismatch between characters.json and archetypes.json | Ensure archetypeId exactly matches an archetype id |
| New question not affecting scores | dimension key is wrong | Must be exactly "EI", "SN", "TF", or "JP" |
| Poster export is blank | html2canvas can't load cross-origin images | Host character images on a CORS-enabled CDN or use base64 data URIs |
| Route returns 404 on Cloudflare Pages | SPA fallback not configured | Add _redirects file: /* /index.html 200 in public/ |
Dev server errors on @/ imports | Vite alias not resolving | Check vite.config.ts has resolve: { alias: { '@': '/src' } } |