Skip to content

WIP: Add random events (framework + events)#1064

Open
HarleyGilpin wants to merge 44 commits into
GregHib:mainfrom
HarleyGilpin:random-events/framework
Open

WIP: Add random events (framework + events)#1064
HarleyGilpin wants to merge 44 commits into
GregHib:mainfrom
HarleyGilpin:random-events/framework

Conversation

@HarleyGilpin

@HarleyGilpin HarleyGilpin commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Overview

Implements the anti-macro Random Events D&D (ref GregHib/void-private#223). This is the main PR for the feature: it lands the shared framework and then accumulates each of the 18 current (2011) random events as they're built, verified against the wiki (oldid=3667851).

Framework

  • Lifecycle (content/activity/event/random/RandomEvents): weighted pick() over the random_events table, start/complete/fail, persisted per-player cooldown, and noteAndTeleport (ignore penalty — notes the inventory then exiles the player so an event can't be macroed through).
  • Trigger (RandomEventTrigger): rolls per XP drop (1-in-N), gated by an eligible() check (no bots, combat, instances, dialogue, or no_random_events areas), plus a ::randomevent [event] admin command.
  • Entry (RandomEventKidnap): relog resume, teleport-out blocking, the kidnap(tile) helper, and the mysteriousOldMan() intro. Two supported patterns:
    • Kidnap-to-instance/room — the player is teleported to an event area (Maze, Freaky Forester).
    • In-place nagging (InPlaceRandomEvent) — the event NPC spawns beside the player, follows and nags, and applies the ignore penalty on timeout (Certer, Sandwich Lady, Evil Twin...).
  • Shared rewardrewardCostumeOrCoins (collect-the-set: camo, lederhosen).

Events

  • Maze — GregHib/void-private#257
  • Freaky Forester — GregHib/void-private#255
  • Certer — GregHib/void-private#253
  • Drill Demon — GregHib/void-private#254
  • Kiss the Frog — GregHib/void-private#256
  • Mime — GregHib/void-private#258
  • Cap'n Arnav's Chest — GregHib/void-private#259
  • Quiz Master — GregHib/void-private#260
  • Sandwich Lady — GregHib/void-private#261
  • Evil Bob — GregHib/void-private#262
  • Surprise Exam — GregHib/void-private#263
  • Bee-Keeper — GregHib/void-private#264
  • Evil Twin — GregHib/void-private#265
  • Pinball — GregHib/void-private#266
  • Gravedigger — GregHib/void-private#267
  • Pillory — GregHib/void-private#268
  • Prison Pete — GregHib/void-private#269
  • Lost and Found — GregHib/void-private#270

Testing

./gradlew :game:test --tests "content.activity.event.random.*" — framework + Maze + Freaky Forester suites green. Spotless clean.

Manual test

::randomevent <event> starts any registered event (or ::randomevent for a random pick). Current: maze, freaky_forester.

Implements the random event trigger, kidnap teleport and mysterious old
man framework, plus the Maze random event where players navigate to a
central shrine before time runs out.

Tutorial Island is tagged no_random_events so events don't fire there.

Ref GregHib/void-private#223
Extend the random event system beyond the kidnap-to-instance pattern so
in-place nagging events (Certer, Sandwich Lady, Evil Twin) can be built:

- RandomEvents.noteAndTeleport: ignore penalty that notes the player's
  inventory then exiles them, so an event can't be macroed through.
- InPlaceRandomEvent + startInPlaceEvent/endInPlaceEvent: spawn an event
  NPC beside the player that follows and nags on a timer, applying the
  ignore penalty when its lifetime runs out.
- Decouple entry from the old man: the trigger now calls RandomEvents.start
  directly and each launcher owns its intro. Kidnap events call the new
  mysteriousOldMan() helper; in-place events spawn their own NPC. Maze's
  launcher allocates its instance before the intro so a resumed timer
  doesn't fail the event mid-intro.
- rewardCostumeOrCoins: shared collect-the-set reward (camo, lederhosen).

Ref GregHib/void-private#223
The player is teleported to the forester's clearing and told to kill a
pheasant with a specific number of tails. Only the assigned pheasant
drops the correct raw pheasant; wrong ones drop an incorrect one that the
forester rejects. Handing in the correct pheasant rewards the first
missing lederhosen piece, or 500 coins if the set is complete.

- Conditional pheasant drops keyed to the killer's assigned task
- Blocks attacking further pheasants once one is killed
- Static pheasant spawns in the clearing; forester dialogue drives the task
- Removes the unconditional pheasant drop table (drops are now per-task)

Ref GregHib/void-private#255
The assigned pheasant now drops the raw pheasant as a normal killer-owned
floor drop (non-tradeable, private) for the player to pick up and hand to
the forester, matching the live behaviour.
@HarleyGilpin HarleyGilpin changed the title Add random events framework + Maze event Add Random Events Framework + Random Events Jul 8, 2026
@HarleyGilpin HarleyGilpin changed the title Add Random Events Framework + Random Events Add random events (framework + events) Jul 8, 2026
First in-place event on the framework: the Sandwich Lady appears beside
the player pushing her tray and nags until spoken to. Talking opens her
refreshment tray (interface 297); choosing the food she offered hands it
over and ends the event in place, while any other choice knocks the
player out via the note-and-teleport ignore penalty. Talking mid-combat
hands over the correct food directly, and other players are turned away.

Adds RandomEvents.completeInPlace for events that finish without a return
teleport, and defines interface 297's seven food buttons (10-22).

Ref GregHib/void-private#261
She now says "Hope that fills you up!" when handing over the correct
food, and smacks the player for 3 before the knockout when they pick the
wrong one, matching live behaviour.
- Show "Hope that fills you up!" as a chatbox line on a correct pick so
  the player actually sees it, instead of a fleeting overhead force-chat.
- On a wrong pick she swings her baguette (animation), smacks the player,
  who plays the death animation and fades out before waking up elsewhere.
Sergeant Damien whisks the player to his exercise yard and barks an
order. Each of the four mats shows an exercise on its sign (a varbit);
the player uses the mat matching the order, performing the exercise
animation. Four correct exercises earns a piece of the camouflage outfit
(helmet/top/bottoms, first missing) or 500 coins if the set is complete.

- Shuffles the four exercises across the mat signs each round
- Exercise index 0 (jog) is read with a 0 default since the variable
  store clears vars set to their default value
- Reuses the shared kidnap/complete and rewardCostumeOrCoins helpers

Ref GregHib/void-private#254
- Encode the four exercises as sign varbit values 1-4 (0 = no sign). The
  variable store clears any var set to its default 0, so a sign set to 0
  never reached the client - one mat showed no sign and the client's signs
  drifted out of sync with the server, making the correct mat read as wrong.
- Walk the player onto the mat before performing the exercise.
- On using a mat the player now faces the sergeant, plays the exercise
  sound and waits two ticks before performing the exercise animation.
- When the signs reshuffle, a puff of smoke (gfx 2000) plays on each sign
  tile in turn from sign 1 to sign 4, a tick apart, as it changes.
The four signs now all change on one tick and the smoke puffs sweep
across them client-side, sign 1 to sign 4, half a tick (15 client ticks)
apart, instead of one server tick between each change.
After Sergeant Damien names the exercise, an item box shows the matching
sign with "Go to this mat and <action>!" ("this mat" in red), matching
2009scape. Adds the missing push-up sign item (10946). Darkan doesn't
implement Drill Demon.
On arrival the yard shows "Follow Sergeant Damien's orders!" and waits
about five seconds before the sergeant barks the first exercise. Using a
mat (or talking to him) before the order is given has him snap "I haven't
given you the order yet, worm!" and earns no credit for that attempt.
@HarleyGilpin HarleyGilpin changed the title Add random events (framework + events) WIP: Add random events (framework + events) Jul 8, 2026
One of the certificate brothers appears beside the player, bows and nags.
Talking opens the identify interface (184) showing an item and three
descriptions; picking the one that matches the item rolls the shared
gem/coin reward table, a wrong pick gives nothing, and ignoring him
applies the note-and-teleport penalty. Adds the shared random_event_certer
loot table and its roller (reused later by Pillory).

Ref GregHib/void-private#253
Interface 184 isn't the Certer window in the 634 cache (only five
components, and the runtime item definition can't supply a model to
render on it), so the puzzle didn't display. Show the item with the
standard item dialogue box and pick the description from a choice menu
instead - reliable and still faithful to the identify-the-item mechanic.
Certer isn't implemented in Darkan (only 2009scape).
Decoded interface 184's full component types: it has a text prompt
(comp 1) and three item-model options (comps 4-6, not text), so the 634
version reads out a description and the player picks the matching item
model - the reverse of the older cache. The certer now sends the
description to the prompt and three item models to the options.
The type-6 model option components don't render items sent via
INTERFACE_ITEM, so send each item's model id directly. The runtime item
definition doesn't expose model ids, so the eleven pool items' models
(decoded from the cache) are listed alongside their descriptions.
The item to identify is a separate model component (comp 0, the box) and
the three A/B/C Select components hold the description text. Send the
item's model to the box and the three descriptions (one correct, two
false) to the options; the player picks the description matching the
shown item.
Comment on lines +30 to +35
private val events = mutableMapOf<String, suspend Player.() -> Unit>()

