Skip to content

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:

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
this.SetData("attempts", this.Data.attempts + 1);  // OK
this.SetData("attempts", "wrong");                   // Compile error
this.SetData("nonexistent", 1);                      // Compile error

Scoped 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.)

typescript
OnObjectivesStart() {
    this.Events.on("Terminal.NmapScan", (data) => {
        // re-attached on every game start;
        // automatically removed on complete/abandon
    });
}
MethodDescription
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

HookWhen 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:

typescript
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 },
];
PropertyTypeDescription
namestringUnique identifier
descriptionstringDisplayed to the player
unlocksAfterstring[]Objective names that must complete first
hiddenbooleanHidden until unlocked
hintstringHint text shown to the player
infostringAdditional info text
terminalCommandstringSuggested terminal command
triggerobjectDeclarative auto-complete trigger

Declarative Triggers

Instead of manually calling completeObjective(), you can use declarative triggers:

typescript
Objectives = [
    {
        name: "scan",
        description: "Scan the target",
        trigger: {
            event: "Terminal.NmapScan",
            condition: (data) => data.ip === this.Data.targetIp,
        },
    },
];

Manual Completion

typescript
this.completeObjective("scan");

Quest Properties

PropertyTypeDefaultDescription
NamestringrequiredUnique quest identifier
TitlestringrequiredDisplay title
DescriptionstringQuest description
IconstringQuest icon
Group"storyline" | "side" | "sandbox""sandbox"Quest category in the journal
RewardsQuestRewardsMoney, XP rewards
EmployerPartial<QuestEmployer>auto-generatedQuest giver (random if not set)
AutoStartbooleanfalseStart automatically
AutoCompletebooleantrueComplete when all objectives done
QuestsToCompletestring[]Required quests before this one
MaxClaimnumberMax times this quest can be claimed
MaxClaimPerDaynumberMax claims per day (daily quests)
AbandonablebooleanWhether player can abandon
HasCompleteButtonbooleanShow a manual "Complete" button
HackhubPostQuestHackhubPostDefinitionAdvertise 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.

typescript
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." },
    ],
};

content and comments[].content are plain strings. media and avatar accept a mod asset path (resolved relative to your mod) or a URL. If you omit author, the quest's (auto-generated) employer is used.

Mail

Send in-game emails during quests:

typescript
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:

typescript
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)

typescript
@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)

typescript
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)

typescript
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):

typescript
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.

typescript
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:

typescript
// In a Website component
<button onClick={() => Quest.claim("DataHeist")}>
    Accept Job
</button>

HotBunny Interactive Entertainment Inc.