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,
};
}
// Runs ONCE, when the quest is first claimed. Use it for one-time
// setup (build the network, send mail, post tweets, seed data).
// It is NOT called again when the game is reloaded.
OnStart() {
Network.createSubnetNetwork({
ip: this.Data.targetIp,
type: Network.Type.Router,
ports: [{ external: 22, internal: 22, active: true, service: "ssh" }],
users: [Network.createUser({ username: "admin", password: "secret123" })],
children: [],
});
}
// Runs once on claim AND again every time the game starts (reload).
// Register event listeners and any work that must survive a reload here —
// listeners live in memory and are lost when the game restarts.
OnObjectivesStart() {
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", (ip) => {
if (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 | Promise<T>
Override this method to return the initial quest data. This is called exactly once.
CreateData(): MyQuestData {
return {
targetIp: Network.randomIp(),
secretCode: Random.password(),
};
}CreateData() may also be async if you need to await something to seed the data. The quest is only claimed after the returned promise resolves.
async CreateData(): Promise<MyQuestData> {
const profile = await fetchTargetProfile();
return {
targetIp: Network.randomIp(),
secretCode: profile.code,
};
}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.
Register listeners in OnObjectivesStart(), not OnStart(). Listeners live in memory and are lost when the game is reloaded — and OnStart() only runs once (at claim). OnObjectivesStart() runs again on every game start, so it re-attaches the listeners. (See Lifecycle Hooks.)
OnObjectivesStart() {
this.Events.on("Terminal.NmapScan", (data) => {
// re-attached on every game start;
// 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() | Once, when the quest is first claimed. One-time setup only (build networks, send mail, post tweets, seed data). Not called again on reload. |
OnObjectivesStart() | Once on claim and again every time the game starts (reload). Register event listeners and any work that must survive a reload here. |
OnComplete() | All objectives completed. Clean up resources here. |
OnAbandon() | Player abandons the quest. |
OnStart vs OnObjectivesStart
OnStart() runs only once, at claim time. Event listeners live in memory and are wiped when the game restarts, so listeners registered in OnStart() silently stop working after a reload and the quest can no longer progress.
Put one-time setup in OnStart(), and put event listeners (and anything that must re-run on every game start) in OnObjectivesStart(). Alternatively, use declarative triggers, which are re-attached automatically on every 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 |
Group | "storyline" | "side" | "sandbox" | "sandbox" | Quest category in the journal |
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 |
MaxClaimPerDay | number | — | Max claims per day (daily quests) |
Abandonable | boolean | — | Whether player can abandon |
HasCompleteButton | boolean | — | Show a manual "Complete" button |
HackhubPost | QuestHackhubPostDefinition | — | Advertise the quest on the Hackhub feed so players can discover & claim it |
Hackhub Feed Post
Make a quest discoverable from the Hackhub homepage feed — the player can find it and claim it straight from the feed, exactly like the built-in side quests. Set HackhubPost and the post appears automatically once the quest's QuestsToComplete prerequisites (if any) are met and it hasn't been claimed yet.
HackhubPost = {
content: "Looking for someone to investigate a suspicious server. Pays well. DM me.",
media: "assets/job-flyer.png", // optional image (mod asset path)
author: { name: "Anonymous", avatar: "assets/anon.png" }, // optional; defaults to the employer / "Hidden User"
likes: 12, // optional
comments: [ // optional, pre-seeded
{ author: { name: "h4x0r" }, content: "Sounds sketchy. I'm in." },
],
};
contentandcomments[].contentare plain strings.mediaandavataraccept a mod asset path (resolved relative to your mod) or a URL. If you omitauthor, the quest's (auto-generated) employer is used.
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");
}Social Integrations
Quests can register NPC accounts and schedule messages on social platforms declaratively.
Twotter (Twitter-like)
@RegisterQuest
export class SocialQuest extends Quest {
Name = "SocialIntel";
Title = "Social Intelligence";
Objectives = [{ name: "find", description: "Find the informant" }];
TwotterAccounts = [
{ username: "darknet_informant", bio: "I know things.", verified: true },
];
Tweets = [
{
content: "Something big is happening tonight...",
interaction: { comments: 5, share: 2, likes: 42, views: 1000 },
showInTimeline: true,
},
];
OnStart() { /* ... */ }
}Kisscord (Discord-like)
KisscordChats = [
{
userId: "informant-id",
messages: [
{ content: "Hey, are you there?", delayMs: 0 },
{ content: "I need to tell you something.", delayMs: 2000 },
{ content: "Check your email.", delayMs: 4000 },
],
},
];WeeChat (IRC)
WeeChatChats = [
{
host: "irc.darknet.org",
password: "secret123",
messages: [
{ content: "Welcome to the channel.", username: "admin" },
{ content: "The target IP is 45.33.32.156", username: "informant", delayMs: 3000 },
],
},
];Messages are sent in sequence when the quest starts. Use delayMs to control timing between messages. Each message can have an onSent callback for triggering logic after delivery.
Gating messages behind progress (unlocksAfter)
By default the whole chain plays back-to-back at quest start. Add unlocksAfter to a Kisscord message to hold it — and every message chained after it — until the listed objectives are all complete. This lets one conversation drip-feed later replies as the player progresses (available since SDK v0.18.0):
KisscordChats = [
{
contactId: "slate",
messages: [
{ content: "you awake", delayMs: 500 }, // plays at start
{ content: "check /arc/ on 4chan", delayMs: 1300 }, // plays at start
{ content: "found the handle", isMine: true,
unlocksAfter: ["run-whois"] }, // waits for run-whois
{ content: "good enough.", delayMs: 1200,
unlocksAfter: ["run-whois", "open-dead-handles-room"] },
],
},
];The chain pauses at the first gated message and resumes automatically the moment its objectives are satisfied (reloads included).
Starting Quests Programmatically
Use Quest.claim() to start a quest from anywhere — website buttons, events, terminal commands, etc.
import { Quest } from "@hotbunny/hackhub-content-sdk";
// By name
Quest.claim("HackThePlanet");
// By class reference
Quest.claim(MyQuestClass);Requires permission: events
This is useful for triggering quests from in-game websites:
// In a Website component
<button onClick={() => Quest.claim("DataHeist")}>
Accept Job
</button>