fun register(id: String, launcher: suspend Player.() -> Unit) {
Script.checkLoading()
events[id] = launcher
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably doesn't matter as you're using a map with unique keys but it would be good practise to have a clear() method like other Script api's because script .init's and registrations can be called more than once (like in a script reload, or tests) which could lead to duplicates or invalid states.

Comment on lines +41 to +45
private fun Player.giveOrDrop(item: String, amount: Int) {
if (!inventory.add(item, amount)) {
FloorItems.add(tile, item, amount, disappearTicks = 300, owner = this)
}
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addOrDrop already exists

Interface 184 builds itself from a load script (cs2 2136): it reads the
three option descriptions from enum 2224 and the item model from enum
2225, all selected by varbits (5855-5863). So instead of pushing text and
models (which the script overwrites), the event now sets those varbits -
three description keys, the model key for the answer ((desc+2)%9 in enum
2225), and rotation/scale - then opens the interface. The dynamically
created option rows declare their Select action so clicks validate.

Removes the temporary interface-click debug log.
The load script's clickable option rows are components 4-6, not 0-2, so
map option_1/2/3 to them; clicks now register and the matching answer
rewards.
The player is whisked to a stage where the mime performs emotes to copy.
A per-player mime spawns in an instance and repeats the target emote on a
timer; the event hooks the emote tab (alongside its own handler) to check
the copy. Four correct emotes in a row unlock the mime emotes (glass
wall/box, climb rope, lean) and award a mime costume piece, or coins if
the set is complete. No 2009scape/Darkan reference - built from the wiki.

Ref GregHib/void-private#258
An evil incarnation of Bob the cat teleports the player to the Scape2009
island and demands fish. A terrified servant pans the camera to the
fishing spot Bob likes; the player nets there (the fish comes out already
cooked), uncooks it at the cold fire, and serves the raw fish to Bob. The
right fish sends him to sleep and opens the exit portal for a 650 Fishing
or Magic XP reward; a wrong one makes Bob more attentive so an extra
correct fish is needed. Ported from 2009scape (no Darkan version).

Ref GregHib/void-private#262
An evil incarnation of Bob the cat teleports the player to the ScapeRune
island and demands fish. A terrified servant pans the camera to the
fishing spot Bob likes; the player nets there (the fish comes out already
cooked), uncooks it at the cold fire, and serves the raw fish to Bob. The
right fish sends him to sleep and opens the exit portal for a 650 Fishing
or Magic XP reward; a wrong one makes Bob more attentive so an extra
correct fish is needed.

Ref GregHib/void-private#262
Odd One Out on interface 191: pick the golden model that doesn't belong; four correct wins a choice of 1000 coins or a random item. Ported from 2009scape. Ref GregHib/void-private#260
Interface 191's models are non-clickable in the cache, so send the access mask on open; without it the interface has no way to answer.
The clickable answer buttons are the invisible components 3-5 (setting=1 in the cache), not the display models on 6-8. Target those instead of trying to unlock the non-clickable model components.
Interface 191's setting=1 buttons deliver clicks as dialogue-continue events (like choice() options), not interface options. Suspend each question on pauseInt and resume it from a continueDialogue handler keyed to the button clicked.
A guard arrests the player into a pillory; unlock it (interface 189) by picking the swinging key matching the spinning lock, N-in-a-row (starts 3, +1 and resets on a wrong pick, cap 6). Escaping rewards a roll of the shared certer table. Ported from 2009scape; object/tile ids remapped for the 634 cache (cage 777, city pillory tiles). Ref GregHib/void-private#268
A guard arrests the player into a pillory; unlock it (interface 189) by picking the swinging key matching the spinning lock, N-in-a-row (starts 3, +1 and resets on a wrong pick, cap 6). Escaping rewards a roll of the shared certer table. Ported from 2009scape; object/tile ids remapped for the 634 cache (cage 777, city pillory tiles). Ref GregHib/void-private#268
The Mysterious Old Man teleports the player into Mr Mordaut's classroom; talking to Mordaut opens a 'what comes next?' pattern quiz (interface 103) - pick the item that completes the three shown. Three correct passes the exam and Mordaut names one of four coloured doors; leaving through it returns the player with a Book of Knowledge (an XP lamp). Ported from 2009scape. Ref GregHib/void-private#263

Also relax the flaky Pillory arrest assertion to allow landing beside a cage when the stocks block the tile.
Interface 103 opens in the chatbox, so answer clicks arrive as dialogue-continue events (like Quiz Master's 191), not interface options. Open it as a dialogue_box, suspend each question on pauseInt, and resume from a continueDialogue handler keyed to the option clicked.
Interface 103 is a centred main-window modal, not a chatbox dialogue; set type=main_screen and take answers as interface options again.
Interface 103's answer icons are enabled in the cache but the client won't report a click until the server sends the interface settings; unlock them on open.
Send the interface settings for slot 0..0 of each answer component so the client enables the click.
getInterfaceItem tried to resolve the clicked answer icon against an inventory the display-only interface doesn't have, so the click was silently dropped. Allowlist surprise_exam_pattern like the other display-only item interfaces (farming_equipment_store, common_item_costs).
getInterfaceItem tried to resolve the clicked answer icon against an inventory the display-only interface doesn't have, so the click was silently dropped. Allowlist surprise_exam_pattern like the other display-only item interfaces (farming_equipment_store, common_item_costs).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants