WIP: Add random events (framework + events)#1064
Open
HarleyGilpin wants to merge 44 commits into
Open
Conversation
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.
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.
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.
GregHib
reviewed
Jul 8, 2026
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 | ||
| } |
Owner
There was a problem hiding this comment.
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) | ||
| } | ||
| } |
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
content/activity/event/random/RandomEvents): weightedpick()over therandom_eventstable,start/complete/fail, persisted per-player cooldown, andnoteAndTeleport(ignore penalty — notes the inventory then exiles the player so an event can't be macroed through).RandomEventTrigger): rolls per XP drop (1-in-N), gated by aneligible()check (no bots, combat, instances, dialogue, orno_random_eventsareas), plus a::randomevent [event]admin command.RandomEventKidnap): relog resume, teleport-out blocking, thekidnap(tile)helper, and themysteriousOldMan()intro. Two supported patterns:InPlaceRandomEvent) — the event NPC spawns beside the player, follows and nags, and applies the ignore penalty on timeout (Certer, Sandwich Lady, Evil Twin...).rewardCostumeOrCoins(collect-the-set: camo, lederhosen).Events
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::randomeventfor a random pick). Current:maze,freaky_forester.