Party Phase Zone: The Nerd Part: Part 2
This post is not in character with other posts in this site, as to keep everything simple and not having to sugarcoat it behind a persona.
Some notes from Paya may appear as a separate callout, however.
In comparison to my other lua mods, this is not the most original or impressive. I consider it an evolution of a past mod I made for SRB2Kart, named ukef
. It was just 'yet another random effect' mod which has already been done a billion times over.
The biggest innovation I made on this formula is that it's directly baked into the map design, and therefore specific things are tweaked in-map to keep it sane and atleast fun.
Now, enough stalling, let's get on with the nerd info.
Libraries
To reduce my chance of going insane, I decided to use/code separate libraries for use in the main Lua script. These are basically the backbone of the project.
TextWriter2
Internally known as WTXT2
, TextWriter2 is an edit of amperbee's work in progress TextWriter, which draws, well, text. I've added a handful of new features to streamline the process in the main Lua script as well as another library right after this that has this as a dependency.
TextWriter2 adds:
- Various new font properties
xsepwidth
which sets the separation between glyphs in pixelsforceUppercase
which forces all text sent with this font to always be uppercasedimfunc
for more advanced separation between glyphs
func
andpredrawfunc
properties inWTXT.writeText
for manipulating drawing data before drawing the glyph to the screen- Inline bold fonts via the
boldFont
draw data property, toggled via the\xC0
control character - Ring Racers' tag system in place of traditional control codes for setting text colors and custom TextWriter2 specific control codes
<bold>This text is bold!<bold> This isn't.
- Bugfixes for numerous Lua warnings
I probably could have switched to a less buggy string drawer library like amperbee's previous string drawer, Ultimate Draw String. Oh well.
ZDialogue
Made by me, this Lua+ACS library is a direct replacement for the Dialogue_
functions.
Currently at version 1, only supporting a specific font set and style, though I will add support for custom dialogue box styles in the future.

