Party Phase Zone: The Nerd Part: Part 2

Note

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 pixels
    • forceUppercase which forces all text sent with this font to always be uppercase
    • dimfunc for more advanced separation between glyphs
  • func and predrawfunc properties in WTXT.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.

Party Phase Zone's thumbnail

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
todo

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 for P_FlashPal as that doesn't work for some reason

Scripts


  • socks.lua - SOC sucks
  • druid.lua - The MYSERIOUS 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.

Code block "filenames"

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.