Quiz
The quiz system provides interactive multiple-choice tests (4 options per question). Quizzes are used in quests to test player knowledge, with randomized answer order and score tracking. <span class="since-badge">v3.0.0</span>
How It Works
graph TD
A[Quest triggers showquiz] --> B[Server loads quiz definition]
B --> C[Options randomized per question]
C --> D[Send question to client]
D --> E[Player selects answer]
E --> F{More questions?}
F -->|YES| C
F -->|NO| G[Show result screen]
G --> H[Score stored in attribute]
H --> I[Auto-check objective]Key behavior:
- Sequential — Questions are answered one at a time, in order.
- Randomized options — The order of A/B/C/D is randomized per player per question. The same player always sees the same order for the same question.
- Persistent — Progress (index, correct count, wrong count, option order) is stored in player
WatchedAttributes. - Auto-check — When the quiz finishes, the framework automatically calls
checkobjectiveso the quest can verify the result. - Retry — The player can retry the quiz. This resets all progress and re-randomizes options.
Quiz Definition
Quizzes are loaded from mod assets: config/quizzes/*.json
{
"id": "lore-quiz-chapter1",
"titleLangKey": "yourmod:quiz-chapter1-title",
"bodyLangKey": "yourmod:quiz-chapter1-body",
"questionCount": 5,
"neededCorrect": 3,
"questionLangKeyFormat": "yourmod:quiz-chapter1-q{0}",
"optionALangKeyFormat": "yourmod:quiz-chapter1-q{0}-a",
"optionBLangKeyFormat": "yourmod:quiz-chapter1-q{0}-b",
"optionCLangKeyFormat": "yourmod:quiz-chapter1-q{0}-c",
"optionDLangKeyFormat": "yourmod:quiz-chapter1-q{0}-d",
"correctOptions": [1, 3, 2, 4, 1],
"scoreAttributeKey": "vsquest:quiz:lore-quiz-chapter1:score",
"progressTemplateLangKey": "yourmod:quiz-progress-template",
"resultTemplateLangKey": "yourmod:quiz-result-template",
"retryButtonLangKey": "yourmod:quiz-retry",
"closeButtonLangKey": "yourmod:quiz-close",
"resultBodyLangKeys": [
"yourmod:quiz-chapter1-result-bad",
"yourmod:quiz-chapter1-result-ok",
"yourmod:quiz-chapter1-result-good"
],
"resultBodyScoreThresholds": [0, 3, 5]
}Fields Reference
| Field | Type | Description |
|---|---|---|
id | string | Unique quiz identifier. |
titleLangKey | string | Localization key for the quiz dialog title. |
bodyLangKey | string | Localization key for the quiz description/instruction text. |
questionCount | int | Number of questions in the quiz. |
neededCorrect | int | Minimum correct answers required to pass (shown in UI). |
questionLangKeyFormat | string | Format string for question localization keys. {0} is replaced with question index (1-based). |
optionALangKeyFormat | string | Format string for option A localization keys. |
optionBLangKeyFormat | string | Format string for option B localization keys. |
optionCLangKeyFormat | string | Format string for option C localization keys. |
optionDLangKeyFormat | string | Format string for option D localization keys. |
correctOptions | int[] | Array of correct answer indices (1=A, 2=B, 3=C, 4=D) for each question. |
scoreAttributeKey | string | Player attribute key where the final score is stored. |
progressTemplateLangKey | string | Localization key for the progress bar text (e.g., "Question {current} of {total}"). |
resultTemplateLangKey | string | Localization key for the result summary text (e.g., "You got {correct} out of {total}"). |
retryButtonLangKey | string | Localization key for the retry button. |
closeButtonLangKey | string | Localization key for the close button. |
resultBodyLangKeys | string[] | Array of localization keys for result body text (different messages based on score). |
resultBodyScoreThresholds | int[] | Score thresholds for selecting result body text. Must match resultBodyLangKeys length. |
Localization
The quiz uses a format-based localization system. You define patterns in the quiz JSON, and the framework resolves keys at runtime.
Example lang file:
{
"yourmod:quiz-chapter1-title": "Chapter 1 Quiz",
"yourmod:quiz-chapter1-body": "Answer these questions to prove your knowledge.",
"yourmod:quiz-chapter1-q1": "What is the capital of the Northern Kingdom?",
"yourmod:quiz-chapter1-q1-a": "Winterhold",
"yourmod:quiz-chapter1-q1-b": "Duskwall",
"yourmod:quiz-chapter1-q1-c": "Ironhaven",
"yourmod:quiz-chapter1-q1-d": "Stonegate",
"yourmod:quiz-chapter1-q2": "Which creature is immune to fire?",
"yourmod:quiz-chapter1-q2-a": "Wolf",
"yourmod:quiz-chapter1-q2-b": "Drifter",
"yourmod:quiz-chapter1-q2-c": "Phoenix",
"yourmod:quiz-chapter1-q2-d": "Bear",
"yourmod:quiz-progress-template": "Question {0} of {1}",
"yourmod:quiz-result-template": "You got {0} correct out of {1}",
"yourmod:quiz-chapter1-result-bad": "You need to study more...",
"yourmod:quiz-chapter1-result-ok": "Not bad, but you can do better.",
"yourmod:quiz-chapter1-result-good": "Excellent! You know this chapter well.",
"yourmod:quiz-retry": "Try Again",
"yourmod:quiz-close": "Close"
}Option Randomization
Answer options are randomized per player per question. The order is stored in player WatchedAttributes so reopening the same question shows the same order.
Storage key: vsquest:quiz:{quizId}:order:{questionIndex}
Example: For quiz lore-quiz-chapter1, question 1:
- Server stores
"vsquest:quiz:lore-quiz-chapter1:order:1" = "3,1,4,2" - This means option A shows original C, B shows A, C shows D, D shows B
- The correct answer is tracked internally via the original index
Retry behavior: When the player clicks retry, all order keys are deleted, so the next attempt gets a new randomization.
Result Body Selection
If resultBodyLangKeys and resultBodyScoreThresholds are defined, the result text is selected based on the player's score:
score >= thresholds[2] → resultBodyLangKeys[2] (best)
score >= thresholds[1] → resultBodyLangKeys[1] (medium)
score >= thresholds[0] → resultBodyLangKeys[0] (worst)The system iterates from index 0 upward, selecting the last threshold the score meets or exceeds.
Example:
thresholds: [0, 3, 5]keys: ["bad", "ok", "good"]- Score 0–2 → "bad"
- Score 3–4 → "ok"
- Score 5 → "good"
Player Attribute Storage
Quiz progress is stored in the player's WatchedAttributes:
| Key | Description |
|---|---|
vsquest:quiz:{quizId}:index | Current question index (1-based). |
vsquest:quiz:{quizId}:correct | Number of correct answers so far. |
vsquest:quiz:{quizId}:wrong | Number of wrong answers so far. |
vsquest:quiz:{quizId}:order:{n} | Randomized option order for question n. |
{scoreAttributeKey} | Final score (set only after quiz completion). |
Network Messages
| Message | Direction | Description |
|---|---|---|
OpenQuizMessage | Client → Server | Player opens the quiz dialog or clicks retry. |
ShowQuizMessage | Server → Client | Question data or result screen for display. |
SubmitQuizAnswerMessage | Client → Server | Player selected an answer (1–4). |
Integration with Quests
Starting a Quiz (Action)
Use the showquiz action in a quest or dialogue:
{
"id": "showquiz",
"args": ["lore-quiz-chapter1"]
}This resets the quiz and sends the first question to the player.
Checking the Result (Objective)
After the quiz finishes, the score is stored in the player attribute defined by scoreAttributeKey. Use checkvariable to check it:
{
"id": "checkvariable",
"args": ["vsquest:quiz:lore-quiz-chapter1:score", ">=", "3"]
}Note: The quiz automatically calls checkobjective on completion, so the quest objective system checks immediately when the player finishes.
Complete Quest Example
A quest that requires the player to pass a quiz:
{
"id": "yourmod:quiz-quest",
"cooldown": -1,
"perPlayer": true,
"onAcceptedActions": [
{ "id": "showquiz", "args": ["lore-quiz-chapter1"] }
],
"actionObjectives": [
{
"id": "pass_quiz",
"objectiveId": "checkvariable",
"args": ["vsquest:quiz:lore-quiz-chapter1:score", ">=", "3"]
}
],
"actionRewards": [
{ "id": "notify", "args": ["yourmod:quiz-passed"] },
{ "id": "addreputation", "args": ["npc", "scholar", "10"] }
]
}What happens:
- Player accepts the quest → quiz opens automatically.
- Player answers 5 questions.
- If score ≥ 3, the objective completes → quest rewards fire.
- If score < 3, the player can retry by clicking "Try Again".
Tips
- Always use
scoreAttributeKey— Without it, the score is not stored, andcheckvariablecannot verify the result. - Match thresholds to
neededCorrect— IfneededCorrect: 3, set thresholds to[0, 3, 5]so the UI shows appropriate feedback. - Keep questions short — The quiz dialog is compact. 1–2 sentences per question is ideal.
- Use
neededCorrectfor UI guidance — The result screen shows "Required: X" so the player knows what they need. - Test retry — After failing, click retry to verify the quiz resets correctly and option order changes.
- One quiz per quest — The quiz system is designed for one quiz at a time per quest. Don't trigger multiple quizzes simultaneously.
- Private shop integration — Quizzes can be used as survey gates for private shops. If a shop's
surveyAttributematches the quiz'sscoreAttributeKey, passing the quiz automatically grants the shop's one-time bonus.