Fig 1. ZDialogue demonstation.
This is used for the secret gallery in Party Phase, shown below.
Spoiler alert!
Paya here, this ol' area is usually off-limits! Please do not tell anyone you've been here in anyway.
Secrets should remain secrets.
These are generally meant for ACS, shown below, for example.
script "PayaHeavenIntro" (void) { Music_Dim(TICRATE, -1); Thing_Teleport(22, 6); Player_ToggleControl(false); Delay(2*TICRATE); ZDialogue_SetSpeaker("Paya", "", "None", "pyal"); ZDialogue_NewDismissText("\n\n\n Welcome,<wait> to <bold>Heaven.<bold>"); Player_ToggleControl(true); }
HAYA_PartyPhaseTake2.wad/SCRIPTS
documentation. seriously
bosshealth.lua
Yes this doesn't have a proper name yet. All this script does is add, well, boss health bars.
if P_RandomRange(0, 3) == 3 then --print("haha") local brak = P_SpawnMobjFromMobj(coffin, 0, 0, 0, MT_CYBRAKDEMON) brak.scale = $*2 brak.target = coffin.target -- Spawns a health bar, with the target mobj, max health, number of bars, and the name itself PPZ_InitHealthBar(brak, 12, 2, "BRAK EGGMAN") if Music_Play and coffin.target and coffin.target.player and coffin.target.player == consoleplayer then Music_Remap("druid", "VSBRAK") Music_Play("druid") end if coffin.target.player then brak.punt = coffin.target.player -- it can switch targets, but always reference the original! end P_KillMobj(coffin.tracer) return end
druid.lua
The way to remove a boss health bar is via the mobj being removed from the level or if hbdeferDie
is set on the mobj.
if (mobj.health <= 0 or mobj.state == mobj.info.deathstate) then mobj.hbthreshold = $ or 0 mobj.hbthreshold = $+1 if mobj.hbthreshold > TICRATE*3 then -- do not do shit further mobj.hbdeferDie = true end end
brakthink.lua
This might see a message board release, if people are interested.
Lightning round
Other libraries/scripts that do something pretty minor or not of upmost importance.
Libraries
quakecsay.lua
- Simulates Quake's "fucking text on your screen".... thingy. Used for the secret gallery.flashbang.lua
- Flashbang! A replacement forP_FlashPal
as that doesn't work for some reason
Scripts
socks.lua
- SOC sucksdruid.lua
- TheMYSERIOUS FLOATING CUBE (& CONE)
boss, as well as the "coffin". Contrary to the name, this also handles spawning Brak Eggman via the ??? monitor random event.brakdef.lua
- Defines ALL of Brak Eggman's objects, states, and sprites.brakthink.lua
- Handles miscellaneous things that state actions don't do directly.
party.lua
The Lua script that handles the party. Quite simple ain't it? Wrong.
Since all of the code here comes from one file (party.lua
), The code block filenames will either add unnecessary descriptions to them, or a snarky remark.
The space object
Having a doomednum
of 29666
, this is the object that gives you the effects when you step on it.
On spawn, it sets a couple of variables, though it's really not particular since lap changes reroll all the spaces anyways.
-- Spawn, dammit addHook("MapThingSpawn", function(mo, mt) -- Adjust scale mo.scale = $*4 -- Set space information mo.sprite = SPR_PZSP mo.frame = E local spacetype = mt.args[0] if spacetype > SPACE_NONE and spacetype <= SPACE_CHANCE then mo.frame = spacetype-1 mo.extravalue1 = spacetype end mo.frame = $ | FF_FULLBRIGHT | FF_FLOORSPRITE mo.renderflags = $ | RF_SLOPESPLAT | RF_NOSPLATBILLBOARD -- inactive thingy mo.target = P_SpawnMobjFromMobj(mo, 0, 0, 0, MT_OVERLAY) mo.target.target = mo mo.target.sprite = SPR_PZSP mo.target.frame = E | FF_FULLBRIGHT | FF_FLOORSPRITE mo.target.renderflags = mo.renderflags | RF_SUBTRACT | (RF_DONTDRAWP1|RF_DONTDRAWP2|RF_DONTDRAWP3|RF_DONTDRAWP4) mo.target.scale = mo.scale -- flash lmao mo.tracer = P_SpawnMobjFromMobj(mo, 0, 0, 0, MT_OVERLAY) mo.tracer.target = mo mo.tracer.sprite = SPR_PZSP mo.tracer.frame = F | FF_FULLBRIGHT | FF_FLOORSPRITE mo.tracer.renderflags = mo.renderflags | RF_DONTDRAW | RF_REDUCEVFX mo.tracer.scale = mo.scale -- shit fix if P_IsObjectOnGround(mo) then copyslope(mo, mo.subsector.sector.f_slope) copyslope(mo.target, mo.subsector.sector.f_slope) copyslope(mo.tracer, mo.subsector.sector.f_slope) end -- players targetted by this shit mo.thresholdlist = {} --mo.tracerlist = {} end, MT_PPZSPACE)
copyslope()⠀missing⠀in⠀action
This sets everything a space needs, like its size, default type, render flags, and its slope since Ring Racers didn't want to fucking cooperate with me and set the slope of these automatically.
Upon touching the space, it will continuously run a hook passing in the player who triggered it and the space object itself.
addHook("TouchSpecial", function(special, toucher) -- KILL YOURSELF if not (toucher.player and toucher.player.valid) then return true end
don't⠀question⠀my⠀comments
Before anything, the hook checks if the space is disabled.
local p = toucher.player if p.ppzGimmickTime > 0 then return true end if special.thresholdlist[#p] then return true end -- no cheesing on a single space you moron if special.cvmem then return true end -- currently rerolling, do nothing if special.cusval then return true end -- manually disabled
nothing⠀here.
Right after this, we can set our (global) space time cooldown, and save the last space for a hud message.
-- check space type local spacetype = special.extravalue1 if spacetype == SPACE_NONE then return true end -- fucking NOTHING -- actual space, so add gimmick time p.ppzGimmickTime = TICRATE*3 p.ppzLastSpace = spacetype
oh⠀hey⠀we⠀can⠀actually⠀get⠀started
Both Ring Gain
and Ring Drain
are easy to explain, so no need to show code for that, however I will show how the game chooses events for random space and chance time.
p.ppzEventName = events[P_RandomRange(1, #events)](p) if g_miscStuff.double then -- ADD: This is set by an event. p.ppzEventName = events[P_RandomRange(1, #events)](p) g_miscStuff.double = false end
Doubling⠀the⠀next⠀random⠀space⠀is⠀an⠀event,⠀yes.
Random events are essentially a list of functions in one massive table. Here's an example featuring the ironman
effect, which randomizes your current skin.
function(p) -- forced ironman local maxskins = 0 p.ppzLastSkin = $ or 0 local usable = {} for i=0,#skins do if not (skins[i] and skins[i].valid) then continue end if i == p.ppzLastSkin then continue end -- commented out for ultiate fun. -- if (skins[i].flags & SF_IRONMAN) then continue end table.insert(usable, { name = skins[i].name, speed = skins[i].kartspeed, weight = skins[i].kartweight, }) end local s = usable[P_RandomRange(1, #usable)] p.mo.skin = s.name p.kartspeed = s.speed p.kartweight = s.weight -- add weight if possible p.kartweight = $ + p.ppzAddWeight -- handle magician box S_StartSound(p.mo, sfx_kc33) S_StartSound(p.mo, sfx_cdfm44) local parent = p.mo local baseangle = P_RandomRange(0, 359) local j = 0 for j=0,5 do local box = P_SpawnMobjFromMobj(parent, 0, 0, 0, MT_MAGICIANBOX) box.target = parent box.angle = FixedAngle((baseangle + j*90)*FRACUNIT) box.flags2 = $ | MF2_AMBUSH box.renderflags = $ | parent.renderflags box.extravalue1 = 10 box.extravalue2 = 5*TICRATE/4 box.cusval = (j == 5) and 1 or 0 if j > 3 then -- >state 'S_MAGICIANBOX_TOP' does not exist. -- >ok box.state = (j == 4) and $+1 or $+2 box.renderflags = $ | RF_NOSPLATBILLBOARD box.angle = FixedAngle(baseangle*FRACUNIT) end end K_SpawnMagicianParticles(parent, 10) return "jironman (Change character, voices unchanged.)" end,
I'm⠀not⠀gonna⠀show⠀the⠀whole⠀table,⠀moron.
The event takes in the player object who triggered the event, and returns a description string of what happened to that player.
There is also no weighting at all on these. Why would there be? That would complicate things and make shit boring so I decided to not do that.
Apart from that, nothing too special, as a random event only runs once, unless it sets a specific player variable, which we'll come back to later.
But Haya.... what about the chance time spaces??????
shut the fuck up here it is
if g_miscStuff.pcount >= 3 then p.ppzEventName = "Uh oh." g_chanceTime.active = true g_chanceTime.hud_active = true resetchancetime() g_chanceTime.state = 1 g_chanceTime.hud_trans[g_chanceTime.state] = 0 g_chanceTime.interoggator = p -- fill victim 1 list first ofc for q in players.iterate do if q.spectator then continue end if not (q.mo and q.mo.valid) then continue end -- Yes, the player interoggating is not included. -- When in doubt, blame the person who initiated a chance time. -- Their fault, technically. if q == p then continue end table.insert(g_chanceTime.victim1list, q) end L_Shuffle(g_chanceTime.victim1list) g_chanceTime.curLen = #g_chanceTime.victim1list lookingforatag = ACS_DISABLECTS
chance⠀time⠀to⠀screw⠀you⠀over
This sets chance time as active, creates the first victim list, and calls ACS to disable all chance time spaces. If there's less than three people well... it just acts like a normal random space.
Chance Time (for real)
Chance Time is really just chance time. It runs on a PlayerThink
hook, meaning it pauses during hitlag, I'm keeping that because it's probably too late to change it already.
-- handle chance time if not g_chanceTime.active then return end -- we are the interoggator -- we get... to choose. local mo = p.mo local cmd = p.cmd p.ppzLastCTState = $ or CHANCESTATE_NONE p.ppzCTRandomOffset = $ or 0
chance⠀time⠀to⠀screw⠀you⠀over⠀part⠀II
The next logical step is to well.. check for player input. Every time it does, the current roulette pauses, saves the index that was selected, and proceeds to the next state.
local confirm = false if g_chanceTime.elapsed > TICRATE then if g_chanceTime.elapsed > TICRATE<<4 then confirm = true elseif ((p.bot or p.exiting) and g_chanceTime.elapsed > (TICRATE*3+p.ppzCTRandomOffset)) then confirm = true else -- wtf? confirm = (!(!(cmd.buttons & BT_ATTACK))) end end
chance⠀time⠀to⠀screw⠀you⠀over⠀part⠀III
The roulette is actually similar to how vanilla handles the item and ring box roulettes. Just with less the balance.
Once we reach the last state, chance time officially ends and has a cooldown of 3 seconds before anyone can use another chance time space again.
elseif g_chanceTime.state == CHANCESTATE_PICKINGV2 then g_chanceTime.victim2 = g_chanceTime.victim2list[g_chanceTime.idx+1] local f, cond = unpack(chancetime_funcs[g_chanceTime.type]) -- finally, run the function --print("ran type! "..tostring(g_chanceTime.type)) g_chanceTime.desc = f(g_chanceTime.victim1, g_chanceTime.victim2, g_chanceTime.rule) g_chanceTime.desctime = TICRATE*3 -- disable this now... g_chanceTime.active = false lookingforatag = ACS_ENABLECTS end
chance⠀time⠀to⠀screw⠀you⠀over⠀part⠀III
The table of functions is similar to the random space, though the type selected is from a list of presets.
Come to think of it, I skipped that didn't I?
for k, v in ipairs(ct_sets) do if (v[4]() == false) then continue end table.insert( g_chanceTime.sets, { rule = v[2], type = v[1], prefix = v[3], } ) end
chance⠀time⠀to⠀screw⠀you⠀over⠀part⠀II.5
local set = g_chanceTime.sets[g_chanceTime.typeidx+1] g_chanceTime.rule = set.rule g_chanceTime.type = set.type
chance⠀time⠀to⠀screw⠀you⠀over⠀part⠀IV
The list of functions for each type are once again, another big ass table. Here's the Die
chance time type.
[CHANCE_DIE] = {function(v1, v2, r) local mo1 = v1.mo local mo2 = v2.mo -- BOTH DIE P_KillMobj(mo2, nil, nil) P_KillMobj(mo1, nil, nil) return fmt("Both %s and %s fucking die", v1.name, v2.name) end, do return true end},
chance⠀time⠀to⠀screw⠀you⠀over⠀part⠀V
Takes in the first victim, the second victim, and the current rule, ranging from 1-3, with 'To Victim #1', 'To Victim #2', and 'To Both' respectively. The rule doesn't matter here however, since this is only meant to run with the 3rd rule only.
....which introduces us to sets. A 'set' defines what the second roulette can choose.
-- Hardcoded sets of types -- {type, rule, graphic, condition} local ct_sets = { {CHANCE_RINGS, CHANCERULE_LEFT, "P_RG%d", do return (gametyperules & GTR_SPHERES) ~= GTR_SPHERES end}, {CHANCE_RINGS, CHANCERULE_RIGHT, "P_RG%d", do return (gametyperules & GTR_SPHERES) ~= GTR_SPHERES end}, {CHANCE_RINGS, CHANCERULE_SWBH, "P_RG%d", do return (gametyperules & GTR_SPHERES) ~= GTR_SPHERES end}, {CHANCE_POSITION, CHANCERULE_SWBH, "P_POS3", do return true end}, {CHANCE_DIE, CHANCERULE_SWBH, "P_DIE3", do return true end}, {CHANCE_ITEMS, CHANCERULE_SWBH, "P_ITEM3", do return true end}, {CHANCE_POINTS, CHANCERULE_LEFT, "P_BNS%d", do return (gametyperules & GTR_BUMPERS) == GTR_BUMPERS end}, {CHANCE_POINTS, CHANCERULE_RIGHT, "P_BNS%d", do return (gametyperules & GTR_BUMPERS) == GTR_BUMPERS end}, {CHANCE_POINTS, CHANCERULE_SWBH, "P_BNS%d", do return (gametyperules & GTR_BUMPERS) == GTR_BUMPERS end}, {CHANCE_BUMPERS, CHANCERULE_LEFT, "P_BMPR%d", do return (gametyperules & GTR_BUMPERS) == GTR_BUMPERS end}, {CHANCE_BUMPERS, CHANCERULE_RIGHT, "P_BMPR%d", do return (gametyperules & GTR_BUMPERS) == GTR_BUMPERS end}, {CHANCE_BUMPERS, CHANCERULE_SWBH, "P_BMPR%d", do return (gametyperules & GTR_BUMPERS) == GTR_BUMPERS end}, {CHANCE_SPHERES, CHANCERULE_LEFT, "P_SPRE%d", do return (gametyperules & GTR_BUMPERS) == GTR_BUMPERS end}, {CHANCE_SPHERES, CHANCERULE_RIGHT, "P_SPRE%d", do return (gametyperules & GTR_SPHERES) == GTR_SPHERES end}, {CHANCE_SPHERES, CHANCERULE_SWBH, "P_SPRE%d", do return (gametyperules & GTR_SPHERES) == GTR_SPHERES end}, }
sets.
The comment should explain the what-should-be contents of this table.
HUD
no.
Conclusion
This probably would have only took me a month to make if I didn't procrastinate. Haya out.