Quests
Quests are multi-objective tasks that players can claim and complete. The quest system supports type-safe data, scoped event listeners, mail, and phone call dialogs.
Basic Quest
Create a quest by extending Quest<T> with a data type parameter:
import { Quest, Network, RegisterQuest } from "@hotbunny/hackhub-content-sdk";
interface MyQuestData {
targetIp: string;
attempts: number;
}
@RegisterQuest
class InfiltrationQuest extends Quest<MyQuestData> {
Name = "Infiltration";
Title = "Server Infiltration";
Description = "Hack into the target server.";
Rewards = { money: 5000, xp: 200 };
Objectives = [
{ name: "scan", description: "Scan the target server" },
{ name: "connect", description: "Connect via SSH", unlocksAfter: ["scan"] },
];
CreateData(): MyQuestData {
return {
targetIp: Network.randomIp(),
attempts: 0,
};
}
OnStart() {
Network.createSubnetNetwork({
ip: this.Data.targetIp,
type: "ROUTER",
ports: [{ external: 22, internal: 22, active: true, service: "ssh" }],
users: [Network.createUser({ username: "admin", password: "secret123" })],
children: [],
});
this.Events.on("Terminal.NmapScan", (data) => {
if (data.ip === this.Data.targetIp) {
this.SetData("attempts", this.Data.attempts + 1);
this.completeObjective("scan");
}
});
this.Events.on("Terminal.SSH.Connected", (data) => {
if (data.ip === this.Data.targetIp) this.completeObjective("connect");
});
}
OnComplete() {
Network.destroyNetwork(this.Data.targetIp);
}
}Quest Data
Quest data is initialized once when the quest is first claimed via CreateData(). The returned object is persisted and available as this.Data throughout the quest lifecycle.
CreateData(): T
Override this method to return the initial quest data. This is called exactly once.
CreateData(): MyQuestData {
return {
targetIp: Network.randomIp(),
secretCode: Random.password(),
};
}this.Data
Access the quest data. Fully typed based on the generic parameter.
OnStart() {
console.log(this.Data.targetIp); // string
console.log(this.Data.secretCode); // string
}SetData(key, value)
Update a single key in the quest data and persist it. Both key and value are type-safe.
this.SetData("attempts", this.Data.attempts + 1); // OK
this.SetData("attempts", "wrong"); // Compile error
this.SetData("nonexistent", 1); // Compile errorScoped Events
Use this.Events instead of the global Events namespace inside quests. Listeners registered through this.Events are automatically cleaned up when the quest completes or is abandoned, preventing memory leaks.
OnStart() {
this.Events.on("Terminal.NmapScan", (data) => {
// automatically removed on complete/abandon
});
}| Method | Description |
|---|---|
this.Events.on(event, handler) | Register a listener (auto-cleaned) |
this.Events.off(event, handler) | Remove a specific listener |
this.Events.offAll() | Remove all listeners |
TIP
Always prefer this.Events.on() over the global Events.on() inside quests. The global Events.on() does not auto-cleanup and can cause memory leaks.
Lifecycle Hooks
| Hook | When it's called |
|---|---|
OnStart() | Quest is claimed/started. Set up event listeners here. |
OnComplete() | All objectives completed. Clean up resources here. |
OnAbandon() | Player abandons the quest. |
OnObjectivesStart() | Objectives listener starts (e.g. after game load). |
Objectives
Objectives are defined as an array of objects:
Objectives = [
{ name: "scan", description: "Scan the target" },
{ name: "exploit", description: "Run the exploit", unlocksAfter: ["scan"] },
{ name: "download", description: "Download the files", unlocksAfter: ["exploit"], hidden: true },
];| Property | Type | Description |
|---|---|---|
name | string | Unique identifier |
description | string | Displayed to the player |
unlocksAfter | string[] | Objective names that must complete first |
hidden | boolean | Hidden until unlocked |
hint | string | Hint text shown to the player |
info | string | Additional info text |
terminalCommand | string | Suggested terminal command |
trigger | object | Declarative auto-complete trigger |
Declarative Triggers
Instead of manually calling completeObjective(), you can use declarative triggers:
Objectives = [
{
name: "scan",
description: "Scan the target",
trigger: {
event: "Terminal.NmapScan",
condition: (data) => data.ip === this.Data.targetIp,
},
},
];Manual Completion
this.completeObjective("scan");Quest Properties
| Property | Type | Default | Description |
|---|---|---|---|
Name | string | required | Unique quest identifier |
Title | string | required | Display title |
Description | string | — | Quest description |
Icon | string | — | Quest icon |
Rewards | QuestRewards | — | Money, XP rewards |
Employer | Partial<QuestEmployer> | auto-generated | Quest giver (random if not set) |
AutoStart | boolean | false | Start automatically |
AutoComplete | boolean | true | Complete when all objectives done |
QuestsToComplete | string[] | — | Required quests before this one |
MaxClaim | number | — | Max times this quest can be claimed |
Abandonable | boolean | — | Whether player can abandon |
Mail
Send in-game emails during quests:
Mails = [
{
title: "Mission Briefing",
content: "Your target is ready. Good luck.",
attachment: { name: "target", extension: "txt", content: "IP: 10.0.0.50" },
},
];
OnStart() {
this.sendMail(0); // Send first mail
}Dialogs
Create phone-call dialog trees:
Dialog = {
default: [
{
speaker: "Handler",
text: "Are you ready for the mission?",
options: [
{ label: "Yes", text: "I'm ready.", switchBranch: "briefing" },
{ label: "No", text: "Not yet.", isEnd: true },
],
},
],
briefing: [
{ speaker: "Handler", text: "Good luck out there.", isEnd: true },
],
};
OnStart() {
this.createDialog("default");
}