├── .gitignore ├── .gitattributes ├── game ├── Axis2D │ ├── Axis2D_Example.wad │ ├── Axis2D_UDMF_Example.wad │ ├── Axis2D_README.txt │ └── l_Axis2D-UDMF.lua ├── l_mapzones.lua ├── customsave_io.lua ├── l_AnimColors.lua └── l_EventStepThinker.lua ├── Examples └── SpriteGroupExample.wad ├── other ├── mapheader_helpers.lua ├── deepcopy_nonrecursive.lua ├── math_extended.lua ├── FL_Table.Lua ├── l_scrollobject.lua ├── l_linepathing.lua └── linkedList.lua ├── mobj ├── mobjtimescale.lua ├── mobj_extended.lua ├── l_parabola.lua ├── l_pickrandommobj.lua ├── l_radiusexplode.lua ├── l_SoundAmbience.lua ├── l_spritemdl.lua ├── l_frameanimate.lua ├── l_atmosparticle.lua └── l_mobjmover.lua ├── README.md ├── player ├── l_setgoalclear.lua ├── l_viewpoint_nonclient.lua └── l_ButtonState.lua └── hud ├── l_worldtoscreen.lua ├── l_FadeScreen.lua ├── l_supertextfont.lua ├── l_hudzordering.lua ├── l_hitdisplay.lua ├── l_fontlib.lua ├── l_textboxes.lua └── l_textboxesv2.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | *.exe 3 | *.dbs 4 | *.wad.* 5 | untracked/ 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /game/Axis2D/Axis2D_Example.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sprkizard/srb2-lua-lib/HEAD/game/Axis2D/Axis2D_Example.wad -------------------------------------------------------------------------------- /Examples/SpriteGroupExample.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sprkizard/srb2-lua-lib/HEAD/Examples/SpriteGroupExample.wad -------------------------------------------------------------------------------- /game/Axis2D/Axis2D_UDMF_Example.wad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sprkizard/srb2-lua-lib/HEAD/game/Axis2D/Axis2D_UDMF_Example.wad -------------------------------------------------------------------------------- /other/mapheader_helpers.lua: -------------------------------------------------------------------------------- 1 | 2 | -- A wrapper that automatically converts a lua mapheader string into an integer, 3 | -- with a default value as a callback 4 | rawset(_G, "M_GetMapLuaInt", function(luastr, defaultvalue) 5 | if not luastr then 6 | return defaultvalue 7 | else 8 | return tonumber(luastr) 9 | end 10 | end) 11 | -------------------------------------------------------------------------------- /mobj/mobjtimescale.lua: -------------------------------------------------------------------------------- 1 | -- TODO: turn accidental Timescale into a working functional thing to use 2 | 3 | rawset(_G, "motimescale", 1) 4 | 5 | COM_AddCommand("timescale", function(t) 6 | motimescale = t 7 | end) 8 | 9 | 10 | -- Someone will find this useful some day 11 | addHook("MobjThinker", function(mo) 12 | if (leveltime % motimescale) then 13 | return true 14 | else 15 | return false 16 | end 17 | end) -------------------------------------------------------------------------------- /mobj/mobj_extended.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | * mobj_extended.lua 4 | * (sprkizard) 5 | * (May 16, 2020 00:23) 6 | 7 | * Desc: More functions for mobj related things 8 | 9 | * Usage: 10 | 11 | ]] 12 | 13 | -- Simple function to shoot a line of objects in the direction the mobj is facing for scripting reference purposes 14 | local function P_DrawFacingLine(source, color) 15 | 16 | local facingangle = P_SpawnMobj(source.x, source.y, source.z, MT_THOK) 17 | facingangle.tics = 6 18 | facingangle.scale = FRACUNIT/3 19 | facingangle.color = color 20 | facingangle.momz = source.momz 21 | P_InstaThrust(facingangle, source.angle, 32*FRACUNIT) 22 | end 23 | 24 | rawset(_G, "P_DrawFacingLine", P_DrawFacingLine) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lua Resource Library (for SRB2 2.2.x) 2 | 3 | A repository of Lua snippets and scripts for SRB2 modders to use in their projects. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ## To Contribute: 12 | _For contribution, please use comments in your script where you can (help people know what they are working with!), and use the following header template at the start of your file:_ 13 | 14 | ```Lua 15 | --[[ 16 | * scriptname.lua 17 | * (Author: x) (Additional Users) 18 | * (June 1, 2020 00:00) 19 | * Desc: A description for what my Lua script does 20 | * with references included if needed 21 | * 22 | * Notes: Example usage - P_TeleportMove(player, x, y, z) 23 | * with a url to the wiki if needed 24 | ]] 25 | ``` 26 | 27 | *Remember to also include proper attribution where needed!* 28 | -------------------------------------------------------------------------------- /player/l_setgoalclear.lua: -------------------------------------------------------------------------------- 1 | -- Forces a sign post goal clear almost anywhere the function is called where spawnpoint is valid 2 | rawset(_G, "P_SetGoalClear", function (source, spawnorigin, playerMobj) 3 | 4 | local special = source 5 | local toucher = playerMobj 6 | --P_RadiusExplode(source, 32, MT_EXPLODE) 7 | 8 | if not (special.DummyGoal and special.DummyGoal.valid) then 9 | special.DummyGoal = P_SpawnMobj(special.x, special.y, special.z, MT_SIGN) 10 | special.DummyGoal.spawnpoint = spawnorigin 11 | special.DummyGoal.state = S_SIGNSPIN1 12 | special.DummyGoal.momz = 8*FRACUNIT 13 | 14 | S_StartSound(thing, special.DummyGoal.info.seesound) 15 | 16 | special.DummyGoal.flags2 = $1|MF2_DONTDRAW 17 | special.DummyGoal.tracer.flags2 = $1|MF2_DONTDRAW 18 | special.DummyGoal.tracer.tracer.flags2 = $1|MF2_DONTDRAW 19 | end 20 | 21 | -- Do the magic clearing stuff here now 22 | toucher.target = special.DummyGoal 23 | P_DoPlayerExit(toucher.player) 24 | end) 25 | -------------------------------------------------------------------------------- /mobj/l_parabola.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_parabola.lua 3 | * (sprkizard, fickleheart, Nev3r) 4 | * (May 11, 2020 15:13) 5 | * Desc: Parabolic trajectory drawing, and launching 6 | 7 | * Usage: 8 | 9 | 10 | 11 | 12 | ]] 13 | 14 | 15 | 16 | 17 | local function P_DrawArc(source, distance, thrust) 18 | --(a/2) * (x^2) + b*x + c 19 | for i=1,64 do 20 | local zt = (thrust + gravity/4) * i - (gravity/2)*i^2 21 | local arc = P_SpawnMobj( 22 | source.x+cos(source.angle)*i*distance, 23 | source.y+sin(source.angle)*i*distance, 24 | source.z+(source.height+8*FRACUNIT)+zt, 25 | MT_THOK) 26 | 27 | arc.scale = FRACUNIT/6 28 | arc.color = SKINCOLOR_RED 29 | arc.tics = 2 30 | --arc.flags = $1&~MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY 31 | end 32 | end 33 | 34 | local function P_ArcThrow(source, distance, thrust, mobjtype) 35 | 36 | local arc = P_SpawnMobj(source.x, source.y, source.z+(source.height+8*FRACUNIT), mobjtype) 37 | 38 | arc.momz = thrust 39 | P_InstaThrust(arc, source.angle, distance*FRACUNIT) 40 | --arc.tics = 444 arc.fuse = 353 41 | arc.flags = $1&~MF_NOGRAVITY 42 | 43 | end 44 | 45 | 46 | rawset(_G, "P_DrawArc", P_DrawArc) 47 | rawset(_G, "P_ArcThrow", P_ArcThrow) 48 | 49 | -------------------------------------------------------------------------------- /hud/l_worldtoscreen.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_worldtoscreen.lua 3 | * (sprkizard) 4 | * (‎Aug 19, ‎2021, ‏‎22:51:56) 5 | * Desc: WIP 6 | 7 | * Usage: TODO 8 | ]] 9 | 10 | local function R_WorldToScreen2(p, cam, target) 11 | 12 | -- local sx = cam.angle - R_PointToAngle2(p.mo.x, p.mo.y, target.x, target.y) 13 | local sx = cam.angle - R_PointToAngle(target.x, target.y) 14 | local visible = false 15 | 16 | -- Get the h distance from the target 17 | local hdist = R_PointToDist(target.x, target.y) 18 | -- print(AngleFixed(sx)/FU ) 19 | if sx > ANGLE_90 or sx < ANGLE_270 then 20 | -- sx = 0 -- return {x=0, y=0, scale=0} 21 | visible = false 22 | else 23 | sx = FixedMul(160*FU, tan($1)) + 160*FU 24 | visible = true 25 | end 26 | 27 | -- local sx = 160*FU + (160 * tan(cam.angle - R_PointToAngle(target.x, target.y))) 28 | -- local sy = 100*FU + (100 * (tan(cam.aiming) - FixedDiv(target.z, hdist))) 29 | local sy = 100*FU + 160 * (tan(cam.aiming) - FixedDiv(target.z-cam.z, 1 + FixedMul(hdist, cos(cam.angle - R_PointToAngle(target.x, target.y))) )) 30 | 31 | -- local c = cos(p.viewrollangle) 32 | -- local s = sin(p.viewrollangle) 33 | -- sx = $1+FixedMul(c, target.x) + FixedMul(s, target.y) 34 | -- sy = $1+FixedMul(c, target.y) - FixedMul(s, target.x) 35 | 36 | local ss = FixedDiv(160*FU, hdist) 37 | 38 | return {x=sx, y=sy, scale=ss, onscreen=visible} 39 | end 40 | 41 | rawset(_G, "R_WorldToScreen2", R_WorldToScreen2) 42 | 43 | 44 | -------------------------------------------------------------------------------- /mobj/l_pickrandommobj.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Selects a player at random, and returns the said player 3 | local function P_PickRandomPlayer(playertable) 4 | 5 | local playerlist = playertable or {} 6 | 7 | -- TODO: see if using a for statement with MAXPLAYERS as a limit is better than players.iterate 8 | if not playertable then 9 | for player in players.iterate do 10 | table.insert(playerlist, player) 11 | end 12 | end 13 | 14 | local randomTarget = P_RandomChoice(playerlist) 15 | 16 | if (randomTarget.mo.valid) then 17 | return randomTarget.mo 18 | else 19 | -- Rerun this exact function 20 | --PickRandomTargetPlayer() 21 | end 22 | 23 | end 24 | 25 | -- Selects a mobj at random, and returns the said mobj 26 | local function P_PickRandomMobj(sourcemo, mobjtable) 27 | 28 | local mobjlist = mobjtable or {} 29 | 30 | for mobj in mobjs.iterate() do 31 | -- TODO: exclude mt push and pull 32 | if (mobj == sourcemo) then continue end 33 | table.insert(mobjlist, mobj) 34 | end 35 | 36 | local randomTarget = P_RandomChoice(mobjlist) 37 | 38 | if (randomTarget and randomTarget.valid) then 39 | return randomTarget 40 | else 41 | -- Rerun this exact function 42 | --P_PickRandomTargetPlayer() 43 | end 44 | 45 | end 46 | 47 | 48 | 49 | rawset(_G, "P_PickRandomPlayer", P_PickRandomPlayer) 50 | rawset(_G, "P_PickRandomMobj", P_PickRandomMobj) 51 | -------------------------------------------------------------------------------- /mobj/l_radiusexplode.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_radiusexplode.lua 3 | * (sprkizard) 4 | * (Feburary 3, 2020 23:59) 5 | * Desc: A function that applies an exploding particle force on 6 | the center of an object 7 | 8 | * Usage: P_RadiusExplode(source, speed, particleType, params) 9 | ------ 10 | source will also accept table values of {x, y, z} as long as valid = true 11 | is an entry inside the table (an example is in this file) 12 | 13 | Table arguments: 14 | ({skip = 32}) - the amount of angles in the radius to skip past, defaulting to 32 15 | 16 | ]] 17 | 18 | local function P_RadiusExplode(position, wavespeed, args) 19 | 20 | -- Load extra parameters for further customization if needed 21 | local angleskip = (args and args.angleskip) or 32 22 | local mobjtype = (args and args.mobjtype or MT_EXPLODE) 23 | 24 | -- if position is a mobj and is not valid, do nothing 25 | if type(position) == "userdata" and not (position and position.valid) then return end 26 | 27 | for angle=1, 360, angleskip do 28 | 29 | local explode = P_SpawnMobj(position.x, position.y, position.z, mobjtype) 30 | 31 | explode.scale = (args and args.scale) or FU 32 | 33 | -- Set a fuse for long lasting objects 34 | if (args and args.fuse) then 35 | explode.fuse = args.fuse 36 | end 37 | 38 | -- Call a callback function 39 | if (args and args.callback) then 40 | do args.callback(explode, angle, angleskip) end 41 | end 42 | 43 | -- Force the objects outward 44 | P_InstaThrust(explode, FixedAngle(angle*FU), wavespeed) 45 | end 46 | end 47 | 48 | 49 | 50 | rawset(_G, "P_RadiusExplode", P_RadiusExplode) 51 | -------------------------------------------------------------------------------- /player/l_viewpoint_nonclient.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_viewpoint_nonclient.lua 3 | * (Author: sprkizard) (Nev3r) 4 | * (July 12, 2021 18:04) 5 | * Desc: A hack that exposes viewpointswitch spectating information to all players 6 | * 7 | * Notes: 8 | ]] 9 | 10 | rawset(_G, "spectating", setmetatable({}, { 11 | __index = function(t, k) 12 | return #consoleplayer -- returns yourself when empty 13 | end 14 | })) 15 | 16 | rawset(_G, "spectators", {}) 17 | 18 | -- Gets how many players are watching the specified player 19 | spectators.watching = function(p) 20 | local count = 0 21 | 22 | for i=0,#spectating do 23 | (function() 24 | if (#p == i) then return end 25 | if (spectating[i] == #p) then count = $1+1 end 26 | end)() 27 | end 28 | 29 | return count 30 | end 31 | 32 | -- Add a command to update the spectators list 33 | COM_AddCommand("__update_spectating", function(player, arg1) 34 | spectating[#player] = tonumber(arg1) 35 | end) 36 | 37 | local function think_updatespectating() 38 | for player in players.iterate do 39 | (function() 40 | if not player and player.__spectating then return end 41 | COM_BufInsertText(player, "__update_spectating " .. tostring(player.__spectating.nextviewedplayer)) 42 | end)() 43 | end 44 | end 45 | 46 | -- Setup the nextviewedplayer variable 47 | addHook("PlayerSpawn", function(player) 48 | player.__spectating = {nextviewedplayer = #player} 49 | end) 50 | 51 | -- When the player spectates another player, reference them privately 52 | addHook("ViewpointSwitch", function(switcher, nextviewed) 53 | if not switcher and switcher.__spectating then return end 54 | switcher.__spectating.nextviewedplayer = #nextviewed 55 | end) 56 | 57 | -- Remove player from the spectating list if they leave 58 | addHook("PlayerQuit", function(player) 59 | spectating[#player] = nil 60 | end) 61 | 62 | -- Update spectators list 63 | addHook("ThinkFrame", function() 64 | think_updatespectating() 65 | end) 66 | -------------------------------------------------------------------------------- /player/l_ButtonState.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | * l_ButtonState.lua 4 | * (fickleheart) 5 | * (December 16, 2014) 6 | 7 | * Version: 1.2 8 | 9 | * Desc: Button state management - Ever want an easy way to 10 | know whether a button's been tapped, held, released, 11 | for how long? Well, here you go. player.buttonstate[blah] 12 | returns x frames being held, or -x frames being released 13 | 14 | ]] 15 | 16 | local buttonstate_dupeprevent = false 17 | 18 | addHook("PlayerThink", function(player) 19 | 20 | local mo = player.mo 21 | 22 | -- Avoid processing buttonstate multiple times per frame if multiple loaded addons have this code snippet in them! 23 | if player.buttonstate and not buttonstate_dupeprevent then return end 24 | buttonstate_dupeprevent = true 25 | 26 | 27 | if not player.buttonstate then player.buttonstate = {} end 28 | local bs = player.buttonstate 29 | 30 | local function state(cond, key) 31 | if cond then 32 | bs[key] = max(($1 or 0)+1, 1) 33 | else 34 | bs[key] = min(($1 or 0)-1, -1) 35 | end 36 | end 37 | 38 | -- Handle normal buttons - get these with player.buttonstate[BT_WHATEVER] 39 | for _,v in ipairs({ 40 | BT_JUMP, BT_USE, BT_ATTACK, BT_FIRENORMAL, 41 | BT_CAMLEFT, BT_CAMRIGHT, 42 | BT_WEAPONNEXT, BT_WEAPONPREV, BT_TOSSFLAG, 43 | BT_CUSTOM1, BT_CUSTOM2, BT_CUSTOM3 44 | }) do 45 | state(player.cmd.buttons & v, v) 46 | end 47 | 48 | -- Handle weapon quick select buttons - get these with player.buttonstate[weaponnum] (can probably use WEP_WHATEVER+1?) 49 | for i = 1,BT_WEAPONMASK-1 do 50 | state(player.cmd.buttons & BT_WEAPONMASK == i, i) 51 | end 52 | 53 | -- Finally, handle directional taps (no analog support, soz) - get these with player.buttonstate["direction"] (up, down, left, right) 54 | for k,v in pairs({ 55 | up = function(p) return p.cmd.forwardmove > 0 end, 56 | down = function(p) return p.cmd.forwardmove < 0 end, 57 | left = function(p) return p.cmd.sidemove < 0 end, 58 | right = function(p) return p.cmd.sidemove > 0 end 59 | }) do 60 | state(v(player), k) 61 | end 62 | 63 | --if player.camdist == nil 64 | -- player.camdist = 240*FRACUNIT 65 | --end 66 | end) 67 | -- ============================================================================ 68 | 69 | 70 | -------------------------------------------------------------------------------- /other/deepcopy_nonrecursive.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | -- deepcopy_nonrecursive.lua, version v1.0.0 4 | * A non-recursive table deep copy implementation in Lua. 5 | (Tested working in SRB2 and vanilla Lua 5.1, 5.2, and 5.3) 6 | 7 | * Authors: Golden 8 | * Originally Released: December 5, 2020 06:03 CST 9 | 10 | -- Load this Lua: 11 | --- local deepcopy_nonrecursive = dofile("deepcopy_nonrecursive.lua") 12 | * (deepcopy_nonrecursive will also be automatically put in the global space if other methods of loading are desirable) 13 | 14 | -- Usage: 15 | --- deepcopy_nonrecursive(table): table 16 | * Returns a deep copy of the input table, respecting recursive table references, but without recursion. 17 | * Takes ~0.095 seconds on my machine to traverse a table (let's call it `t'..) 1962 levels deep, 18 | and ~0.1 seconds when input this structure: 19 | {t, {t}, {{t}}, {{{t}}}, ... more ... , {{{{{{{{{t}}}}}}}}}} 20 | ]] 21 | 22 | local function deepcopy_nonrecursive(t) 23 | -- Copying a non-table? Just return it to copy the value. 24 | if type(t) ~= "table" then 25 | return t 26 | end 27 | 28 | -- We're... 29 | local curtables = {t} -- ...copying this original table... 30 | local curcopies = {{}} -- ...to this new table... 31 | local curindex = 1 -- ...starting from the first index of curtables... 32 | 33 | local unprocessedtables = 1 -- ...with only the original table to process to start with. 34 | 35 | local copied = curcopies[1] -- Keep a reference to that table copy to return later. 36 | 37 | repeat -- Run at least once. 38 | local curtable, curcopy = curtables[curindex], curcopies[curindex] -- Some useful references. 39 | 40 | unprocessedtables = unprocessedtables - 1 -- Processed previous table. 41 | 42 | for k, v in pairs(curtable) do -- Iterate the current table 43 | if type(v) ~= "table" then -- Value not a table? 44 | curcopy[k] = v -- Automatically copied. 45 | else -- Is a table? 46 | local recursive_caught = false -- We haven't found a table in our collection yet 47 | 48 | for i, copy_v in ipairs(curtables) do -- Find out if we've seen this table before... 49 | if copy_v == v then -- These 2 original tables are the same table? 50 | curcopy[k] = curcopies[i] -- Then copy a reference to the previously copied version. This works even if it hasn't been processed yet. 51 | recursive_caught = true -- We've got ourselves a recursive table reference! 52 | break -- Don't waste time. 53 | end 54 | end 55 | 56 | if not recursive_caught then -- If this is actually a new table we haven't seen before... 57 | table.insert(curtables, v) -- Copy a reference to curtables 58 | table.insert(curcopies, {}) -- Create a new table to hold the copy 59 | curcopy[k] = curcopies[#curcopies] -- Create a reference to the pending copy 60 | 61 | unprocessedtables = unprocessedtables + 1 -- Another table to process... 62 | end 63 | end 64 | end 65 | 66 | curindex = curindex + 1 -- Advance to the next index 67 | until unprocessedtables == 0 -- Stop when there's no longer any tables to copy. 68 | 69 | return copied -- Give it back! 70 | end 71 | 72 | -- Haphazardly throw the deepcopy algorithm in the general direction of the global space. 73 | rawset(_G, "deepcopy_nonrecursive", deepcopy_nonrecursive) 74 | 75 | return deepcopy_nonrecursive -- Satisfy the 0 other people who use dofile to load libraries like these -------------------------------------------------------------------------------- /mobj/l_SoundAmbience.lua: -------------------------------------------------------------------------------- 1 | 2 | -- reimplementation of soundtable.lua (2020) 3 | -- TODO: format file 4 | 5 | 6 | -- note: only for loops, use S_StartSound for one entry that has no looptime 7 | 8 | freeslot("MT_SOUNDPLAYER", "S_SOUNDPLAYER_SET", "S_SOUNDPLAYER_PLAY", "S_SOUNDPLAYER_END") 9 | 10 | mobjinfo[MT_SOUNDPLAYER] = { 11 | doomednum = -1, 12 | spawnstate = S_SOUNDPLAYER_SET, 13 | flags = MF_NOGRAVITY|MF_NOCLIPHEIGHT|MF_NOCLIP, 14 | } 15 | states[S_SOUNDPLAYER_SET] = {SPR_NULL,0|FF_TRANS40,1,function(mo) 16 | -- set loop 17 | mo.audiosrc.loop = mo.audiosrc.sfxlist[mo.audiosrc.num][2] 18 | end,0,0,S_SOUNDPLAYER_PLAY} 19 | states[S_SOUNDPLAYER_PLAY] = {SPR_NULL,0|FF_TRANS40,1,function(mo) 20 | 21 | local soundid = mo.audiosrc.sfxlist[mo.audiosrc.num] 22 | local loopnum = mo.audiosrc.sfxlist[mo.audiosrc.num][2] 23 | 24 | if S_SoundPlaying(mo, soundid[1]) then 25 | 26 | -- stop loop while playing 27 | if mo.audiosrc.loop and mo.audiosrc.loop == 1 then 28 | S_StopSound(mo, mo.audiosrc.sfxlist[mo.audiosrc.num][1]) 29 | mo.audiosrc.loop = $1 - 1 30 | -- print("stopped loop") 31 | return 32 | end 33 | else 34 | -- TODO: looppoint with third argument 35 | -- TODO: re-add repeat mode 36 | -- TODO: infinite loop 37 | --[[ if (mo.audiosrc.loop == -1) then 38 | S_StartSound(mo, mo.audiosrc.sfxlist[mo.audiosrc.num][1]) 39 | return 40 | end--]] 41 | -- reset sound in loop 42 | if (mo.audiosrc.loop > 0) then 43 | S_StartSound(mo, mo.audiosrc.sfxlist[mo.audiosrc.num][1]) 44 | return 45 | end 46 | 47 | if (mo.audiosrc.num < #mo.audiosrc.sfxlist) then 48 | mo.audiosrc.num = (mo.audiosrc.playing) and $1+1 or $1+0 49 | mo.audiosrc.playing = true 50 | 51 | -- play next sound and reset state 52 | -- print(string.format("soundid %s", tostring(mo.audiosrc.num))) 53 | S_StartSound(mo, mo.audiosrc.sfxlist[mo.audiosrc.num][1]) 54 | mo.state = S_SOUNDPLAYER_SET 55 | else 56 | -- hit end of list 57 | mo.state = S_SOUNDPLAYER_END 58 | -- print("Ended") 59 | return 60 | end 61 | end 62 | 63 | -- print(string.format("loop - %d", tostring(mo.audiosrc.loop))) 64 | mo.audiosrc.loop = max(0, $1-1) 65 | 66 | end,0,0,S_SOUNDPLAYER_PLAY} 67 | states[S_SOUNDPLAYER_END] = {SPR_THOK,0|FF_TRANS40,1,nil,0,0,S_NULL} 68 | 69 | 70 | 71 | 72 | -- Creates a mobj sound source 73 | local function S_StartSoundEnviro(sounds, soundorigin, args) 74 | 75 | local src = P_SpawnMobj(soundorigin.x, soundorigin.y, soundorigin.z, MT_SOUNDPLAYER) 76 | 77 | src.audiosrc = {soundmobj=src, sfxlist=sounds, playing=false, num=1, loop=0, origin=soundorigin, static=(args and args.static)} 78 | 79 | -- TODO: reference created object properly 80 | return src 81 | end 82 | rawset(_G, "S_StartSoundEnviro", S_StartSoundEnviro) 83 | 84 | 85 | addHook("MobjSpawn", function(mo) 86 | mo.audiosrc = {} 87 | end) 88 | 89 | addHook("MobjThinker", function(mo) 90 | 91 | if not mo and mo.audiosrc then return end 92 | 93 | if (mo.audiosrc.origin and mo.audiosrc.origin.valid) and not mo.audiosrc.static then 94 | P_TeleportMove(mo, mo.audiosrc.origin.x, mo.audiosrc.origin.y, mo.audiosrc.origin.z) 95 | end 96 | end) 97 | 98 | 99 | addHook("ThinkFrame", function() 100 | if leveltime == 2*TICRATE then 101 | S_StartSoundEnviro({{sfx_jump,0}, {sfx_eleva1,0}, {sfx_eleva2, 8*TICRATE}, {sfx_eleva3,0}}, server.mo, {static=false}) 102 | end 103 | end) 104 | -------------------------------------------------------------------------------- /other/math_extended.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | * math_extended.lua 4 | * (sprkizard) 5 | * (none) 6 | 7 | * Desc: More functions for math related things 8 | 9 | * Usage: 10 | 11 | ]] 12 | 13 | 14 | local function map(x, in_min, in_max, out_min, out_max) 15 | return out_min + (x - in_min)*(out_max - out_min)/(in_max - in_min) 16 | end 17 | 18 | local function FixedLerp(a, b, t) 19 | if (a == b) then 20 | return a 21 | else 22 | -- 0.005 equiv to 65535/(65535*200), but better would be 0.5 (65535/2) 23 | if abs(a-b) < FRACUNIT/FRACUNIT*200 or abs(b-a) < FRACUNIT/FRACUNIT*200 then return b else return a + FixedMul(b - a, t) end 24 | end 25 | end 26 | 27 | -- A fixed lerp function that creates its own time TODO: review??? 28 | local function DeltaFixedLerp(start, dest, speed) 29 | local deltscaletime = nil 30 | 31 | if deltscaletime == nil then 32 | deltscaletime = 0 33 | end 34 | 35 | --if (leveltime > 15) then deltscaletime = $1+1 * speed end 36 | deltscaletime = $1+1 * speed 37 | 38 | return FixedLerp(start, dest, deltscaletime) 39 | end 40 | 41 | -- A function created by SwitchKaze in #scripting 42 | local function floatToFixed(str) 43 | if not (str and str:len()) then return 0 end 44 | local decpos = str:find('%.') -- decposito 45 | if decpos == nil then 46 | return tonumber(str)*FRACUNIT 47 | end 48 | local num = tonumber(str:sub(0,decpos-1))*FRACUNIT 49 | local frac = 0 50 | local i = 1 51 | for c in str:sub(decpos+1,str:len()):gmatch("%d+") do 52 | frac = frac + tonumber(c)*FRACUNIT/(10^i) 53 | i = i+1 54 | -- no digit n*65536 will ever be > 10^7 55 | if i==7 then break end 56 | end 57 | 58 | return num+frac 59 | end 60 | 61 | local function P_RandomChoice(choices) 62 | local RandomKey = P_RandomRange(1, #choices) 63 | if type(choices[RandomKey]) == "function" then 64 | choices[RandomKey]() 65 | else 66 | return choices[RandomKey] 67 | end 68 | end 69 | 70 | -- Lazy cos variable moving 71 | local function P_CosWave(speedangle, timer, numrange) 72 | return cos(FixedAngle(speedangle*FRACUNIT)*timer)*numrange 73 | end 74 | 75 | local function P_SinWave(speedangle, timer, numrange) 76 | return sin(FixedAngle(speedangle*FRACUNIT)*timer)*numrange 77 | end 78 | 79 | -- Triangular Wave 80 | local function tri(m, tm, period) 81 | return abs((tm % (period or (m*2)) ) - m) 82 | end 83 | 84 | -- x position cosine math for angle rotation around a point in space 85 | local function P_XAngle(distance, direction_angle, rotation) 86 | return distance*cos(direction_angle+FixedAngle(rotation*FRACUNIT)) 87 | end 88 | -- y position cosine math for angle rotation around a point in space 89 | local function P_YAngle(distance, direction_angle, rotation) 90 | return distance*sin(direction_angle+FixedAngle(rotation*FRACUNIT)) 91 | end 92 | -- z position cosine math for angle rotation around a point in space 93 | local function P_ZAngle(distance, direction_angle, rotation, dist2, dir_angle2, rot2) 94 | return FixedMul(P_XAngle(distance, direction_angle, rotation), 95 | P_YAngle(dist2 or distance, dir_angle2 or direction_angle, rot2 or rotation)) 96 | end 97 | 98 | rawset(_G, "map", map) 99 | rawset(_G, "FixedLerp", FixedLerp) 100 | rawset(_G, "DeltaFixedLerp", FixedLerp) 101 | rawset(_G, "floatToFixed", floatToFixed) 102 | rawset(_G, "P_RandomChoice", P_RandomChoice) 103 | rawset(_G, "P_CosWave", P_CosWave) 104 | rawset(_G, "P_SinWave", P_SinWave) 105 | rawset(_G, "tri", tri) 106 | rawset(_G, "P_XAngle", P_XAngle) 107 | rawset(_G, "P_YAngle", P_YAngle) 108 | rawset(_G, "P_ZAngle", P_ZAngle) 109 | 110 | 111 | -------------------------------------------------------------------------------- /other/FL_Table.Lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- FL_Table.Lua 3 | -- Resource file for Table functions 4 | -- 5 | -- Flame 6 | -- Date 3-27-21 7 | -- 8 | 9 | -- 10 | -- Function; 11 | -- createFlags(name, t) 12 | -- 13 | -- Arguments; 14 | -- name - Existing blank table. 15 | -- t - Table with a list of strings. 16 | -- 17 | -- Description: 18 | -- Creates flags similar to MF_*, MF2_*, MFE_*, constraints 19 | -- Gets length of 't' table, assigns each value of 't' in the Global Table and assigns it a flag value 20 | -- Also places in the tname table for other accessibility such as verifying the tname.string or tname.value 21 | -- 22 | -- Example: 23 | -- createFlags(tname, {"FL_FLAG", "FL_FLAG2", "FL_FLAG3"}) 24 | -- 25 | -- Creates FL_FLAG with value 1 26 | -- Creates FL_FLAG2 with value 2 27 | -- Creates FL_FLAG3 with value 4 28 | -- 29 | -- Creates tname[1].string = "FL_FLAG", tname[1].value = 1 30 | -- Creates tname[2].string = "FL_FLAG2", tname[2].value = 2 31 | -- Creates tname[2].string = "FL_FLAG3", tname[2].value = 4 32 | -- 33 | -- Usage: 34 | -- mobj.customflags = $ | FL_FLAG 35 | -- 36 | rawset(_G, "createFlags", function(tname, t) 37 | for i = 1,#t do 38 | rawset(_G, t[i], 2^(i-1)) 39 | table.insert(tname, {string = t[i], value = 2^(i-1)} ) 40 | end 41 | end) 42 | 43 | -- Function; 44 | -- createEnum(name, t) 45 | -- 46 | -- Arguments; 47 | -- name - Existing blank table. 48 | -- t - Table with a list of strings. 49 | -- 50 | -- Description: 51 | -- Gets length of 't' table, assigns each value of 't' in the Global Table and assigns it a enum value - 1 52 | -- Different from createFlags in that it allows familar table access. Think player.powers[pw_shield] for example 53 | -- Also places in the tname table for other accessibility such as verifying the tname.string or tname.value 54 | -- 55 | -- Example: 56 | -- createEnum(tname, {"FL_Enum", "FL_Enum2", "FL_Enum3"}) 57 | -- 58 | -- Creates Global FL_Enum with value 0 59 | -- Creates Global FL_Enum2 with value 1 60 | -- Creates Global FL_Enum3 with value 2 61 | -- 62 | -- Creates tname[1].string = "FL_Enum", tname[1].value = 0 63 | -- Creates tname[2].string = "FL_Enum2", tname[2].value = 1 64 | -- Creates tname[2].string = "FL_Enum3", tname[2].value = 2 65 | -- 66 | -- Usage: 67 | -- mobj.exampletable[FL_Enum] -- Notice the lack of need for quotation marks (" ") 68 | -- 69 | rawset(_G, "createEnum", function(tname, t, from) 70 | if from == nil then from = 0 end 71 | for i = 1,#t do 72 | rawset(_G, t[i], from+(i-1)) 73 | table.insert(tname, {string = t[i], value = from+(i-1)} ) 74 | end 75 | end) 76 | 77 | 78 | -- Function; 79 | -- spairs(t, order) 80 | -- 81 | -- Arguments; 82 | -- t - Table. 83 | -- order - . 84 | -- 85 | -- Description: 86 | -- Sorts t pairs with a f 87 | -- 88 | -- Example usage: 89 | -- local Scores = { Sonic = 8, Tails = 10, Knuckles = 11 } 90 | -- for k,v in spairs(Scores, function(t,a,b) return t[b] < t[a] end) do 91 | -- print(k,v) 92 | -- end 93 | -- 94 | -- Output: 95 | -- --> Knuckles 11 96 | -- --> Tails 10 97 | -- --> Sonic 8 98 | -- 99 | rawset(_G, "spairs", function(t, order) 100 | -- collect the keys 101 | local keys = {} 102 | for k in pairs(t) do keys[#keys+1] = k end 103 | 104 | -- if order function given, sort by it by passing the table and keys a, b, 105 | -- otherwise just sort the keys 106 | if order then 107 | table.sort(keys, function(a,b) return order(t, a, b) end) 108 | else 109 | table.sort(keys) 110 | end 111 | 112 | -- return the iterator function 113 | local i = 0 114 | return function() 115 | i = i + 1 116 | if keys[i] then 117 | return keys[i], t[keys[i]] 118 | end 119 | end 120 | end) 121 | -------------------------------------------------------------------------------- /hud/l_FadeScreen.lua: -------------------------------------------------------------------------------- 1 | -- Do not add this twice if it is in another file, please 2 | if _G["R_ScreenFade"] then return end 3 | 4 | -- Legacy Version 5 | --[[local function fadescreen(direction, color, speed, timescale, dontdraw) 6 | 7 | local dt = 0 -- time 8 | local dts = timescale or 1 -- timescale 9 | local spd = speed or 1 --speed 10 | local visible_layer = (dontdraw == false) and -1 or 99 -- toggle for visibility 11 | 12 | -- If it is a fade_in, then start from max value instead of 0 13 | if (direction == "in") then dt = 32 end 14 | 15 | -- Send new overwritable hudlayer entry 16 | R_AddHud("fadescreen", visible_layer, function(v, stplyr, cam) 17 | -- simple time math for out and in fading 18 | -- through min and max values of 0 and 32 19 | if (leveltime % dts == 0) then 20 | if (direction == "out") then 21 | dt = min(32, $+1 * spd) 22 | elseif (direction == "in") then 23 | dt = max(0, $-1 * spd) 24 | else 25 | return 26 | end 27 | end 28 | -- Only one fadescreen needed outside the if here 29 | v.fadeScreen(color, dt%33) 30 | end) 31 | 32 | end 33 | rawset(_G, "fadescreen", fadescreen) 34 | --]] 35 | 36 | 37 | -- New Version 38 | local function R_ScreenFade(ftype, args) 39 | 40 | -- Requires: l_hudzordering.lua 41 | if not _G["R_AddHud"] then print("(!)\x82 R_ScreenFade requires l_hudzordering.lua to work!") return end 42 | 43 | -- Allow the user to relocate the layer the fade is on 44 | local layer = (args and args.layer or 256) 45 | 46 | local a = {} 47 | a.fadetype = ftype -- The fading type (in, out, full, clear) 48 | a.time = 0 -- Elapsed Time 49 | a.delay = (args and args.delay or 1) -- Delay of the fade TODO: merge speed and delay? 50 | -- a.speed = (args and args.speed or 1) -- Speed of the fade (Unused) 51 | 52 | -- Allow the user to access the built in fadetypes with an alias 53 | local colortypes = { 54 | type1 = 0xFF00, 55 | -- value1 = 0xFF00, 56 | type2 = 0xFA00, 57 | -- value2 = 0xFA00, 58 | type3 = 0xFB00, 59 | -- value3 = 0xFB00, 60 | } 61 | for k,v in pairs(colortypes) do 62 | if (args and args.color == k) then 63 | a.color = v 64 | break 65 | else 66 | a.color = (args and args.color or 0) -- color 67 | end 68 | end 69 | 70 | -- Determine the max strength between palette and special values 71 | a.maxstrength = ((args and (a.color == 0xFF00 or a.color == 0xFA00 or a.color == 0xFB00)) and 32 or 10) 72 | 73 | -- Inverse the time if a fade-in 74 | if (ftype == "in") then a.time = a.maxstrength end 75 | 76 | -- Set the Hud attributes 77 | R_SetHud("__vfadeScreen", layer, a) 78 | -- print(string.format("time:%d | type: %s | strn: %s", a.time, a.fadetype, a.maxstrength)) 79 | end 80 | 81 | -- Custom Hud Entry 82 | R_AddHud("__vfadeScreen", nil, 83 | { 84 | fadetype = "in", 85 | time = 0, 86 | delay = 1, 87 | -- speed = 1, 88 | color = 0, 89 | maxstrength = 10, 90 | }, 91 | function(args, v, stplyr) 92 | 93 | -- Handle the hud entry options (full, clear, or default) 94 | if (args.fadetype == "full") then 95 | v.fadeScreen(args.color, args.maxstrength) 96 | elseif (args.fadetype == "clear") 97 | R_DeleteHud("__vfadeScreen") 98 | else 99 | if (leveltime % args.delay == 0) and not paused then 100 | if (args.fadetype == "out") then 101 | args.time = min(args.maxstrength, $+1 * 1) 102 | elseif (args.fadetype == "in") then 103 | args.time = max(0, $-1 * 1) 104 | else 105 | return 106 | end 107 | end 108 | -- Only one fadescreen needed here 109 | v.fadeScreen(args.color, args.time) 110 | end 111 | end) 112 | 113 | rawset(_G, "R_ScreenFade", R_ScreenFade) 114 | -------------------------------------------------------------------------------- /game/Axis2D/Axis2D_README.txt: -------------------------------------------------------------------------------- 1 | Axis2D script 2 | by sprki_zard and Fickleheart 3 | Version 1.3 (Alpha) 4 | Setup documentation 5 | --------------- 6 | 7 | Loading into a PK3/WAD: 8 | 9 | For testing, adding the script separately will work. Otherwise, load l_Axis2D.lua into 10 | your PK3 by dropping it inside of the Lua sub-folder. For WAD files, use any lump name that triggers 11 | the game to load it as a Lua script. (The suggested lump name is LUA_AX2D.) 12 | 13 | 14 | 15 | Thing types: 16 | 17 | Axis (1700): Set up similar to a NiGHTS axis, but without multimare settings. Angle 18 | is axis radius (add 16384 to invert), and the flags value is the axis number. (Tip: 19 | Zone Builder has a rendering option to draw the axis circles in-editor.) 20 | 21 | Line Axis (1702): Same thing type as NiGHTS axis transfer line, but set up slightly 22 | differently. Angle is the direction pressing right should take the player, and the 23 | flags value is the axis number. No second Thing is necessary per axis. Players may 24 | slide a bit when behind (to the left camera-wise of) the reference object, so put it 25 | as far back as needed to avoid this scenario. 26 | 27 | 28 | 29 | Linedef types: 30 | 31 | New Behaviour (> 2.2): 32 | Linedef Executor - Call Lua Function (443): Write "P_Axis2D" (case-insensitive) 33 | across the textures. The tag is the axis number to snap to. Call this 34 | with a tag of 0 to exit Axis2D mode and go back to 3D. 35 | 36 | -- Axis2D Options (443): 37 | 38 | -- Tag: The tag is the axis number to use this with. 39 | 40 | -- [5] EFFECT 1 (E1): The frontsector floor height will set the camera distance 41 | from the player (It defaults to 448 otherwise). 42 | 43 | -- [6] No Climb: If checked, this is an absolute angle, otherwise it's relative 44 | to the normal camera angle. 45 | 46 | -- [7] EFFECT 2 (E2): The backsector floor height will set the camera's height 47 | relative to the player. 48 | 49 | -- [8] EFFECT 3 (E3): The x offset texture field of the lindef's frontside 50 | will determine the angle of the camera. The angle of the 51 | linedef is the camera angle by default when unchecked (in reverse). 52 | 53 | -- [9] EFFECT 2 (E4): The y offset texture field of the lindef's frontside 54 | will determine if the camera aims either up or downwards using the player's 55 | aiming field. 56 | 57 | Legacy Behaviour (Backwards Compatability): 58 | Linedef Executor - Call Lua Function (443): Write "P_DoAngleSpin" (case-insensitive) 59 | across the textures. (One way to do this is writing "P_DOANGL" in the upper texture 60 | and "ESPIN" in the mid texture.) The tag is the axis number to snap to. Call this 61 | with a tag of 0 to exit Axis2D mode and go back to 3D. 62 | 63 | Axis2D Switch Sector (9001): Use this like an invisible, intangible FOF. X offset is 64 | the axis number to switch to. (A simpler method of axis switching than the linedef 65 | executor method.) Does nothing if not already in Axis2D mode. 66 | 67 | Axis2D Options (9000): The tag is the axis number to use this with. The angle of the 68 | linedef is the camera angle (in reverse); if No Climb is checked, this is an absolute 69 | angle, otherwise it's relative to the normal camera angle. If Effect 1 is checked, 70 | then the linedef's length will determine the camera distance from the player. (It 71 | defaults to 448 otherwise, so design your geometry around that.) 72 | 73 | 74 | 75 | Issues: Due to Axis2D not accounting for 'Simple Mode' as it was not included during 76 | development, players will thok backwards when using the charability CA_THOK. 77 | This will be addressed in the future. 78 | 79 | 80 | 81 | Special notes: 82 | 83 | The current release does not make spilled rings execute Axis2D-related linedef 84 | executors. If you used an older version and need this functionality back, set 85 | axis2d.legacymode = true in the script. 86 | 87 | If legacy mode is enabled, spilled rings can execute any linedef trigger that 88 | contains a trigger to switch axes. This is so they stay on the level track. Be 89 | careful with your linedef setup and try to keep triggers to only switching axes or 90 | only doing other things. 91 | 92 | Axis2D must never be on at the same time as vanilla 2D. Movement will have issues 93 | otherwise. Make sure to always set the player into 3D mode if you're transitioning 94 | between vanilla 2D and Axis2D, and DO NOT set the 2D typeoflevel in your level's 95 | header. 96 | 97 | Be careful to always place triggers to enter Axis2D axes at and around starposts, or 98 | the player may unintentionally respawn into 3D. 99 | 100 | 101 | 102 | Have fun! If this readme doesn't do a great job at explaining things, ask sprki_zard (Varren) 103 | on the MB, Discord, or through any other means of contact! 104 | -------------------------------------------------------------------------------- /other/l_scrollobject.lua: -------------------------------------------------------------------------------- 1 | 2 | local RUNNING_VALUES = {} 3 | local WAITING_REMOVAL = {} 4 | 5 | function table.size(table_name) 6 | local n = 0 7 | for k,v in pairs(table_name) do 8 | n = n + 1 9 | end 10 | return n 11 | end 12 | 13 | local function FixedLerp(a, b, t) 14 | if (a == b) then 15 | return a 16 | else 17 | -- 0.005 equiv to 65535/(65535*200), but better would be 0.5 (65535/2) 18 | if abs(a-b) < FRACUNIT/FRACUNIT*200 or abs(b-a) < FRACUNIT/FRACUNIT*200 then return b else return a + FixedMul(b - a, t) end 19 | end 20 | end 21 | 22 | 23 | --local function scrollvar(name, val, targetvalue, speed) 24 | local function scrollvar(name, parms, single) 25 | 26 | --aliases 27 | parms.val = parms.startv or parms.val 28 | parms.targetvalue = parms.endv or parms.targetvalue 29 | parms.speed = parms.spd or parms.speed 30 | 31 | -- insert a new scrollobject into the global table 32 | if not (RUNNING_VALUES[name] and RUNNING_VALUES[name].active) then 33 | RUNNING_VALUES[name] = {active = true, value = parms.val or 0, target = parms.targetvalue or 0, speed = parms.speed or 128, func = parms.func, once = single, dt = 0} 34 | end 35 | return RUNNING_VALUES[name].value 36 | end 37 | 38 | -- Reset an object from the global table by removing it immediately 39 | rawset(_G, "ResetSO", function(name) 40 | RUNNING_VALUES[name] = nil 41 | end) 42 | 43 | -- Toss an object into the garbage so it no longer runs if not using once 44 | rawset(_G, "TossSO", function(name) 45 | table.insert(WAITING_REMOVAL, RUNNING_VALUES[name]) 46 | end) 47 | 48 | 49 | 50 | 51 | -- Reset table on change 52 | addHook("MapChange", function() 53 | 54 | -- Pick which to clone over into a new table if it is meant to be persistent 55 | for k,v in pairs(RUNNING_VALUES) do 56 | --print(k,v) 57 | end 58 | 59 | -- Wipe the table 60 | RUNNING_VALUES = {} 61 | WAITING_REMOVAL = {} 62 | end) 63 | 64 | 65 | 66 | --local remove = {} 67 | addHook("ThinkFrame", function() 68 | 69 | -- Remove any in waiting 70 | for i = 1, #WAITING_REMOVAL do 71 | RUNNING_VALUES[WAITING_REMOVAL[i]] = nil 72 | end 73 | 74 | -- Iterate the container and update and lerp all values 75 | for k,v in pairs(RUNNING_VALUES) do 76 | 77 | v.dt = $1+1 * v.speed -- (Individual time value instead of leveltime) 78 | v.value = FixedLerp(v.value, v.target, v.dt) 79 | --v.ref.scale = FixedLerp(v.value, v.target, leveltime * v.speed) 80 | 81 | -- Run a callback function when ended 82 | if (v.value == v.target and v.func) then 83 | v.func() 84 | end 85 | 86 | -- Set removal when finished 87 | if (v.value == v.target and v.once) then table.insert(WAITING_REMOVAL, k) end --print("Removing - "..k) end 88 | --print(k,v.value) 89 | end 90 | 91 | end) 92 | 93 | rawset(_G, "scrollvar", scrollvar) 94 | rawset(_G, "smoothval", scrollvar) 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | local function LerpTest(v, stplyr, ticker, endtime) 105 | 106 | --local w = scrollvar("wfill", 0*FRACUNIT, 330*FRACUNIT, 256) 107 | --v.drawFill(0, 0, w/FRACUNIT, 200, 30) 108 | --v.drawFill(0, 0, scrollvar("wfill1", 0*FRACUNIT, v.width()*FRACUNIT, 256)/FRACUNIT, 42, 30) 109 | --v.drawFill(0, 42, scrollvar("wfill2", 0*FRACUNIT, v.width()*FRACUNIT, 64)/FRACUNIT, 32, 28) 110 | --v.drawFill(0, 74, scrollvar("wfill3", 0*FRACUNIT, v.width()*FRACUNIT, 32)/FRACUNIT, 64, 26) 111 | --v.drawFill(0, 138, scrollvar("wfill4", 0*FRACUNIT, v.width()*FRACUNIT, 16)/FRACUNIT, 200, 24) 112 | 113 | --local x = scrollvar("x", 0*FRACUNIT, 256*FRACUNIT) 114 | -- 115 | --v.drawString(16, 64, "x ["..x.."]", V_ORANGEMAP) 116 | --v.drawString(16, 84, "x Int ["..x/FRACUNIT.."]", V_ORANGEMAP) 117 | -- 118 | --if (leveltime >= 5*TICRATE) then 119 | -- local z = scrollvar("z", 32*FRACUNIT, 0*FRACUNIT) 120 | -- v.drawString(16, 74, "z ["..z.."]", V_ORANGEMAP) 121 | -- v.drawString(16, 94, "z Int ["..z/FRACUNIT.."]", V_ORANGEMAP) 122 | --end 123 | -- 124 | -- 125 | --local x = scrollvar("x_hud", {startv = 20*FRACUNIT, targetvalue = 70*FRACUNIT, speed = 128}) 126 | --v.drawString(x/FRACUNIT, 171, "Scroll String", V_ORANGEMAP) 127 | --v.drawString(x/FRACUNIT, 181, "Above x position ["..x/FRACUNIT.."]", V_ORANGEMAP) 128 | 129 | v.drawString(310, 190, "RUNNING_VALUES ["..table.size(RUNNING_VALUES).."]", V_ORANGEMAP, "right") 130 | 131 | end 132 | 133 | hud.add(LerpTest, "titlecard") 134 | 135 | 136 | addHook("ThinkFrame", function() 137 | 138 | for player in players.iterate do 139 | --player.mo.momx = 0 140 | --player.mo.momy = 0 141 | --player.mo.momz = 0 142 | --P_TeleportMove(player.mo, scrollvar("x", -32*FRACUNIT, 1524*FRACUNIT ), scrollvar("y", -64*FRACUNIT, 1524*FRACUNIT), scrollvar("z", 32*FRACUNIT, 1024*FRACUNIT)) 143 | 144 | end 145 | 146 | end) -------------------------------------------------------------------------------- /game/l_mapzones.lua: -------------------------------------------------------------------------------- 1 | 2 | rawset(_G, "MapZone", {effects={}}) 3 | 4 | local mapzones = {} 5 | 6 | -- Wrapper to search for an area by it's tag 7 | function MapZone.searchZoneByID(tag) 8 | for i=1, #mapzones do 9 | if (mapzones[i].tag == tag) then 10 | return mapzones[i] 11 | end 12 | end 13 | end 14 | 15 | -- Wrapper to sloppily for-loop the effects table to run functions stored in it with stacked tags 16 | function MapZone.runZoneFunction(mobj, zone) 17 | for i=1, #MapZone.effects do 18 | 19 | for j=1, #MapZone.effects[i].tags do 20 | if (MapZone.effects[i].tags[j] == zone.tag) then 21 | MapZone.effects[i].func(mobj, zone) 22 | end 23 | end 24 | end 25 | end 26 | 27 | -- Adds a function on an area's tag, allows for tag-stacked effects 28 | function MapZone.AddBoundEffect(name, tags, f) 29 | table.insert(MapZone.effects, {name=name, tags=tags, func=f}) 30 | -- MapZone.effects[tag] = {func=f} 31 | end 32 | 33 | -- Function to create a rectangle using two coordinate points 34 | function MapZone.makeRect(x1, y1, x2, y2) 35 | 36 | -- I don't remember if this is trig or not, but this gets the x and y distance of our 2 points 37 | local x = x2 - x1 38 | local y = y2 - y1 39 | 40 | return { 41 | ca = {x=x1, y=y1}, 42 | cb = {x=x2, y=y2}, 43 | cc = {x=x1 + x, y=y2 - y}, 44 | cd = {x=x2 - x, y=y1 + y}, 45 | } 46 | end 47 | 48 | -- Linedef Executor made to be run at level load, or triggered non-continously to set 49 | -- rectangular boundaries in a map to run effects, play sounds, etc 50 | -- This technically acts as an invisible rectangular FOF, but not defined by a sector 51 | addHook("LinedefExecute", function(line, trigger, sector) 52 | 53 | -- @ Flag Effects: 54 | -- [1] Block Enemies: Enable or disable a zone 55 | -- [6] Not Climbable: Use axis mobjs 56 | -- [10] Repeat Midtexture: Disable on creation 57 | -- @ Default: 58 | -- search by front and back sector offsets + assign lua function by linedef tag 59 | 60 | local rect = {} 61 | -- local x1,y1,x2,ys2 = 0 62 | 63 | if (line.flags & ML_BLOCKMONSTERS) then 64 | -- enables / disables the area and exits the executor 65 | local znb = MapZone.searchZoneByID(line.tag) 66 | znb.enabled = (not $1) 67 | return 68 | elseif (line.flags & ML_NOCLIMB) then 69 | 70 | local mo1, mo2 71 | 72 | for mt in mobjs.iterate() do 73 | -- TODO: custom object or keep MT_AXIS? 74 | if ((mt.type == MT_AXIS or mt.type == MT_BLASTEXECUTOR) and mt.spawnpoint.angle == line.tag) then 75 | -- Find objects using parameter values 76 | if (mt.spawnpoint.extrainfo == 0) then 77 | mo1 = mt 78 | -- x1 = mt.x 79 | -- y1 = mt.y 80 | elseif (mt.spawnpoint.extrainfo == 1) then 81 | mo2 = mt 82 | -- x2 = mt.x 83 | -- y2 = mt.y 84 | end 85 | end 86 | end 87 | 88 | -- If no mobjs exist for the tag, this exits if any one of the numbers are nil as a result 89 | if (mo1 == nil or mo2 == nil) then 90 | -- if (x1 == nil or x2 == nil or y1 == nil or y2 == nil) then 91 | print(string.format("\x81LinedefExecute [MAPZONE]: Cannot find mobj boundaries with tag [%d]!", line.tag)) 92 | return 93 | end 94 | 95 | -- Create rectangle from mobjs 96 | rect = MapZone.makeRect(mo1.x, mo1.y, mo2.x, mo2.y) 97 | -- rect = MapZone.makeRect(x1, y1, x2, y2) 98 | 99 | else 100 | -- Create rectangle from line offset coordinates 101 | rect = MapZone.makeRect(line.frontside.textureoffset, 102 | line.frontside.rowoffset, 103 | line.backside.textureoffset, 104 | line.backside.rowoffset) 105 | end 106 | 107 | -- Use the line tag as the identifier for this boundary 108 | rect.tag = line.tag 109 | 110 | -- Apply floor and ceiling boundaries 111 | rect.floor = line.frontsector.floorheight 112 | rect.ceiling = line.frontsector.ceilingheight 113 | 114 | -- EFFECT5 determines if this area is off/on on trigger 115 | if (line.flags & ML_EFFECT5) then rect.enabled = false else rect.enabled = true end 116 | 117 | -- print(string.format("Point A: (%d, %d)", (rect.ca.x)/FU, (rect.ca.y)/FU)) 118 | -- print(string.format("Point B: (%d, %d)", (rect.cb.x)/FU, (rect.cb.y)/FU)) 119 | -- print(string.format("Point C: (%d, %d)", (rect.cc.x)/FU, (rect.cc.y)/FU)) 120 | -- print(string.format("Point D: (%d, %d)", (rect.cd.x)/FU, (rect.cd.y)/FU)) 121 | 122 | -- Insert rectangle 123 | table.insert(mapzones, rect) 124 | 125 | end, "MapZone") 126 | 127 | 128 | -- Allows functions and thinkers to be executed when inside a created boundary 129 | function MapZone.isInZoneTrigger(mobj) 130 | 131 | for i=1, #mapzones do 132 | 133 | local zone = mapzones[i] 134 | 135 | if not zone.enabled then return end -- This area is disabled 136 | 137 | -- Check if the mobj is inside of the rectangle boundaries 138 | if ((mobj.x > zone.ca.x and mobj.x < zone.cb.x) 139 | and (mobj.y > zone.cd.y and mobj.y < zone.cc.y) 140 | or (mobj.x < zone.ca.x and mobj.x > zone.cb.x) 141 | and (mobj.y < zone.cd.y and mobj.y > zone.cc.y)) 142 | and (mobj.z > zone.floor and mobj.z < zone.ceiling) then 143 | -- Run function 144 | MapZone.runZoneFunction(mobj, zone) 145 | -- MapZone.effects[zone.tag].func(mobj, zone) 146 | end 147 | end 148 | end 149 | 150 | 151 | --[[addHook("ThinkFrame", function() 152 | 153 | for player in players.iterate do 154 | MapZone.isInZoneTrigger(player.mo) 155 | end 156 | 157 | end)--]] 158 | -------------------------------------------------------------------------------- /hud/l_supertextfont.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_supertextfont.lua 3 | * (sprkizard) 4 | * (May 29, 2020 12:42) 5 | * Desc: Custom font drawer 6 | 7 | * Usage: TODO 8 | ]] 9 | 10 | 11 | -- Copy of the creditwidth function in-source, but accounting for any given font type 12 | -- https://github.com/STJr/SRB2/blob/225095afa2fb1c61d12cf96c1b7c56cb4dbb4350/src/v_video.c#L3211 13 | local function GetInternalFontWidth(str, font) 14 | 15 | -- No string 16 | if not (str) then return 0 end 17 | 18 | local width = 0 19 | 20 | for i=1,#str do 21 | -- Spaces before fonts 22 | if str:sub(i):byte() == 32 then 23 | width = $1+2 24 | continue 25 | end 26 | -- Ignore skincolors completely 27 | if str:sub(i):byte() >= 131 and str:sub(i):byte() <= 198 then 28 | continue 29 | end 30 | -- TODO: count special characters? 31 | if str:sub(i):byte() >= 200 then 32 | width = $1+8 33 | continue 34 | end 35 | 36 | -- (Using patch width by the way) 37 | if (font == "STCFN") then -- default font 38 | width = $1+8 39 | elseif (font == "TNYFN") then 40 | width = $1+7 41 | elseif (font == "LTFNT") then 42 | width = $1+20 43 | elseif (font == "TTL") then 44 | width = $1+29 45 | elseif (font == "CRFNT" or font == "NTFNT") then -- TODO: Credit font centers wrongly 46 | width = $1+16 47 | elseif (font == "NTFNO") then 48 | width = $1+20 49 | else 50 | width = $1+8 51 | end 52 | end 53 | return width 54 | end 55 | 56 | 57 | local function drawSuperText(v, x, y, str, parms) 58 | 59 | -- Scaling 60 | local scale = (parms and parms.scale) or 1*FRACUNIT 61 | local hscale = (parms and parms.hscale) or 0 62 | local vscale = (parms and parms.vscale) or 0 63 | local yscale = (8*(FRACUNIT-scale)) 64 | -- Spacing 65 | local xspacing = (parms and parms.xspace) or 0 -- Default: 8 66 | local yspacing = (parms and parms.yspace) or 4 67 | -- Text Font 68 | local font = (parms and parms.font) or "STCFN" 69 | local color = (parms and parms.color) or v.getColormap(-1, 1) 70 | local uppercs = (parms and parms.uppercase) or false 71 | local align = (parms and parms.align) or nil 72 | local flags = (parms and parms.flags) or 0 73 | 74 | -- Split our string into new lines from line-breaks 75 | local lines = {} 76 | 77 | for ls in str:gmatch("[^\r\n]+") do 78 | table.insert(lines, ls) 79 | end 80 | 81 | -- For each line, set some stuff up 82 | for seg=1,#lines do 83 | 84 | local line = lines[seg] 85 | -- Fixed Position 86 | local fx = x << FRACBITS 87 | local fy = y << FRACBITS 88 | -- Offset position 89 | local off_x = 0 90 | local off_y = 0 91 | -- Current character & font patch (we assign later later instead of local each char) 92 | local char 93 | local charpatch 94 | 95 | -- Alignment options 96 | if (align) then 97 | -- TODO: not working correctly for CRFNT 98 | if (align == "center") then 99 | fx = $1-FixedMul( (GetInternalFontWidth(line, font)/2), scale) << FRACBITS -- accs for scale 100 | -- fx = $1-FixedMul( (v.stringWidth(line, 0, "normal")/2), scale) << FRACBITS 101 | elseif (align == "right") then 102 | fx = $1-FixedMul( (GetInternalFontWidth(line, font)), scale) << FRACBITS 103 | -- fx = $1-FixedMul( (v.stringWidth(line, 0, "normal")), scale) << FRACBITS 104 | end 105 | end 106 | 107 | -- Go over each character in the line 108 | for strpos=1,#line do 109 | 110 | -- get our character step by step 111 | char = line:sub(strpos, strpos) 112 | 113 | -- TODO: custom skincolors will make a mess of this since the charlimit is 255 114 | -- Set text color, inputs, and more through special characters 115 | -- Referencing skincolors https://wiki.srb2.org/wiki/List_of_skin_colors 116 | if (char:byte() == 130) then 117 | color = nil 118 | continue 119 | elseif (char:byte() >= 131 and char:byte() <= 198) then 120 | color = v.getColormap(-1, char:byte() - 130) 121 | continue 122 | end 123 | 124 | -- TODO: effects? 125 | -- if (char:byte() == 161) then 126 | -- continue 127 | -- end 128 | -- print(strpos<<27) 129 | -- off_x = (cos(v.RandomRange(ANG1, ANG10)*leveltime)) 130 | -- off_y = (sin(v.RandomRange(ANG1, ANG10)*leveltime)) 131 | -- local step = strpos%3+1 132 | -- print(step) 133 | -- off_x = cos(ANG10*leveltime)*step 134 | -- off_y = sin(ANG10*leveltime)*step 135 | 136 | -- Skip and replace non-existent space graphics 137 | if not char:byte() or char:byte() == 32 then 138 | fx = $1+2*scale 139 | continue 140 | end 141 | 142 | -- Unavoidable non V_ALLOWLOWERCASE flag toggle (exclude specials above 210) 143 | if (uppercs or (font == "CRFNT" or font == "NTFNT")) 144 | and not (char:byte() >= 210) then 145 | char = tostring(char):upper() 146 | end 147 | 148 | -- transform the char to byte to a font patch 149 | charpatch = v.cachePatch( string.format("%s%03d", font, string.byte(char)) ) 150 | 151 | -- Draw char patch 152 | v.drawStretched( 153 | fx+off_x, fy+off_y+yscale, 154 | scale+hscale, scale+vscale, charpatch, flags, color) 155 | -- Sets the space between each character using font width 156 | fx = $1+(xspacing+charpatch.width)*scale 157 | --fy = $1+yspacing*scale 158 | end 159 | 160 | -- Break new lines by spacing and patch width for semi-accurate spacing 161 | y = $1+(yspacing+charpatch.height)*scale >> FRACBITS 162 | end 163 | 164 | end 165 | 166 | 167 | rawset(_G, "drawSuperText", drawSuperText) 168 | rawset(_G, "GetInternalFontWidth", GetInternalFontWidth) 169 | -------------------------------------------------------------------------------- /hud/l_hudzordering.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_hudzordering.lua 3 | * (sprkizard) 4 | * (November 28, 2018 1:04) 5 | * Desc: A tiny HUD library made to allow multiple HUD elements with 6 | layer priority. Inspired by a script made by fickleheart 7 | 8 | * Usage: hudlayer["layer"] = {} 9 | ------ 10 | Table arguments: 11 | ({layernum = 1}) - The 'priority' or layer the HUD element sits on. 12 | A value of -1 Removes it from view 13 | 14 | ({func = function(v, stplyr, cam)}) - The HUD function to run 15 | which can contain HUD elements, strings, etc 16 | 17 | Can also be called with hudlayer["layer"].layernum and hudlayer["layer"].func() 18 | 19 | * Depends on: 20 | spairs 21 | ]] 22 | 23 | -- spairs attribution by: 24 | -- https://stackoverflow.com/questions/15706270/sort-a-table-in-lua 25 | ------------------- 26 | -- Do not add this twice if it is in another file, please 27 | if _G["hudLayer"] then return end 28 | 29 | 30 | -- Attempts to sort pairs 31 | local function spairs(t, order) 32 | -- collect the keys 33 | local keys = {} 34 | for k in pairs(t) do keys[#keys+1] = k end 35 | 36 | -- if order function given, sort by it by passing the table and keys a, b, 37 | -- otherwise just sort the keys 38 | if order then 39 | table.sort(keys, function(a,b) return order(t, a, b) end) 40 | else 41 | table.sort(keys) 42 | end 43 | 44 | -- return the iterator function 45 | local i = 0 46 | return function() 47 | i = i + 1 48 | if keys[i] then 49 | return keys[i], t[keys[i]] 50 | end 51 | end 52 | end 53 | 54 | 55 | -- The container table that holds all user created HUD elements 56 | rawset(_G, "hudLayer", {items={}}) 57 | 58 | 59 | -- Iterates and sorts through hudLayer with spairs 60 | local function sortHudItems(v, stplyr, cam) 61 | 62 | --for k,huditem in spairs(hudLayer.items, function(t,a,b) return t[b].layernum < t[a].layernum end) do -- desc 63 | for _,huditem in spairs(hudLayer.items, function(t,a,b) return t[a].layernum > t[b].layernum end) do -- asc 64 | 65 | -- Delete the item (Stop it from running at all and skip it) 66 | if (huditem.layernum == -99) then 67 | -- huditem = nil 68 | return 69 | end 70 | 71 | (function() 72 | -- Do not run on items with -1 at all. 73 | if (huditem.layernum == -1) then return end 74 | 75 | -- Run hud functions 76 | huditem.func(huditem.args, v, stplyr, cam) 77 | end)() 78 | end 79 | end 80 | hud.add(sortHudItems, "game") 81 | 82 | -- Adds a new hud to the layer table 83 | local function R_AddHud(layername, ordernum, args, hudfunc, forcereset) 84 | hudLayer.items[layername] = { 85 | args = args or {}, 86 | -- reset = setreset or true, (Unused) 87 | layernum = ordernum or -1, 88 | func = hudfunc, 89 | } 90 | end 91 | 92 | -- Sets layer order 93 | local function R_SetHud(layername, ordernum, args) 94 | if not (hudLayer and hudLayer.items[layername]) then return end 95 | 96 | -- TODO: find another way to keep the default value 97 | hudLayer.items[layername].layernum = (ordernum and type(ordernum) == "string" and $ or ordernum or -1) 98 | -- hudLayer.items[layername].layernum = ordernum or -1 99 | 100 | -- Add or update variables to the Hud if specified 101 | if (args) then 102 | for k,v in pairs(args) do 103 | (function() 104 | hudLayer.items[layername].args[k] = v 105 | end)() 106 | end 107 | end 108 | end 109 | 110 | -- Disables a layer 111 | local function R_DisableHud(layername) 112 | if not (hudLayer and hudLayer.items[layername]) then return end 113 | hudLayer.items[layername].layernum = -1 114 | end 115 | 116 | -- Sets the layer to be deleted 117 | local function R_DeleteHud(layername) 118 | if not (hudLayer and hudLayer.items[layername]) then return end 119 | hudLayer.items[layername].layernum = -99 120 | end 121 | 122 | -- Sets internal game huds to be hidden/unhidden 123 | local function R_SetInternalHudStatus(namelist, huditemvisible) 124 | for i=1,#namelist do 125 | if (huditemvisible) then 126 | hud.enable(namelist[i]) 127 | else 128 | hud.disable(namelist[i]) 129 | end 130 | end 131 | end 132 | 133 | -- Netvars hook / function 134 | function hudLayer.netvars(n) 135 | for _,entry in pairs(hudLayer.items) do 136 | entry.args = n($) 137 | entry.layernum = n($) 138 | -- entry.func = n($) 139 | end 140 | end 141 | 142 | --[[addHook("NetVars", function(network) 143 | hudLayer.netvars(network) 144 | end)--]] 145 | 146 | rawset(_G, "R_AddHud", R_AddHud) 147 | rawset(_G, "R_SetHud", R_SetHud) 148 | rawset(_G, "R_DisableHud", R_DisableHud) 149 | rawset(_G, "R_DeleteHud", R_DeleteHud) 150 | rawset(_G, "R_SetInternalHudStatus", R_SetInternalHudStatus) 151 | 152 | 153 | -- Some examples 154 | -- (Uncomment to test) 155 | -- Test the script addon by removing the removable layer, and replacing the text on 'test1' 156 | --[[R_AddHud("test1", 5, {text=nil}, -- Custom text variable 157 | function(args, v, stplyr) 158 | v.drawString(64, 64, args.text or "Jump to replace my text!", V_ALLOWLOWERCASE, "left") 159 | end) 160 | 161 | R_AddHud("test2", 4, {}, 162 | function(args, v, stplyr) 163 | local x = 6*cos(leveltime*ANG10)/FU 164 | v.drawString(64+x, 68, "Moving text!", V_ALLOWLOWERCASE, "left") 165 | end) 166 | 167 | R_AddHud("removable", 3, {}, 168 | function(args, v, stplyr) 169 | v.drawString(64, 76, "Remove me on Jump", V_ALLOWLOWERCASE, "left") 170 | end) 171 | 172 | addHook("ThinkFrame", function() 173 | 174 | for player in players.iterate do 175 | if (player.cmd.buttons & BT_JUMP) then 176 | R_DeleteHud("removable") 177 | R_SetHud("test1", "", {text="Replaced Text!"}) 178 | end 179 | end 180 | end)--]] 181 | 182 | -------------------------------------------------------------------------------- /mobj/l_spritemdl.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_spritemdl.lua 3 | * (sprkizard) 4 | * (May 16, 2020 00:28) 5 | * Desc: Small script that allows the simple creation of 6 | grouped sprites on objects in the form of a 3D object ala 7 | ACZ2 minecart (v2.2) 8 | 9 | * Usage: 10 | ]] 11 | 12 | 13 | -- Builds up a mobj sprite group from a list of mobjs 14 | local function P_BuildSpriteModel(source, itemlist) 15 | 16 | -- source mobj is not valid 17 | if not source.valid then return end 18 | 19 | source.sprmdl = {} 20 | 21 | for i=1,#itemlist do 22 | local mo = P_SpawnMobjFromMobj(source, 0, 0, 0, itemlist[i].mobjtype or MT_THOK) 23 | 24 | if (itemlist[i].spritetype == "splat") then 25 | mo.renderflags = $1|RF_FLOORSPRITE|RF_SLOPESPLAT|RF_NOSPLATBILLBOARD 26 | elseif (itemlist[i].spritetype == "paper") then 27 | mo.renderflags = $1|RF_PAPERSPRITE 28 | end 29 | 30 | -- References the parent in every object created by the parent builder, including the object itself 31 | table.insert(source.sprmdl, { 32 | mobj = mo, 33 | parent = source, 34 | offset = itemlist[i].offset, -- the object's offset position 35 | angleoffset = (not (itemlist[i].angleoffset == nil) and itemlist[i].angleoffset or 0), -- the object's angle 36 | scaleoffset = (not (itemlist[i].scaleoffset == nil) and itemlist[i].scaleoffset or 0), 37 | rotation = (not (itemlist[i].rotation == nil) and itemlist[i].rotation or 0), -- the objects rotation around the source's radius 38 | spritetype = itemlist[i].spritetype, 39 | zangle = itemlist[i].zangle, 40 | id = itemlist[i].id, 41 | }) 42 | end 43 | end 44 | 45 | local function P_UpdateSpriteModel(source, callback) 46 | 47 | -- source mobj is not valid 48 | if not source.valid then return end 49 | 50 | -- Run through the entire source spritegroup 51 | for i=1,#source.sprmdl do 52 | 53 | local modelpart = source.sprmdl[i] 54 | 55 | if not modelpart.mobj.valid then return end 56 | 57 | -- Follow the source angle + offset 58 | modelpart.mobj.angle = R_PointToAngle2(modelpart.mobj.x, modelpart.mobj.y, source.x, source.y) + FixedAngle(modelpart.angleoffset*FU) 59 | 60 | -- Set scale to source scale 61 | modelpart.mobj.scale = source.scale + modelpart.scaleoffset 62 | 63 | if (modelpart.spritetype == "splat" and modelpart.zangle) then 64 | P_CreateFloorSpriteSlope(modelpart.mobj) 65 | modelpart.mobj.floorspriteslope.zangle = FixedAngle(modelpart.zangle*FU) 66 | modelpart.mobj.floorspriteslope.xydirection = modelpart.mobj.angle 67 | modelpart.mobj.floorspriteslope.o = { 68 | x = source.x+FixedMul(modelpart.offset.x*cos(source.angle+FixedAngle(modelpart.rotation*FU)), source.scale), 69 | y = source.y+FixedMul(modelpart.offset.y*sin(source.angle+FixedAngle(modelpart.rotation*FU)), source.scale), 70 | z = source.z+FixedMul(modelpart.offset.z*FU, source.scale) 71 | } 72 | end 73 | 74 | -- Run a callback function to edit one or all items 75 | if (callback) then 76 | do 77 | callback(modelpart) 78 | end 79 | end 80 | 81 | -- Update the position of all parts to be relative to the mobj + offsets (+ scaling) 82 | P_TeleportMove(modelpart.mobj, 83 | source.x+FixedMul(modelpart.offset.x*cos(source.angle+FixedAngle(modelpart.rotation*FU)), source.scale), 84 | source.y+FixedMul(modelpart.offset.y*sin(source.angle+FixedAngle(modelpart.rotation*FU)), source.scale), 85 | source.z+FixedMul(modelpart.offset.z*FU, source.scale)) 86 | 87 | end 88 | 89 | end 90 | 91 | 92 | 93 | local function P_BuildSpriteMdl(source, grouplist) 94 | 95 | -- source mobj is not valid 96 | if not source.valid then return end 97 | 98 | source.sprmdl = grouplist 99 | for i=1,#source.sprmdl do 100 | 101 | -- References the parent in every object created by the parent builder, including the object itself 102 | if (source.sprmdl[i].mobj.valid) then 103 | source.sprmdl[i].mobj.sprmdl_parent = source 104 | source.sprmdl[i].mobj.sprmdl_self = source.sprmdl[i] 105 | end 106 | end 107 | end 108 | 109 | -- Updates the position and callback functions of the sprite group 110 | local function P_UpdateSpriteMdl(source, func) 111 | 112 | -- source mobj is not valid 113 | if not source.valid then return end 114 | 115 | -- Run through the entire source spritegroup 116 | for i=1,#source.sprmdl do 117 | 118 | -- sprmdl mobj is not valid 119 | if not source.sprmdl[i].mobj.valid then return end 120 | 121 | local groupmobj = source.sprmdl[i].mobj 122 | local offset = source.sprmdl[i].offset or {x = 0, y = 0, z = 0} -- TODO: be able to exclude each axis; default to 0 123 | local direction = source.sprmdl[i].angle or 0 124 | local rotation = source.sprmdl[i].rotation or 0 125 | 126 | -- Follow the source angle + independent angle 127 | groupmobj.angle = source.angle+FixedAngle(direction*FRACUNIT) 128 | 129 | -- Set scale to source scale 130 | groupmobj.scale = source.scale 131 | 132 | -- Run a callback function to edit one or all items 133 | if (func) then 134 | do 135 | func(source.sprmdl[i]) 136 | end 137 | end 138 | 139 | -- Update the position of all group items to be relative to the mobj angle + offsets (and scaling!) 140 | P_TeleportMove( 141 | groupmobj, 142 | source.x+FixedMul(offset.x*cos(source.angle+FixedAngle(rotation*FRACUNIT)), groupmobj.scale), 143 | source.y+FixedMul(offset.y*sin(source.angle+FixedAngle(rotation*FRACUNIT)), groupmobj.scale), 144 | source.z+FixedMul(offset.z*FRACUNIT, groupmobj.scale)) 145 | end 146 | 147 | end 148 | 149 | -- Expose globals 150 | rawset(_G, "P_BuildSpriteMdl", P_BuildSpriteMdl) 151 | rawset(_G, "P_UpdateSpriteMdl", P_UpdateSpriteMdl) 152 | 153 | rawset(_G, "P_BuildSpriteModel", P_BuildSpriteModel) 154 | rawset(_G, "P_UpdateSpriteModel", P_UpdateSpriteModel) -------------------------------------------------------------------------------- /game/customsave_io.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * customsave_io.lua 3 | * (sprkizard) 4 | * (June 11, 2020 16:08) 5 | * Desc: Library for Saving/Loading data into a custom file in SRB2 I/O 6 | https://wiki.srb2.org/wiki/User:Rapidgame7/iodocs 7 | 8 | * Usage: Refer to - https://github.com/sprkizard/srb2-lua-lib/wiki/I-O-Easy-Custom-Save-Files 9 | ]] 10 | 11 | -- Create global name for save functions 12 | rawset(_G, "SaveData_I", {}) 13 | 14 | 15 | 16 | 17 | -- Converts a Lua table to a text representation. 18 | local function Serialize(input) 19 | if type(input) == "string" then 20 | return "[[" .. input:gsub("](%--)]", "]-%1]") .. "]]" 21 | elseif type(input) == "number" then 22 | return "[" .. input .. "]" 23 | elseif type(input) == "boolean" then 24 | if input then 25 | return "T" 26 | else 27 | return "F" 28 | end 29 | elseif type(input) == "nil" then 30 | return "N" 31 | elseif type(input) == "table" then 32 | 33 | local str = "{" 34 | 35 | local delimit = false 36 | for k,v in pairs(input) do 37 | if delimit then 38 | str = $ .. ";" 39 | end 40 | 41 | str = $ .. Serialize(k) .. ":" .. Serialize(v) 42 | 43 | delimit = true 44 | end 45 | 46 | str = $ .. "}" 47 | return str 48 | 49 | else 50 | error("table contains a non-serializable element!") 51 | end 52 | end 53 | 54 | -- Converts the text representation of a Lua table back to the original table. 55 | local function Deserialize(input) 56 | local output 57 | 58 | if input:sub(1, 2) == "[[" then -- string 59 | local endpos = input:find("]]") 60 | output = input:sub(3, endpos-1):gsub("]%-(%--)]", "]%1]") 61 | input = $:sub(endpos+2) 62 | elseif input:sub(1, 1) == "[" then -- number 63 | local endpos = input:find("]") 64 | output = tonumber(input:sub(2, endpos-1)) 65 | input = $:sub(endpos+1) 66 | elseif input:sub(1, 1) == "T" then -- true 67 | output = true 68 | input = $:sub(2) 69 | elseif input:sub(1, 1) == "F" then -- false 70 | output = false 71 | input = $:sub(2) 72 | elseif input:sub(1, 1) == "N" then -- nil 73 | output = nil 74 | input = $:sub(2) 75 | elseif input:sub(1, 2) == "{}" then -- empty table 76 | output = {} 77 | input = $:sub(3) 78 | elseif input:sub(1, 1) == "{" then -- table 79 | output = {} 80 | 81 | while input:sub(1, 1) ~= "}" do 82 | local key, value 83 | 84 | key, input = Deserialize(input:sub(2)) 85 | if input:sub(1, 1) ~= ":" then 86 | error("string is badly formatted! "..input) 87 | end 88 | 89 | value, input = Deserialize(input:sub(2)) 90 | 91 | output[key] = value 92 | end 93 | 94 | input = $:sub(2) 95 | else 96 | error("string is badly formatted! "..input) 97 | end 98 | 99 | if input:len() then 100 | return output, input 101 | else 102 | return output 103 | end 104 | end 105 | 106 | 107 | 108 | 109 | -- Simple encrtyps/decrypts a text string or file 110 | local function convert(chars,dist,inv) 111 | local charInt = string.byte(chars); 112 | for i=1,dist do 113 | if(inv)then charInt = charInt - 1; else charInt = charInt + 1; end 114 | if(charInt<32)then 115 | if(inv)then charInt = 126; else charInt = 126; end 116 | elseif(charInt>126)then 117 | if(inv)then charInt = 32; else charInt = 32; end 118 | end 119 | end 120 | return string.char(charInt); 121 | end 122 | 123 | local function crypt(str,k,inv) 124 | local enc= ""; 125 | for i=1,#str do 126 | if(#str-k[5] >= i or not inv)then 127 | for inc=0,3 do 128 | if(i%4 == inc)then 129 | enc = enc .. convert(string.sub(str,i,i),k[inc+1],inv); 130 | break; 131 | end 132 | end 133 | end 134 | end 135 | if(not inv)then 136 | for i=1,k[5] do 137 | enc = enc .. string.char(P_RandomRange(32,126)); 138 | end 139 | end 140 | return enc; 141 | end 142 | 143 | local enc1 = {1,2,3,4,0}; 144 | local enc2 = {124,533,663,123,27}; 145 | 146 | 147 | 148 | 149 | 150 | 151 | -- Reading data 152 | 153 | -- Checks if a local file exists 154 | function SaveData_I.FileExists(fileStr) 155 | 156 | local f = io.openlocal(fileStr, "r") 157 | 158 | if f ~= nil then 159 | f:close() 160 | return true 161 | else 162 | return false 163 | end 164 | end 165 | 166 | -- Reads a saved table into a variable from a file path 167 | function SaveData_I.ReadSaveFile(fileStr, decrypt) 168 | 169 | local file = io.openlocal(fileStr) -- TODO: try regular open() 170 | 171 | if (file == nil) then 172 | print("Save Data is either missing or corrupt.") 173 | -- file:close() 174 | else 175 | local loaded_file = file:read("*a") 176 | 177 | file:close() 178 | 179 | if (decrypt) then 180 | return Deserialize(crypt(loaded_file, enc2, true)) 181 | else 182 | return Deserialize(loaded_file) 183 | end 184 | end 185 | 186 | return false 187 | end 188 | 189 | 190 | 191 | -- Writing data 192 | 193 | -- Writes a save table to the specified file path 194 | function SaveData_I.WriteSaveFile(fileStr, dataTable, encrypt) 195 | 196 | local file = io.openlocal(fileStr, "w+") -- TODO: try regular open() 197 | 198 | if (encrypt) then 199 | file:write(crypt(Serialize(data), enc2)) 200 | else 201 | file:write(Serialize(dataTable)) 202 | end 203 | 204 | file:flush() 205 | 206 | file:close() 207 | end 208 | 209 | 210 | -- Testing Commands 211 | -- (Each command will write/write a test file with a sample table) 212 | 213 | local devsave = { 214 | One = "One", 215 | Two = 2, 216 | Three = False, 217 | Four = {}, 218 | } 219 | 220 | -- Reads the test file, and prints its data 221 | COM_AddCommand("readfile", function() 222 | 223 | -- if titlemapinaction and not (netgame or multiplayer) then 224 | devsave = I_ReadSaveFile("iosave_test.txt") 225 | 226 | for k,v in pairs(devsave) do 227 | print(k,v) 228 | end 229 | -- end 230 | end) 231 | 232 | -- Writes a test file with a sample table 233 | COM_AddCommand("writefile", function() 234 | 235 | -- if titlemapinaction and not (netgame or multiplayer) then 236 | I_WriteSaveFile("iosave_test.txt", devsave) 237 | -- end 238 | end) 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /mobj/l_frameanimate.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * frameanimate.lua 3 | * (Author: sprki_zard) 4 | * (June 6, 2020 23:29) 5 | * Desc: n/a 6 | * 7 | * Notes: n/a 8 | ]] 9 | rawset(_G, "FrameAnim", {}) 10 | 11 | FrameAnim.playing = {} 12 | 13 | 14 | -- Adds a new animation to the playing animation list 15 | function FrameAnim.add(animname, animator, nsprite, startf, endf, args) 16 | 17 | -- Duplicate animation? Set it to be replaced immediately 18 | for i,anim in ipairs(FrameAnim.playing) do 19 | if (anim.name == animname) then 20 | anim.deleted = true 21 | end 22 | end 23 | 24 | -- Start by setting the initial sprite and frame 25 | animator.sprite = nsprite 26 | animator.frame = R_Char2Frame(startf) --startf 27 | 28 | -- Throw it into the playing list 29 | table.insert(FrameAnim.playing, 30 | { 31 | name=animname, 32 | mobj=animator, 33 | sprite=nsprite, 34 | startframe=R_Char2Frame(startf), --startf, 35 | endframe=R_Char2Frame(endf), --endf, 36 | loop=(args and args.loop) or ((R_Char2Frame(endf)-R_Char2Frame(startf)) * ((args and args.delay) or 1)), -- if no loop, set to end frame (ef*delay for slower animations) 37 | frameskip=(args and args.frameskip) or 1, -- TODO: implement frameskip 38 | delay=(args and args.delay) or 1, 39 | flag=(args and args.flag) or 0, -- TODO: sprite flag that applies to separate frames. currently does not work. 40 | -- spritelist = {}, -- TODO: separate flags per frame? 41 | paused=false, 42 | deleted=false, 43 | }) 44 | -- print("Inserted Animation: "..animname) 45 | end 46 | 47 | -- Seeks an animation and runs a function when found (also returns true if exists, false if not) 48 | function FrameAnim.seek(animname, callback) 49 | for i,anim in ipairs(FrameAnim.playing) do 50 | if (anim.name == animname) then 51 | if (callback) then 52 | do callback(anim) end 53 | end 54 | return true 55 | end 56 | end 57 | return false 58 | end 59 | 60 | -- Removes one or all animations 61 | function FrameAnim.remove(animname, removeall) 62 | if (removeall) then 63 | for i=1, #FrameAnim.playing do 64 | FrameAnim.playing[i].deleted = true 65 | end 66 | else 67 | FrameAnim.seek(animname, function(an) an.deleted = true end) 68 | end 69 | end 70 | 71 | -- Pauses an animation 72 | function FrameAnim.pause(animname, ispaused) 73 | FrameAnim.seek(animname, function(an) an.paused = ispaused end) 74 | end 75 | 76 | -- Checks if an animation just finished 77 | function FrameAnim.checkexisting(animname) 78 | return FrameAnim.seek(animname) 79 | end 80 | 81 | -- ================ 82 | -- Event Extension 83 | -- Checks if a certain script is loaded to extend some of it's features 84 | function FrameAnim.EventExists() 85 | if not _G["Event"] then print("(!)\x82 This function requires 'Event' to work!") return false else return true end 86 | end 87 | 88 | -- Add a custom wrapper event + others when the companion script exists 89 | if FrameAnim.EventExists() then 90 | 91 | -- Suspends an event until the animation ends 92 | function FrameAnim.waitframedone(event, animname) 93 | if (FrameAnim.checkexisting(animname)) then Event.pause(event) else Event.resume(event) end 94 | end 95 | -- else 96 | -- local handler_str = "(!)\x82 Function [FrameAnim.waitframedone] requires 'Event' to work, is it available?" 97 | 98 | -- function FrameAnim.waitframedone() 99 | -- print(handler_str) 100 | -- end 101 | end 102 | 103 | -- ================ 104 | 105 | 106 | -- The thinker function that handles animations 107 | function FrameAnim.Thinker() 108 | 109 | for i=1, #FrameAnim.playing do 110 | 111 | local anim = FrameAnim.playing[i] 112 | 113 | -- Remove if animation is set to be deleted 114 | if (anim and anim.deleted) then 115 | table.remove(FrameAnim.playing, i) 116 | end 117 | 118 | -- no mobj means to not continue 119 | if anim and not (anim.mobj and anim.mobj.valid) then 120 | anim.deleted = true 121 | end 122 | 123 | -- Continue next iteration if animation is set to be deleted, otherwise continue 124 | (function() 125 | if (anim and not anim.deleted) then 126 | 127 | -- Set sprite 128 | anim.mobj.sprite = anim.sprite 129 | 130 | -- the animation is paused 131 | if (anim.paused) then return end 132 | 133 | if (anim.loop > 0) then 134 | 135 | -- Play the sprite's frames by set speed and delay 136 | if (leveltime % anim.delay == 0) then 137 | anim.mobj.frame = ($1+1 * anim.frameskip)|anim.flag 138 | end 139 | 140 | -- Reset the framecount to the beginning 141 | if (anim.mobj.frame > anim.endframe) then 142 | anim.mobj.frame = anim.startframe 143 | end 144 | 145 | anim.loop = $1-1 146 | else 147 | -- When our sprite loop time has ended, set the animation to be removed 148 | anim.deleted = true 149 | -- print(string.format("animation [%s] finished", anim.name)) 150 | end 151 | 152 | --[[-- Play the sprite's frames by set speed and delay 153 | if (leveltime % anim.delay == 0) then 154 | anim.mobj.frame = $1+1 * anim.speed 155 | end 156 | 157 | -- Reset the framecount to the beginning 158 | if (anim.mobj.frame > anim.endframe) then 159 | anim.mobj.frame = anim.startframe 160 | end 161 | 162 | -- When our sprite loop time has ended, set the animation to be removed 163 | if (anim.loop <= 0) then 164 | anim.deleted = true 165 | -- print("animation finished") 166 | return 167 | else 168 | anim.loop = $1-1 169 | end--]] 170 | end 171 | end)() 172 | end 173 | 174 | end 175 | 176 | function FrameAnim.netvars(n) 177 | FrameAnim.playing = n($) 178 | 179 | --[[local a = #FrameAnim.playing 180 | a = n(a) 181 | for i = 1, a do 182 | FrameAnim.playing[i] = n($) 183 | end--]] 184 | end 185 | 186 | 187 | 188 | -- Uncomment for Example 189 | --[[addHook("NetVars", function(network) 190 | FrameAnim.netvars(network) 191 | end) 192 | 193 | addHook("ThinkFrame", function() 194 | FrameAnim.Thinker() 195 | end)--]] 196 | --[[addHook("ThinkFrame", function() 197 | 198 | -- Checks for existing 199 | print(FrameAnim.checkexisting("existing")) 200 | 201 | if leveltime == 2*TICRATE then 202 | local s = P_SpawnMobj(-64*FU, -96*FU, 32*FU, MT_PULL) 203 | -- FrameAnim.add("name", s, SPR_EGGM, A, F) 204 | -- FrameAnim.add("name", s, SPR_EGGM, V, W, {loop=2*TICRATE}) 205 | -- FrameAnim.add("name", s, SPR_PIKE, A, P) 206 | FrameAnim.add("name", s, SPR_GFZD, R_Frame2Char(0), R_Frame2Char(31), {loop=10*TICRATE}) 207 | end 208 | 209 | if leveltime == 7*TICRATE then 210 | -- FrameAnim.pause("name", true) 211 | end 212 | 213 | if leveltime == 10*TICRATE then 214 | -- FrameAnim.remove("name", true) 215 | end 216 | 217 | if leveltime == 8*TICRATE then 218 | local s = P_SpawnMobj(-64*FU, -96*FU, 64*FU, MT_PULL) 219 | FrameAnim.add("existing", s, SPR_EGGM, "V", "W", {loop=8*TICRATE, delay=15}) 220 | end 221 | end) 222 | --]] -------------------------------------------------------------------------------- /mobj/l_atmosparticle.lua: -------------------------------------------------------------------------------- 1 | 2 | freeslot( 3 | "MT_ATMOSPARTICLE", 4 | "S_ATMOSPARTICLE" 5 | ) 6 | 7 | freeslot("SPR_BPRT") 8 | 9 | 10 | mobjinfo[MT_ATMOSPARTICLE] = { 11 | doomednum = -1, 12 | spawnhealth = 8*TICRATE, 13 | spawnstate = S_ATMOSPARTICLE, 14 | speed = 8, 15 | radius = 32*FRACUNIT, 16 | height = 32*FRACUNIT, 17 | mass = 100, 18 | reactiontime = 10*TICRATE, 19 | flags = MF_NOGRAVITY|MF_NOCLIP|MF_NOBLOCKMAP|MF_NOCLIPHEIGHT 20 | } 21 | states[S_ATMOSPARTICLE] = {SPR_THOK,A,-1,nil,0,0,S_NULL} 22 | -- states[S_ATMOSPARTICLE] = {SPR_BPRT,A|FF_FULLBRIGHT|FF_ANIMATE|FF_TRANS30,-1,A_None,5,2,S_NULL} 23 | 24 | addHook("MobjSpawn", function(mo) 25 | mo._dist = 512 26 | mo._roll = 0 27 | end, MT_ATMOSPARTICLE) 28 | 29 | addHook("MobjThinker", function(mo) 30 | if mo.valid then 31 | mo.health = max($1-1, 0) 32 | -- TODO: mobj._roll, scale, and splat/papersprite 33 | mo.destscale = FRACUNIT/8 34 | mo.scalespeed = FRACUNIT/64 35 | --print(mo.health) 36 | if mo._roll then mo.rollangle = $1+mo._roll end 37 | 38 | if not mo.health then 39 | P_RemoveMobj(mo) 40 | --mo.state = S_NULL 41 | -- Destroy any past distance threshold 42 | elseif R_PointToDist(mo.x, mo.y) > mo._dist*FRACUNIT then 43 | P_RemoveMobj(mo) 44 | end 45 | end 46 | end, MT_ATMOSPARTICLE) 47 | 48 | 49 | 50 | local function P_CreateEmitter(spawner, settings) 51 | 52 | local x,y,z = spawner.x, spawner.y, spawner.z 53 | 54 | -- The maximium amount of distance the spawner stays is in 55 | local distance_threshold = (settings and settings.maxdist) or 512 56 | 57 | -- The range of where objects spawn from the spawner 58 | local maxrad = (settings and settings.maxrad) or 256 59 | 60 | -- The height range of both the top and bottom of the spawner 61 | local maxceil = (settings and settings.ceil) or 128 62 | local maxfloor = (settings and settings.floor) or -32 63 | 64 | -- If specified, replace the default particle mobj with your own 65 | local particlemo = (settings and settings.mobj) 66 | 67 | -- The amount of time each particle lasts 68 | local decay = (settings and settings.decay) or 8*TICRATE 69 | 70 | -- Set emitter coords relative to in front of spawner (default distance was -32..) 71 | local spawnx = x + 0*cos(spawner.angle+FixedAngle(ANGLE_90)) 72 | local spawny = y + 0*sin(spawner.angle+FixedAngle(ANGLE_90)) 73 | 74 | -- Set up particle spawn below view distance threshold 75 | -- if R_PointToDist(spawnx, spawny) < distance_threshold*FRACUNIT then 76 | -- local particle = P_SpawnMobj( 77 | -- x + cos(spawner.angle) + P_RandomRange(-maxrad, maxrad)*FRACUNIT, 78 | -- y + sin(spawner.angle) + P_RandomRange(-maxrad, maxrad)*FRACUNIT, 79 | -- z + P_RandomRange(maxfloor, maxceil)*FRACUNIT, MT_ATMOSPARTICLE) 80 | -- P_SpawnMobj(spawnx, spawny, spawner.z, MT_THOK) 81 | if R_PointToDist(spawnx, spawny) < distance_threshold*FRACUNIT then 82 | 83 | local rx = P_RandomRange(-maxrad, maxrad)*FRACUNIT 84 | local ry = P_RandomRange(-maxrad, maxrad)*FRACUNIT 85 | local rz = P_RandomRange(maxfloor, maxceil)*FRACUNIT 86 | 87 | local particle = P_SpawnMobj( 88 | -- x + cos(spawner.angle) + P_RandomRange(-maxrad, maxrad)*FRACUNIT, 89 | -- y + sin(spawner.angle) + P_RandomRange(-maxrad, maxrad)*FRACUNIT, 90 | -- spawnx + P_RandomRange(-maxrad, maxrad)*FRACUNIT, 91 | -- spawny + P_RandomRange(-maxrad, maxrad)*FRACUNIT, 92 | -- z + P_RandomRange(maxfloor, maxceil)*FRACUNIT, MT_ATMOSPARTICLE) 93 | spawnx + rx, 94 | spawny + ry, 95 | z + rz, particlemo or MT_ATMOSPARTICLE) 96 | 97 | -- Sync object settings 98 | particle._dist = distance_threshold 99 | particle._roll = (settings and settings.roll) or 0 100 | particle.health = decay 101 | --[[local particle = P_SpawnMobj(player.mo.x + P_RandomRange(-256, 256)*cos(player.mo.angle+FixedAngle(P_RandomRange(-32, 32)*FRACUNIT)), 102 | player.mo.y + P_RandomRange(-256, 256)*sin(player.mo.angle+FixedAngle(P_RandomRange(-32, 32)*FRACUNIT)), 103 | player.mo.z + P_RandomRange(-32, 128)*FRACUNIT, MT_ATMOSPARTICLE)]] 104 | 105 | -- Be able to customize what your particles can do 106 | if (settings and settings.func) then 107 | settings.func(particle) 108 | -- Ex: Keep momentum with movement using momx and momy 109 | end 110 | 111 | -- Destroy any past distance threshold 112 | --[[if R_PointToDist(particle.x, particle.y) > distance_threshold*FRACUNIT then 113 | P_RemoveMobj(particle) 114 | end]] 115 | end 116 | 117 | end 118 | 119 | rawset(_G, "P_CreateEmitter", P_CreateEmitter) 120 | 121 | 122 | -- local particle_viewplayer = nil 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | --[[local function viewPlayerCheck(v, stplyr, cam) 135 | particle_viewplayer = stplyr 136 | end 137 | hud.add(viewPlayerCheck, "game")--]] 138 | 139 | -- addHook("ThinkFrame", do 140 | 141 | -- for player in players.iterate 142 | 143 | -- local x,y,z = player.mo.x, player.mo.y, player.mo.z 144 | -- local distance_threshold = 512 145 | -- if (particle_viewplayer == player) then 146 | 147 | -- local spawnx = player.mo.x + 32*cos(player.mo.angle+FixedAngle(ANGLE_90)) 148 | -- local spawny = player.mo.y + 32*sin(player.mo.angle+FixedAngle(ANGLE_90)) 149 | 150 | -- -- Set up particle spawn below distance threshold 151 | -- if R_PointToDist(spawnx, spawny) < distance_threshold*FRACUNIT then 152 | -- local particle = P_SpawnMobj(player.mo.x + cos(player.mo.angle) + P_RandomRange(-256, 256)*FRACUNIT, 153 | -- player.mo.y + sin(player.mo.angle) + P_RandomRange(-256, 256)*FRACUNIT, 154 | -- player.mo.z + P_RandomRange(-32, 128)*FRACUNIT, MT_ATMOSPARTICLE) 155 | -- --[[local particle = P_SpawnMobj(player.mo.x + P_RandomRange(-256, 256)*cos(player.mo.angle+FixedAngle(P_RandomRange(-32, 32)*FRACUNIT)), 156 | -- player.mo.y + P_RandomRange(-256, 256)*sin(player.mo.angle+FixedAngle(P_RandomRange(-32, 32)*FRACUNIT)), 157 | -- player.mo.z + P_RandomRange(-32, 128)*FRACUNIT, MT_ATMOSPARTICLE)]] 158 | -- particle.momz = $1+2*FRACUNIT 159 | 160 | -- -- Keep momentum with movement 161 | -- particle.momx = player.mo.momx 162 | -- particle.momy = player.mo.momy 163 | 164 | -- -- Destroy any past distance threshold 165 | -- --[[if R_PointToDist(particle.x, particle.y) > distance_threshold*FRACUNIT then 166 | -- P_RemoveMobj(particle) 167 | -- end]] 168 | -- end 169 | 170 | -- end 171 | -- end 172 | -- end) 173 | 174 | --[[ -- Particle Reference Code 175 | local spawnx = player.mo.x + 32*cos(player.mo.angle+FixedAngle(ANGLE_90)) 176 | local spawny = player.mo.y + 32*sin(player.mo.angle+FixedAngle(ANGLE_90)) 177 | if R_PointToDist(spawnx, spawny) < distance_threshold*FRACUNIT then 178 | local particle = P_SpawnMobj(player.mo.x + P_RandomRange(-256, 256)*cos(player.mo.angle+FixedAngle(0)), 179 | player.mo.y + P_RandomRange(-256, 256)*sin(player.mo.angle+FixedAngle(0)), 180 | player.mo.z + P_RandomRange(-32, 32)*FRACUNIT, MT_THOK) 181 | particle.momz = $1+2*FRACUNIT 182 | end 183 | ]] -------------------------------------------------------------------------------- /hud/l_hitdisplay.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_hitdisplay.lua 3 | * (sprkizard) 4 | * (Jun 8, 2022 17:27) 5 | * Desc: Custom boss meter display 6 | 7 | 8 | * Usage: TODO 9 | ]] 10 | 11 | 12 | -- TODO: recover amount display - drain(+)/flash() damagetype - start sound - force flash damage 13 | rawset(_G, "HitDisplay", {}) 14 | 15 | local health_tab = {} 16 | 17 | 18 | local function altval(v, av) 19 | return (v and not nil) and v or av 20 | end 21 | 22 | local function clamp(v, i, o) 23 | return min(max(v, i), o) 24 | end 25 | 26 | 27 | function HitDisplay.shownew(mo, disparg) 28 | 29 | local disp = {} 30 | 31 | disparg = altval(disparg, {}) 32 | 33 | disp["mobj"] = mo 34 | disp["identifier"] = disparg.identifier or "hitdisplay" 35 | -- Drawing Origin 36 | disp["drawx"] = altval(disparg.drawx, 8) 37 | disp["drawy"] = altval(disparg.drawy, 96) 38 | -- HP drawing offset / graphics / empty flag / width scale / maxhealth 39 | disp["hp_drawx"] = altval(disparg.hp_drawx, 0) 40 | disp["hp_drawy"] = altval(disparg.hp_drawy, 0) 41 | disp["hpfill"] = disparg.hpfill or "CROSHAI3" 42 | disp["hphurt"] = disparg.hphurt or "CROSHAI3" 43 | disp["hpempty"] = disparg.hpempty or "CROSHAI1" 44 | disp["hpempty_flags"] = disparg.hpempty_flags or 0 45 | disp["boxwidth"] = altval(disparg.boxwidth, 16) 46 | disp["boxheight"] = altval(disparg.boxheight, 1) 47 | disp["maxhealth"] = altval(disparg.maxhealth, 8) -- mo.info.spawnhealth 48 | -- Enemy name / text alignment / drawing offset 49 | disp["enemyname"] = disparg.enemyname or "ENEMY" 50 | disp["enemyname_align"] = disparg.enemyname_align or "left" 51 | disp["name_drawx"] = altval(disparg.name_drawx, 0) 52 | disp["name_drawy"] = altval(disparg.name_drawy, -12) 53 | -- Border graphic 54 | disp["border"] = disparg.border or "CROSHAI1" 55 | 56 | -- removes meter on 0 automatically 57 | disp["removeonempty"] = disparg.removeonempty 58 | 59 | -- Hurt timer tree reference variable / sliding damage value 60 | disp["hurtref"] = disparg.hurtref 61 | disp["dmgref"] = altval(mo.health, 0) 62 | 63 | -- First time use 64 | disp["setup"] = true 65 | disp["fillref"] = 0 66 | disp["displaysound"] = disparg.displaysound or sfx_ding 67 | 68 | -- Extra vars 69 | disp["var1"] = disparg.var1 70 | disp["var2"] = disparg.var2 71 | 72 | table.insert(health_tab, disp) 73 | 74 | return HitDisplay 75 | end 76 | 77 | -- removes by identifier or enemyname 78 | function HitDisplay.remove(identifier, byenemyname) 79 | for i=1, #health_tab do 80 | 81 | local t = health_tab[i] 82 | 83 | if (byenemyname and t["enemyname"] == identifier) 84 | or (t["identifier"] == identifier) then 85 | table.remove(health_tab, i) 86 | end 87 | end 88 | end 89 | 90 | function HitDisplay.clearall() 91 | for i=1, #health_tab do 92 | table.remove(health_tab, i) 93 | end 94 | end 95 | 96 | -- Shortcut to remove and add a new meter 97 | function HitDisplay.refill(identifier, byenemyname, mo, disparg) 98 | HitDisplay.remove(identifier, byenemyname) 99 | HitDisplay.shownew(mo, disparg) 100 | end 101 | 102 | -- Shortcut to just set mobj health 103 | function HitDisplay.sethealth(mobj, newhealth, relative) 104 | if mobj then 105 | mobj.health = (relative) and $1+newhealth or newhealth 106 | end 107 | end 108 | 109 | function HitDisplay.meterdisplay(v, stplyr) 110 | 111 | for i=1, #health_tab do 112 | local meow = health_tab[i] 113 | 114 | -- automatic removal on empty (TODO: thinkframe?) 115 | if meow.removeonempty and meow.mobj.health <= 0 then table.remove(health_tab, i) end 116 | 117 | -- Drawing start origin 118 | local startx = meow.drawx 119 | local starty = meow.drawy 120 | -- HP offset 121 | local hpx = (meow.drawx+meow.hp_drawx) 122 | local hpy = (meow.drawy+meow.hp_drawy) 123 | -- HP width calc 124 | local hpfillwidth = FixedFloor(FU*(meow.mobj.health*meow.boxwidth/meow.maxhealth)) 125 | local dmgfillwidth = FixedFloor(FU*(meow.dmgref*meow.boxwidth/meow.maxhealth)) 126 | -- Name offset 127 | local namex = (meow.drawx+meow.name_drawx) 128 | local namey = (meow.drawy+meow.name_drawy) 129 | -- Name string 130 | local enemyname = meow.enemyname 131 | -- cached graphics 132 | local enemywindow_g = v.cachePatch(meow.border) 133 | local enemyfill_g = v.cachePatch(meow.hpfill) 134 | local enemyhurt_g = v.cachePatch(meow.hphurt) 135 | local enemyempty_g = v.cachePatch(meow.hpempty) 136 | -- Toggles when character limit goes past 13 137 | local thintoggle = (enemyname:len() > 13) and "thin-" or "" 138 | 139 | 140 | -- Damage color (Background) 141 | v.drawStretched(FU*hpx, FU*hpy, FU*(meow.boxwidth), FU*meow.boxheight, enemyempty_g, meow.hpempty_flags, v.getColormap(1)) 142 | 143 | -- Reference damage to ease to when damage is added or subtracted (Hurt timer mandatory) 144 | if meow.dmgref < meow.mobj.health and not meow.mobj[meow.hurtref] then 145 | meow.dmgref = $ + 1 146 | elseif meow.dmgref > meow.mobj.health and not meow.mobj[meow.hurtref] then 147 | meow.dmgref = $ - 1 148 | end 149 | 150 | -- Damage color (Reference) 151 | v.drawStretched(FU*hpx, FU*hpy, abs(dmgfillwidth), FU*meow.boxheight, enemyhurt_g, 0, v.getColormap(1)) 152 | 153 | -- Damage color (Main) 154 | if (meow.mobj.health > 0) then 155 | v.drawStretched(FU*hpx, FU*hpy, abs(hpfillwidth), FU*meow.boxheight, enemyfill_g, 0, v.getColormap(1)) 156 | end 157 | 158 | -- Enemy Border 159 | v.drawStretched(FU*meow.drawx, FU*meow.drawy, FU, FU, enemywindow_g, 0, v.getColormap(1)) 160 | 161 | -- Enemy Name 162 | v.drawString(namex, namey, enemyname, V_ALLOWLOWERCASE, thintoggle .. meow.enemyname_align) 163 | 164 | 165 | -- Fill startup 166 | if (meow.fillref < meow.mobj.health and meow.setup) 167 | 168 | -- Scales amount based on boxwidth and maximum health 169 | -- TODO: properly insert formula to account for values > 45, < 45 to fill 1.2 seconds evenly 170 | meow.fillref = $1+(1+(meow.maxhealth/meow.boxwidth)*2) 171 | 172 | -- Sound plays at different rate 173 | if (leveltime*2) % 5 < 2 then S_StartSound(nil, meow.displaysound, stplyr) end 174 | 175 | v.drawStretched(FU*hpx, FU*hpy, FU*(meow.boxwidth), FU*meow.boxheight, enemyempty_g, 0, v.getColormap(1)) 176 | v.drawStretched(FU*hpx, FU*hpy, FU*(meow.fillref*meow.boxwidth/meow.maxhealth), FU*meow.boxheight, enemyfill_g, 0, v.getColormap(1)) 177 | 178 | meow.setup = (meow.fillref > meow.mobj.health) and false or true 179 | end 180 | 181 | -- v.drawFill(320/2, 160/2, (meow.extra and meow.mobj[meow.extra].health or meow.mobj.health), 6, 35) 182 | -- v.drawFill(320/2, 160/2, meow.health/meow.maxhealth, 6, 35) 183 | 184 | -- v.drawString(96, 96, string.format("%d / %d", meow.mobj.health, meow.maxhealth), 0, "left") 185 | -- v.drawString(96, 96, ((leveltime*2) % 5), 0, "left") 186 | end 187 | end 188 | 189 | function HitDisplay.netvars(n) 190 | health_tab = n($) 191 | end 192 | 193 | 194 | 195 | hud.add(HitDisplay.meterdisplay, "game") 196 | addHook("MapLoad", HitDisplay.clearall) 197 | addHook("MapChange", HitDisplay.clearall) 198 | addHook("NetVars", HitDisplay.netvars) 199 | 200 | -------------------------------------------------------------------------------- /mobj/l_mobjmover.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * mobjmover.lua 3 | * (Author: sprki_zard) 4 | * (June 13, 2020 00:24) 5 | * Desc: n/a 6 | * 7 | * Notes: n/a 8 | ]] 9 | 10 | rawset(_G, "MobjMover", {travelling={}}) 11 | 12 | -- Sets up a new mover (pls be stable and worth using) 13 | function MobjMover.moveto(n, source, mtarget, args) 14 | 15 | local dist = FixedHypot(mtarget.z-source.z, 16 | FixedHypot(mtarget.x-source.x, 17 | mtarget.y-source.y)) 18 | 19 | local tspeed = (args and args.speed) or 8*FU 20 | local _movepercent = FixedDiv(tspeed, dist) 21 | 22 | -- End duplicates 23 | for i,mover in ipairs(MobjMover.travelling) do 24 | if (mover.name == n) then 25 | mover.ended = true 26 | end 27 | end 28 | 29 | local mt = { 30 | name=n, 31 | mobj=source, 32 | angle=args and args.angle, 33 | noangle=args and args.noangle, 34 | startpos={x=source.x, y=source.y, z=source.z}, 35 | ease=args and args.ease or "linear", 36 | arc=args and args.arc, 37 | target=mtarget, 38 | speed=tspeed, 39 | movepercent=_movepercent, 40 | movetime=0, 41 | stopped=false, 42 | ended=false, 43 | } 44 | 45 | -- (This will be ignored if the companion script does not exist at all) 46 | -- Add a reference to a target event if the library exists 47 | if ((args and args.eventref) and MobjMover.EventExists()) then 48 | mt.eventref = args.eventref 49 | end 50 | 51 | table.insert(MobjMover.travelling, mt) 52 | end 53 | 54 | function MobjMover.seek(n, callback) 55 | for i,mover in ipairs(MobjMover.travelling) do 56 | if (mover.name == n) then 57 | if (callback) then 58 | do callback(mover) end 59 | end 60 | return true 61 | end 62 | -- TODO: no requiring name case to affect all movers 63 | end 64 | return false 65 | end 66 | 67 | -- Removes one or all animations 68 | function MobjMover.stop(n, removeall) 69 | if (removeall) then 70 | for i=1, #MobjMover.travelling do 71 | MobjMover.travelling[i].ended = true 72 | end 73 | else 74 | MobjMover.seek(n, function(mv) mv.ended = true end) 75 | end 76 | end 77 | 78 | -- Pause 79 | function MobjMover.pause(n, isstopped) 80 | MobjMover.seek(n, function(mv) mv.stopped = isstopped end) 81 | end 82 | 83 | -- Checks if object is still travelling 84 | function MobjMover.ismoving(n) 85 | return MobjMover.seek(n) 86 | end 87 | 88 | -- ================ 89 | -- Event Extension 90 | -- Checks if a certain script is loaded to extend some of it's features 91 | function MobjMover.EventExists() 92 | if not _G["Event"] then print("(!)\x82 This function requires 'Event' to work!") return false else return true end 93 | end 94 | 95 | -- Add a custom wrapper event + others when the companion script exists 96 | if MobjMover.EventExists() then 97 | 98 | -- Check if an object is still moving 99 | function MobjMover.waitmovedone(event, n) 100 | if (MobjMover.ismoving(n) and MobjMover.EventExists()) then Event.pause(event) else Event.resume(event) end 101 | end 102 | 103 | -- Sets a mobj moveto target with the ability to stop an event 104 | -- until movement is finished when the library is added 105 | -- (Replacement for Event.newsub moveto calls) 106 | function MobjMover.moveto_ev(n, source, mtarget, args) 107 | Event.start("_ev_mobjmover", {movername=n, moversource=source, movertarget=mtarget, moverargs=args or {}}) 108 | end 109 | 110 | Event.new("_ev_mobjmover", { 111 | function(c, e) 112 | c.moverargs.eventref = e -- set self ref 113 | MobjMover.moveto(c.movername, c.moversource, c.movertarget, c.moverargs) 114 | end}) 115 | 116 | end 117 | 118 | 119 | -- ================ 120 | 121 | function MobjMover.Thinker() 122 | 123 | for i=1, #MobjMover.travelling do 124 | 125 | local mover = MobjMover.travelling[i] 126 | 127 | -- Remove if set to be ended 128 | if (mover and mover.ended) then 129 | 130 | -- event library is added: Resume event when ended to prevent locking or accidentally resuming 131 | if (mover.eventref and MobjMover.EventExists()) then 132 | Event.resume(mover.eventref) 133 | end 134 | 135 | table.remove(MobjMover.travelling, i) 136 | end 137 | 138 | -- no mobj means to not continue 139 | if mover and not (mover.mobj and mover.mobj.valid) then 140 | mover.ended = true 141 | end 142 | 143 | -- Continue next iteration if set to be ended, otherwise continue 144 | (function() 145 | if (mover and not mover.ended) then 146 | 147 | -- Lock to final target position and end the movement 148 | if (mover.movetime > FRACUNIT) then 149 | -- TODO: run a net-safe callback when ended? 150 | P_TeleportMove(mover.mobj, mover.target.x, mover.target.y, mover.target.z) 151 | mover.ended = true 152 | return 153 | end 154 | 155 | if (mover.stopped) then return end -- stop movement 156 | 157 | -- event library is added: Allow the event to pause if the mover passes the reference to it 158 | if (mover.eventref and MobjMover.EventExists()) then 159 | Event.stop(mover.eventref) 160 | end 161 | 162 | mover.movetime = $1+mover.movepercent -- percent to move per frame 163 | 164 | -- Interpolate the constant movement on all axis based on the distance 165 | local movex = ease[mover.ease](mover.movetime, mover.startpos.x, mover.target.x) 166 | local movey = ease[mover.ease](mover.movetime, mover.startpos.y, mover.target.y) 167 | local movez = ease[mover.ease](mover.movetime, mover.startpos.z, mover.target.z) 168 | local moveangle = R_PointToAngle2(mover.mobj.x, mover.mobj.y, mover.target.x, mover.target.y) 169 | 170 | -- Set mobj angle (if only a number exists :v) 171 | if (type(mover.angle) == "number") then 172 | mover.mobj.angle = mover.noangle and $1 or mover.angle 173 | else 174 | mover.mobj.angle = mover.noangle and $1 or R_PointToAngle2(mover.mobj.x, mover.mobj.y, mover.target.x, mover.target.y) 175 | end 176 | 177 | -- Move in an arc motion horizontally or vertically 178 | if mover.arc then 179 | local ang = FixedMul(ANGLE_180, mover.movetime) 180 | movex = $+P_ReturnThrustX(nil, moveangle + ANGLE_90, FixedMul((mover.arc.horz or 0), sin(ang))) 181 | movey = $+P_ReturnThrustY(nil, moveangle + ANGLE_90, FixedMul((mover.arc.horz or 0), sin(ang))) 182 | movez = $-FixedMul(mover.arc.vert or 0, sin(ang)) 183 | end 184 | 185 | -- Apply the movement 186 | P_TeleportMove(mover.mobj, 187 | movex, 188 | movey, 189 | movez 190 | ) 191 | end 192 | end)() 193 | end 194 | end 195 | 196 | function MobjMover.netvars(n) 197 | MobjMover.travelling = n($) 198 | 199 | --[[local a = #MobjMover.travelling 200 | a = n(a) 201 | for i = 1, a do 202 | MobjMover.travelling[i] = n($) 203 | end--]] 204 | end 205 | 206 | 207 | -- Example code 208 | --[[addHook("NetVars", function(network) 209 | MobjMover.netvars(network) 210 | end) 211 | 212 | addHook("ThinkFrame", function() 213 | MobjMover.Thinker() 214 | end)--]] 215 | --[[addHook("ThinkFrame", function() 216 | -- Moves directly to 0,0,56 217 | if leveltime == 2*TICRATE then 218 | server.mo.s = P_SpawnMobj(server.mo.x, server.mo.y, server.mo.z, MT_THOK) 219 | local sd = P_SpawnMobj(0, 0, 56*FU, MT_THOK) 220 | server.mo.s.tics = FU 221 | server.mo.s.sprite = SPR_EGGM 222 | sd.tics = FU 223 | sd.sprite = SPR_DRWN 224 | sd.scale = 2*FU 225 | MobjMover.moveto("test", server.mo.s, {x=0,y=0,z=56*FU}, {angle=0}) 226 | end 227 | -- Starts a new, arc movement after the first 228 | if leveltime == 6*TICRATE then 229 | local sd = P_SpawnMobj(0, 2000*FU, 56*FU, MT_THOK) 230 | sd.tics = FU 231 | sd.sprite = SPR_DRWN 232 | sd.scale = 2*FU 233 | MobjMover.moveto("test", server.mo.s, {x=0,y=2000*FU,z=56*FU}, {arc={vert=128*FU}}) 234 | end 235 | -- Pauses movement for 3 seconds 236 | if (leveltime == 10*TICRATE) then 237 | MobjMover.pause("test", true) 238 | end 239 | -- Unpauses and finishes 240 | if (leveltime == 13*TICRATE) then 241 | MobjMover.pause("test", false) 242 | end 243 | end) 244 | --]] 245 | 246 | 247 | -------------------------------------------------------------------------------- /other/l_linepathing.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | * l_linepathing.lua 4 | * (sprkizard) 5 | * (November ‎29, ‎2019) 6 | 7 | * Desc: A function made to copy Unity's Gizmos.DrawLine() method, 8 | and imitate using arrays to create a chain of Gizmo lines 9 | in the viewport 10 | https://docs.unity3d.com/ScriptReference/Gizmos.DrawLine.html 11 | 12 | * Usage: R_DrawMobjLine(from, to, params) 13 | ------ 14 | Accepts both mobj coordinates and table xyz coords for 15 | from and to destinations 16 | ( eg. R_DrawMobjLine(player.mo, mo) ) 17 | 18 | params Parameters: 19 | ({width = 64}) - The amount of object 'depth' in a line 20 | ({dots = True}) - Toggles the visibility of the from and to points 21 | ({lines = True}) - Toggles the visibility of the line paths 22 | ({lineMobj = MT_THOK}) - Changes the object used in rendering lines 23 | ({scale = FRACUNIT}) - Changes the line object scale 24 | 25 | ]] 26 | 27 | rawset(_G, "List_LinePaths", {}) 28 | 29 | local LINEPATH_MAX = 512 30 | 31 | local FF_FLAT = FF_PAPERSPRITE 32 | local FF_FLATCOLLIDER = MF_PAPERCOLLISION 33 | 34 | -- Minimum freeslot items 35 | freeslot("MT_LINEPATH", "S_LINEPATH", "SPR_SPLN") 36 | 37 | -- Railing Example 38 | freeslot("MT_FLAT", "S_FLAT", "SPR_RAIL") 39 | 40 | 41 | 42 | 43 | mobjinfo[MT_LINEPATH] = { 44 | --$Name Spline Path 45 | --$Sprite SPLN 46 | doomednum = 840, 47 | spawnhealth = 1000, 48 | spawnstate = S_LINEPATH, 49 | speed = 8, 50 | radius = 16*FRACUNIT, 51 | height = 16*FRACUNIT, 52 | damage = 0, 53 | mass = 10, 54 | flags = MF_NOGRAVITY|MF_NOCLIP|MF_NOCLIPHEIGHT, 55 | } 56 | states[S_LINEPATH] = {SPR_CEMG,0,-1,A_None,0,0,S_NULL} 57 | 58 | -- (Add a flat object for demonstration purposes) 59 | mobjinfo[MT_FLAT] = { 60 | --$Name Flat Railing Object 61 | --$Sprite SPHR 62 | doomednum = -1, 63 | spawnhealth = 1000, 64 | spawnstate = S_FLAT, 65 | radius = 16*FRACUNIT, 66 | height = 16*FRACUNIT, 67 | flags = MF_NOGRAVITY|MF_NOCLIP|MF_NOCLIPHEIGHT, 68 | } 69 | states[S_FLAT] = {SPR_RAIL,0|FF_FLAT,2,A_None,0,0,S_NULL} 70 | 71 | 72 | addHook("MapThingSpawn", function(mo, mthing) 73 | 74 | -- Set up path options 75 | mo.paths = {} 76 | --mo.pathcolor = 0 77 | --mo.pathradius = nil 78 | 79 | -- Set the start point id (moved to here from thinker instead) 80 | mo.startid = mo.spawnpoint.angle 81 | 82 | -- Add the mapthing object into a table to iterate over rather than the entire map 83 | table.insert(List_LinePaths, mthing) 84 | 85 | -- Sort values 86 | table.sort(List_LinePaths, function(a,b) return a.angle < b.angle end) 87 | 88 | --print("added object type: "..tostring(mo.type).." of "..tostring(mo)) 89 | end, MT_LINEPATH) 90 | 91 | 92 | -- Reset line path tables on map change 93 | addHook("MapChange", do 94 | List_LinePaths = {} 95 | end) 96 | 97 | 98 | 99 | local function map(x, in_min, in_max, out_min, out_max) 100 | return out_min + (x - in_min)*(out_max - out_min)/(in_max - in_min) 101 | end 102 | 103 | 104 | -- Calculate 3D distance (x,y + z height) 105 | local function R_Distance(p1, p2) 106 | return FixedHypot(FixedHypot(p1.x-p2.x, p1.y-p2.y), p1.z-p2.z) 107 | end 108 | 109 | -- Calculate 2D distance (x,y only) 110 | local function R_Distance2D(p1, p2) 111 | return FixedHypot(p1.x-p2.x, p1.y-p2.y) 112 | end 113 | 114 | -- Calculate and draw a line to the coordinates specified 115 | local function R_DrawMobjLine(from, to, params) 116 | 117 | -- We want the line casting to be customizable, so we sort out this stuff in a table 118 | local width = (params and params.width) or 64 119 | local dots = (params and params.dots == false) and MF2_DONTDRAW or 0 120 | local lines = (params and params.lines == false) and MF2_DONTDRAW or 0 121 | local lineMobj = (params and params.linemobj) or MT_THOK 122 | local scale = (params and params.scale) or FRACUNIT 123 | -- TODO: local func(), what if a function could run at the end of each spawn? 124 | 125 | 126 | -- The lower the width, the more depth the line has (eg, more objects) 127 | local pointDistance = R_Distance(from, to) 128 | local linkcount = pointDistance/(width< from.links[i].angle) then 158 | invert = 1 159 | end 160 | 161 | from.links[i].rollangle = vertangle*invert 162 | 163 | end 164 | end 165 | 166 | 167 | addHook("MobjThinker", function(mo) 168 | 169 | -- TODO: get options such as width, lines and dots from a line effect or elsewhere for this 170 | 171 | if (mo.valid) then 172 | 173 | -- Gather mobj properties 174 | local mobjflags = mo.spawnpoint.options 175 | 176 | -- Gather the objects path set once 177 | if (mo.spawnpoint.options & MTF_OBJECTSPECIAL and #mo.paths <= 0) then 178 | 179 | -- Search the global path list for the mobj 180 | for amnt=1,#List_LinePaths do 181 | 182 | -- Gather path list mobj properties 183 | local pl_mthing = List_LinePaths[amnt] -- current mapthing mobj in list 184 | local pl_mthing_angle = pl_mthing.angle -- mobj spawnpoint angle 185 | local pl_mthing_flags = pl_mthing.options -- mobj spawnpoint flags 186 | 187 | if (pl_mthing.valid) then 188 | 189 | -- Find angle ids that follow after the start point angle and insert into the path list 190 | -- as long as it is not another starting point not identical to this set 191 | --print("List_LinePaths Index: " .. amnt .. " - has angle of " .. pl_mthing_angle) 192 | 193 | for i=mo.startid,LINEPATH_MAX do 194 | if ( (pl_mthing_angle == mo.startid+#mo.paths) ) then 195 | 196 | --print("Path start id [" .. mo.startid .. "] found next path id - ".. amnt) 197 | table.insert(mo.paths, pl_mthing.mobj) 198 | 199 | end 200 | end 201 | end 202 | end 203 | --print("Done gathering paths!") 204 | --print("Path start id [" .. mo.startid .. "] has " .. #mo.paths .. " paths") 205 | end 206 | 207 | -- Draw lines from the path list (continuous) 208 | for i=1,#mo.paths-1 do 209 | R_DrawMobjLine(mo.paths[i], mo.paths[i+1], {width = 45, lines = true, dots = true}) 210 | --mo.rollangle = $1 + ANG1 211 | end 212 | end 213 | end, MT_LINEPATH) 214 | 215 | rawset(_G, "R_DrawMobjLine", R_DrawMobjLine) 216 | 217 | -- Unused Test Code 218 | --addHook("ThinkFrame", function() 219 | -- 220 | -- for i=1,#List_LinePaths do 221 | -- 222 | -- --local mobj = List_LinePaths[i] 223 | -- for player in players.iterate() do 224 | -- -- TODO: map a maximum distance that sprites will continue to be drawn 225 | -- --R_DrawMobjLine(player.mo, List_LinePaths[2], 64) 226 | -- end 227 | -- --print(R_Distance(List_LinePaths[1], List_LinePaths[2])/FRACUNIT) 228 | -- --print(R_Distance2D(List_LinePaths[1], List_LinePaths[2])/FRACUNIT) 229 | -- end 230 | -- 231 | --end) 232 | -------------------------------------------------------------------------------- /game/l_AnimColors.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_AnimColors.lua 3 | * (sprkizard) 4 | * (July 10, 2020 14:35) 5 | * (Dec 23, 2020 00:00) 6 | * Desc: Takes a list of palette/ramp indices and automatically 7 | creates an animated skincolor from them 8 | For a list of skincolors: 9 | https://wiki.srb2.org/wiki/List_of_skin_colors 10 | * Usage: (See example skin Dreamy or Blackwave) 11 | ]] 12 | 13 | -- The animcolors global 14 | rawset(_G, "AnimColors", {}) 15 | 16 | -- Concat. a list of ramps 17 | function AnimColors.merge(...) 18 | 19 | local list = {...} 20 | local merged = {} 21 | 22 | for _,entry in pairs(list) do 23 | 24 | -- print(string.format("Table: %s", tostring(entry) )) 25 | local offset = (type(entry) == "userdata") and 1 or 0 26 | 27 | for i=(1-offset), (#entry-offset) do 28 | -- print(string.format("---Color: %s inserted!", entry[i])) 29 | table.insert(merged, entry[i]) 30 | end 31 | end 32 | -- print("MERGED:") 33 | -- for k,v in pairs(merged) do 34 | -- print(v) 35 | -- end 36 | return merged 37 | end 38 | 39 | function AnimColors.reverse(ramp) 40 | local rev = {} 41 | 42 | local offset = (type(ramp) == "userdata") and 1 or 0 43 | 44 | for i=(#ramp-offset), (1-offset), -1 do 45 | table.insert(rev, ramp[i]) 46 | end 47 | return rev 48 | end 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -- Example colors 57 | 58 | freeslot("SKINCOLOR_GOLDRAMPWAVE") 59 | skincolors[SKINCOLOR_GOLDRAMPWAVE] = { 60 | name = "Gold Ramp Wave", 61 | ramp = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, 62 | chatcolor = V_GRAYMAP, 63 | accessible = true 64 | } 65 | M_MoveColorAfter(SKINCOLOR_GOLDRAMPWAVE, SKINCOLOR_BLUE) 66 | 67 | AnimColors.r_goldwave = { 68 | startpos = 1, 69 | type = "ramp", 70 | style = "wave", 71 | delay = 4, 72 | ramp = { 73 | skincolors[SKINCOLOR_SUPERGOLD1].ramp, 74 | skincolors[SKINCOLOR_SUPERGOLD2].ramp, 75 | skincolors[SKINCOLOR_SUPERGOLD3].ramp, 76 | skincolors[SKINCOLOR_SUPERGOLD4].ramp, 77 | skincolors[SKINCOLOR_SUPERGOLD5].ramp, 78 | }, 79 | } 80 | 81 | freeslot("SKINCOLOR_GOLDWAVE") 82 | skincolors[SKINCOLOR_GOLDWAVE] = { 83 | name = "Gold Color Wave", 84 | ramp = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, 85 | chatcolor = V_GRAYMAP, 86 | accessible = true 87 | } 88 | M_MoveColorAfter(SKINCOLOR_GOLDWAVE, SKINCOLOR_GOLDRAMPWAVE) 89 | 90 | AnimColors.goldwave = { 91 | startpos = 1, 92 | type = "palette", 93 | style = "wave", 94 | delay = 1, 95 | ramp = AnimColors.merge( 96 | skincolors[SKINCOLOR_SUPERGOLD1].ramp, 97 | AnimColors.reverse(skincolors[SKINCOLOR_SUPERGOLD1].ramp), 98 | skincolors[SKINCOLOR_SUPERGOLD2].ramp, 99 | AnimColors.reverse(skincolors[SKINCOLOR_SUPERGOLD2].ramp), 100 | skincolors[SKINCOLOR_SUPERGOLD3].ramp, 101 | AnimColors.reverse(skincolors[SKINCOLOR_SUPERGOLD3].ramp), 102 | skincolors[SKINCOLOR_SUPERGOLD4].ramp, 103 | AnimColors.reverse(skincolors[SKINCOLOR_SUPERGOLD4].ramp), 104 | skincolors[SKINCOLOR_SUPERGOLD5].ramp, 105 | AnimColors.reverse(skincolors[SKINCOLOR_SUPERGOLD5].ramp), 106 | skincolors[SKINCOLOR_SUPERGOLD4].ramp, 107 | AnimColors.reverse(skincolors[SKINCOLOR_SUPERGOLD4].ramp), 108 | skincolors[SKINCOLOR_SUPERGOLD3].ramp, 109 | AnimColors.reverse(skincolors[SKINCOLOR_SUPERGOLD3].ramp), 110 | skincolors[SKINCOLOR_SUPERGOLD2].ramp, 111 | AnimColors.reverse(skincolors[SKINCOLOR_SUPERGOLD2].ramp) 112 | ), 113 | } 114 | 115 | 116 | freeslot("SKINCOLOR_GOLDBOUNCE") 117 | skincolors[SKINCOLOR_GOLDBOUNCE] = { 118 | name = "Gold (Bounce)", 119 | ramp = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, 120 | chatcolor = V_GRAYMAP, 121 | accessible = true 122 | } 123 | M_MoveColorAfter(SKINCOLOR_GOLDBOUNCE, SKINCOLOR_GOLDWAVE) 124 | 125 | AnimColors.superbounce = { 126 | startpos = 1, 127 | type = "ramp", 128 | style = "bounce", 129 | delay = 2, 130 | ramp = { 131 | skincolors[SKINCOLOR_SUPERGOLD1].ramp, 132 | skincolors[SKINCOLOR_SUPERGOLD2].ramp, 133 | skincolors[SKINCOLOR_SUPERGOLD3].ramp, 134 | skincolors[SKINCOLOR_SUPERGOLD4].ramp, 135 | skincolors[SKINCOLOR_SUPERGOLD5].ramp 136 | }, 137 | } 138 | 139 | 140 | freeslot("SKINCOLOR_REDWAVE") 141 | skincolors[SKINCOLOR_REDWAVE] = { 142 | name = "Red Wave", 143 | ramp = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, 144 | chatcolor = V_GRAYMAP, 145 | accessible = true 146 | } 147 | M_MoveColorAfter(SKINCOLOR_REDWAVE, SKINCOLOR_GOLDBOUNCE) 148 | 149 | AnimColors.redwave = { 150 | startpos = 1, 151 | type = "palette", 152 | style = "wave", 153 | delay = 3, 154 | ramp = { 155 | 32,33,34,35,36,37,38,39,40,41,42,43,44,45,46, 156 | 47,46,45,44,43,42,41,40,39,38,37,36,35,34,33 157 | }, 158 | } 159 | 160 | freeslot("SKINCOLOR_CRAINBOW") 161 | skincolors[SKINCOLOR_CRAINBOW] = { 162 | name = "Rainbow Wave", 163 | ramp = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, 164 | chatcolor = V_GRAYMAP, 165 | accessible = true 166 | } 167 | M_MoveColorAfter(SKINCOLOR_CRAINBOW, SKINCOLOR_REDWAVE) 168 | 169 | AnimColors.rainbowwave = { 170 | startpos = 1, 171 | type = "palette", 172 | style = "wave", 173 | delay = 2, 174 | ramp = { 175 | 47,46,45,44,43,42,41,40,39,38,37,36,35,34,33,32, 176 | 176,177,178,179,180,181,182,183,184,185,186,187, 177 | 199,198,197,196,195,194,193,192, 178 | 160,161,162,163,164,165,166,167,168,169, 179 | 159,158,157,156,155,154,153,152,151,150,149,148,147,146,145,144, 180 | 128,129,130,131,132,133,134,135,136,137,138,139, 181 | 111,110,109,108,107,106,105,104,103,102,101,100,99,98,97,96, 182 | 88,89,90,91,92,93,94,95, 183 | 79,78,77,76,75,74,73,72, 184 | 48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63, 185 | }, 186 | } 187 | 188 | 189 | 190 | 191 | local function P_AnimateSkinColor(skincolornum, animdef) 192 | 193 | if (animdef.style == "wave") then 194 | 195 | if (leveltime % animdef.delay) then return end -- set delay speed 196 | 197 | -- This is the range that normally appears (max -> 1) 198 | animdef.startpos = ($ > 1) and $1-1 or #animdef.ramp 199 | 200 | -- Decide on if the ramp table is only colors, or using a full ramp 201 | if (animdef.type == "palette") then 202 | for i=15,0,-1 do 203 | skincolors[skincolornum].ramp[i] = animdef.ramp[((animdef.startpos+i) % #animdef.ramp)+1] 204 | end 205 | else 206 | skincolors[skincolornum].ramp = animdef.ramp[((animdef.startpos) % #animdef.ramp)+1] 207 | end 208 | 209 | elseif (animdef.style == "wavereverse") then 210 | 211 | if (leveltime % animdef.delay) then return end -- set delay speed 212 | 213 | -- 1 -> max (appears as reversed) 214 | animdef.startpos = ($ < #animdef.ramp) and $1+1 or 1 215 | 216 | -- Decide on if the ramp table is only colors, or using a full ramp 217 | if (animdef.type == "palette") then 218 | for i=0,15 do 219 | skincolors[skincolornum].ramp[i] = animdef.ramp[((animdef.startpos+i) % #animdef.ramp)+1] 220 | end 221 | else 222 | skincolors[skincolornum].ramp = animdef.ramp[((animdef.startpos) % #animdef.ramp)+1] 223 | end 224 | 225 | elseif (animdef.style == "bounce") then 226 | 227 | if (leveltime % animdef.delay) then return end -- set delay speed 228 | 229 | -- Get the bounce speed 230 | if not animdef.dir then animdef.dir = 1 end 231 | 232 | -- Swap directions on ramp edges 233 | if (animdef.startpos <= 1) then animdef.dir = 1 234 | elseif (animdef.startpos >= #animdef.ramp) then animdef.dir = -1 end 235 | 236 | animdef.startpos = $1+1*animdef.dir 237 | 238 | -- Decide on if the ramp table is only colors, or using a full ramp 239 | if (animdef.type == "palette") then 240 | -- Cycle colors through ramp 241 | for i=0,15 do 242 | skincolors[skincolornum].ramp[i] = animdef.ramp[((animdef.startpos+i) % #animdef.ramp)+1] 243 | end 244 | else 245 | -- Cycle ramps 246 | skincolors[skincolornum].ramp = animdef.ramp[animdef.startpos] 247 | end 248 | 249 | --[[elseif (animdef.style == "shift") then 250 | -- The original scroll error is so silly that it should be kept as a type 251 | animdef.startpos = ($ < #animdef.ramp) and $1+1 or 1 252 | skincolors[skincolornum].ramp[(leveltime % 16)] = animdef.ramp[(animdef.startpos % #animdef.ramp)+1] 253 | --]] 254 | end 255 | end 256 | 257 | -- Thinkframe hook (duh) 258 | addHook("ThinkFrame", function() 259 | 260 | P_AnimateSkinColor(SKINCOLOR_GOLDRAMPWAVE, AnimColors.r_goldwave) 261 | P_AnimateSkinColor(SKINCOLOR_GOLDWAVE, AnimColors.goldwave) 262 | P_AnimateSkinColor(SKINCOLOR_GOLDBOUNCE, AnimColors.superbounce) 263 | P_AnimateSkinColor(SKINCOLOR_REDWAVE, AnimColors.redwave) 264 | P_AnimateSkinColor(SKINCOLOR_CRAINBOW, AnimColors.rainbowwave) 265 | 266 | end) 267 | 268 | -- Create a global for this 269 | rawset(_G, "P_AnimateSkinColor", P_AnimateSkinColor) 270 | 271 | 272 | -- REFERENCE CODE FROM SWITCHKAZE 273 | --[[freeslot("SKINCOLOR_ANIMATED") 274 | skincolors[SKINCOLOR_ANIMATED] = { 275 | name = "Animated", 276 | accessible = true 277 | } 278 | local test = { 279 | 0,0,0,0,0,0,0,0,0,0, 280 | 31,31,31,31,31,31,31,31,31,31 281 | } 282 | local pos = 1 283 | addHook("ThinkFrame", do 284 | pos = $<#test and $1+1 or 1 285 | for i=0,15 286 | skincolors[SKINCOLOR_ANIMATED].ramp[i] = test[((pos+i)%#test)+1] 287 | end 288 | end)--]] -------------------------------------------------------------------------------- /hud/l_fontlib.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_fontlib.lua 3 | * (sprkizard) 4 | * (Dec 6, 2021 16:51) 5 | * Desc: A custom drawstring function that can 6 | use custom font sets 7 | 8 | * Usage: TODO 9 | ]] 10 | 11 | rawset(_G, "Fontlib", {}) 12 | 13 | Fontlib.defaultspace = 4 14 | Fontlib.defaultreturn = 4 15 | 16 | Fontlib.fontinfo = { 17 | ["CRFNT"] = {upperonly=true}, 18 | ["LTFNT"] = {}, 19 | ["NTFNO"] = {}, 20 | ["NTFNT"] = {upperonly=true}, 21 | ["STCFN"] = {spacewidth=4, returnheight=4}, 22 | ["TNYFN"] = {spacewidth=2, returnheight=5}, 23 | } 24 | 25 | -- Creates an entry for custom font info (width between letters, return spacing, etc) 26 | function Fontlib.setFontAttr(font, t) 27 | 28 | Fontlib.fontinfo[font] = {} 29 | for k,v in pairs(t) do 30 | Fontlib.fontinfo[font][k] = v 31 | end 32 | end 33 | 34 | -- Checks if the fontinfo entry exists, and gets the supplied attribute of it 35 | function Fontlib.getFontAttr(font, attr) 36 | return (Fontlib.fontinfo[font] and Fontlib.fontinfo[font][attr]) 37 | end 38 | 39 | -- Wrapper for validating table contents used here, and using them 40 | function Fontlib.getAttr(t, key, attr) 41 | return (t[key] and t[key][attr]) 42 | end 43 | 44 | -- Returns the width what the string as patches would be, 45 | -- and returns all cached patches in a table 46 | -- function Fontlib.GetInternalFontWidth(str, font) 47 | function Fontlib.cachePatchWidth(v, str, font, space) 48 | 49 | -- No string 50 | if not (str) then return 0 end 51 | 52 | local patches = {} 53 | local width = 0 54 | 55 | for i=1,#str do 56 | 57 | local char = str:sub(i,i) 58 | 59 | -- Spaces before fonts. Use accurate font space widths or revert to defaults 60 | if char:byte() == 32 then 61 | width = $1 + (Fontlib.getFontAttr(font, "spacewidth") or 4) 62 | continue 63 | end 64 | -- Ignore skincolors completely 65 | if char:byte() >= 131 and char:byte() <= 199 then 66 | continue 67 | end 68 | -- TODO: count special characters? 69 | if char:byte() >= 200 and char:byte() <= 203 then 70 | -- width = $1+8 71 | continue 72 | end 73 | 74 | -- Create patch name format 75 | local patchname = string.format("%s%03d", font, char:byte()) 76 | 77 | -- TODO: some patches do not exist, and the fix below may not be enough 78 | if not (v.patchExists(patchname)) then patches[char:byte()] = nil continue end 79 | 80 | -- Cache patches assigned by byte number 81 | if not (patches[char:byte()]) then 82 | -- Avoid caching the same character twice 83 | patches[char:byte()] = v.cachePatch( patchname ) 84 | end 85 | width = $1 + (patches[char:byte()].width or 8) + space 86 | 87 | end 88 | return {patches=patches, linewidth=width} 89 | end 90 | 91 | function Fontlib.invalidCharPatch(patchlist, char) 92 | return patchlist[char:byte()] == nil and true or false 93 | end 94 | 95 | -- TODO: separate text effects 96 | -- function Fontlib.stringEffects(x, y, char, time) 97 | -- end 98 | 99 | -- TODO: Find a way to capture embedded flags 100 | -- (eg. Font swapping with format: t[font..char:byte()] to cache multiple characters)) 101 | --[[function Fontlib.gettextflag() 102 | local keypos = 0 103 | local ahead = 0 104 | 105 | if keypos and (pos >= keypos and pos <= ahead) then return end 106 | if line:sub(pos, pos+5) == "") ahead = $1+#capture break end 115 | capture = $1 .. line:sub(j, j) 116 | -- print(keypos.."|"..str:sub(j, j)) 117 | end 118 | x = capture 119 | return 120 | end 121 | end--]] 122 | 123 | 124 | function Fontlib.drawString(v, sx, sy, text, flags, align) 125 | 126 | -- Font Options 127 | local font = (flags and flags.font) or "STCFN" 128 | local uppercs = (flags and flags.upper) or false 129 | local ramp = (flags and flags.ramp) or nil -- Custom rainbow color 130 | local marspeed = (flags and flags.marspeed) or 4 -- Rainbow marquee speed 131 | local color = nil 132 | 133 | -- Constants 134 | local spacewidth = (Fontlib.getFontAttr(font, "spacewidth") or 4) 135 | 136 | -- Scale adjustments 137 | local scale = (flags and flags.scale) or FRACUNIT 138 | local hscale = (flags and flags.hscale) or 0 139 | local vscale = (flags and flags.vscale) or 0 140 | 141 | -- Spacing 142 | local xspace = (flags and flags.xspace) or 0 143 | local yspace = (flags and flags.yspace) or (Fontlib.getFontAttr(font, "returnheight") or 4) 144 | 145 | 146 | 147 | -- Split our string into new lines from line-breaks 148 | local lines = {} 149 | 150 | for breaks in text:gmatch("[^\r\n]+") do 151 | table.insert(lines, breaks) 152 | end 153 | 154 | -- Interate through the text blocks (alignment should always go last before char drawing) 155 | for seg=1,#lines do 156 | 157 | local line = lines[seg] 158 | 159 | -- Screen x and y positions 160 | local x = sx 161 | local y = sy 162 | 163 | -- Text effects 164 | local off_x = 0 165 | local off_y = 0 166 | local swirl = 0 167 | local shake = 0 168 | local rainbow = 0 169 | local rcolors = ramp or {SKINCOLOR_SALMON,SKINCOLOR_ORANGE,SKINCOLOR_YELLOW,SKINCOLOR_MINT,SKINCOLOR_SKY,SKINCOLOR_PASTEL,SKINCOLOR_BUBBLEGUM} 170 | 171 | -- Current (+backwards) character & font patch (hopeful optimization) 172 | local bchar 173 | local char 174 | local charpatch 175 | 176 | -- Fixed is no longer an alignment option, and is now a flag 177 | if not (flags and flags.fixed) then 178 | x = $1 << FRACBITS 179 | y = $1 << FRACBITS 180 | end 181 | 182 | -- V_ALLOWLOWERCASE flag replacement 183 | if (uppercs or Fontlib.getFontAttr(font, "upperonly")) then 184 | line = tostring(line):upper() 185 | end 186 | 187 | -- Get used character patches and the width of the line 188 | -- TODO: character spacing does not work correctly 189 | local cache = Fontlib.cachePatchWidth(v, line, font, xspace) 190 | 191 | -- Text block alignment settings 192 | if (align == "center") then 193 | x = $1-FixedMul( cache.linewidth/2, scale) << FRACBITS 194 | elseif (align == "right") then 195 | x = $1-FixedMul( cache.linewidth, scale) << FRACBITS 196 | end 197 | 198 | 199 | -- v.drawString(320/2, 150+seg*8, "Line Length: "+#line, 0, "thin") 200 | for pos=1,#line do 201 | (function() 202 | 203 | -- String sub each character 204 | char = line:sub(pos, pos) 205 | 206 | -- Text Effects 207 | -- TODO: separate text effects 208 | -- ======== 209 | -- Text Effect: Rainbow color 210 | if (char:byte() == 199) then 211 | color = nil 212 | rainbow = leveltime/marspeed + #line 213 | return 214 | end 215 | 216 | -- Text Effect: Text color (Custom skincolors unsupported, only MAXSKINCOLORS allowed.) 217 | if (char:byte() == 130) then 218 | color = nil 219 | return 220 | elseif (char:byte() >= 131 and char:byte() <= 198) then 221 | color = v.getColormap(TC_DEFAULT, char:byte() - 130) 222 | rainbow = 0 223 | return 224 | end 225 | -- Stop effects 226 | if (char:byte() == 200) then 227 | swirl = 0 228 | shake = 0 229 | return 230 | end 231 | 232 | -- Text Effect: That one undertale groove effect 233 | if (char:byte() == 201) then 234 | swirl = leveltime*2 235 | shake = 0 236 | return 237 | end 238 | 239 | -- Text Effect: That one deltarune shake effect 240 | if (char:byte() == 202) then 241 | shake = (leveltime/1)*(leveltime/1)*3 242 | swirl = 0 243 | return 244 | end 245 | 246 | if (rainbow) then 247 | color = v.getColormap(TC_DEFAULT, rcolors[((rainbow-pos) % #rcolors)+1]) 248 | end 249 | 250 | if (swirl) then 251 | swirl = $1+2 252 | off_x = (cos(ANG10*(swirl))) 253 | off_y = (sin(ANG10*(swirl))) 254 | end 255 | 256 | if (shake) then 257 | shake = ($1+1)*($1+FU) 258 | off_x = (cos(ANG10*shake)) 259 | shake = ($1+1)*($1+FU) 260 | off_y = (sin(ANG10*shake)) 261 | end 262 | -- ======== 263 | 264 | -- Prevent spaces and non-existent characters from drawing altogether 265 | if not char:byte() or char:byte() == 32 then 266 | x = $1+spacewidth*scale 267 | return 268 | end 269 | 270 | -- Weird return spacing fix by getting the previous character 271 | bchar = line:sub(pos-1, pos) 272 | 273 | -- If a character has no patch: prevent from drawing 274 | if (Fontlib.invalidCharPatch(cache.patches, char)) then return end 275 | 276 | -- Draw the current character given 277 | v.drawStretched(x+off_x, y+off_y, scale+hscale, scale+vscale, cache.patches[char:byte()], 0, color or v.getColormap(TC_DEFAULT, 1)) 278 | 279 | -- Sets the space between each character using the font's width 280 | x = $1 + (xspace+cache.patches[char:byte()].width)*scale 281 | 282 | end)() 283 | end 284 | -- For control code effects, try to get the last character height 285 | local cheight = Fontlib.getAttr(cache.patches, char:byte(), "height") 286 | local bcheight = Fontlib.getAttr(cache.patches, bchar:byte(), "height") 287 | -- Break new lines by spacing and patch height for source-accurate spacing 288 | local linespacing = FixedMul( (yspace + (cheight or bcheight or 4) )*FU, scale ) 289 | sy = $1 + ((flags and flags.fixed) and linespacing or linespacing >> FRACBITS) 290 | 291 | end 292 | end 293 | 294 | 295 | 296 | 297 | 298 | -- Example 299 | --[[hud.add(function(v, stplyr, cam) 300 | 301 | -- v.drawFill(320/2, 0, 1, v.height(), 35) 302 | 303 | -- Vanilla 304 | v.drawString(320/2, 0, "Fontlib\n-Version 2-\nCustom Text Drawer", 0, "left") 305 | 306 | -- Fontlib 307 | local text = "\201Fontlib\200\n-Version 2-\n\202Custom Text Drawer\200" 308 | Fontlib.drawString(v, 320/2, 32, text, {font="STCFN"}, "center") 309 | Fontlib.drawString(v, 320/2, 64, text, {font="CRFNT"}, "center") 310 | Fontlib.drawString(v, 320/2, 128, text, {font="LTFNT"}, "center") 311 | 312 | end, "game") 313 | --]] 314 | 315 | -------------------------------------------------------------------------------- /other/linkedList.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | -- linkedList.lua, version v1.0.0 4 | * A doubly-linked list implementation in SRB2's Lua. 5 | 6 | * Authors: Golden 7 | * Originally Released: September 11, 2020 22:08 CST 8 | 9 | -- Load this Lua: 10 | --- local linkedList = dofile("linkedList.lua") 11 | * (linkedList will also be automatically put in the global space if other methods of loading are desirable) 12 | 13 | -- Usage: 14 | --- linkedList.newList(): list 15 | * Returns a new linked list. 16 | 17 | --- linkedList.newNode([data]): node 18 | * Returns a new node with data. 19 | 20 | --- linkedList.forward(list): iterator 21 | * Iterates a list forward. 22 | 23 | --- linkedList.backward(list): iterator 24 | * Iterates a list backward. 25 | 26 | --- linkedList.insertBeginning(node, list): node 27 | --- node:insertBeginning(list): node 28 | * Insert node at the beginning of a list. Removes from the previous list it was in. 29 | 30 | --- linkedList.insertEnd(node, list): node 31 | --- node:insertEnd(list): node 32 | * Insert node at the end of a list. Removes from the previous list it was in. 33 | 34 | --- linkedList.insertBefore(node, list, [anchorNode]): node 35 | --- node:insertBefore(list, [anchorNode]): node 36 | * Insert node before an optional `anchorNode'. Removes from the previous list it was in. 37 | * If there is no `anchorNode' given then it will insert `node' at the beginning of the list. 38 | (Useful if you want to call `linkedList.insertBefore' with the beginning or end of a list as an `anchorNode'.) 39 | 40 | --- linkedList.insertAfter(node, list, [anchorNode]): node 41 | --- node:insertAfter(list, [anchorNode]): node 42 | * Insert node after an optional `anchorNode'. Removes from the previous list it was in. 43 | * If there is no `anchorNode' given then it will insert `node' at the beginning of the list. 44 | (Useful if you want to call `linkedList.insertAfter' with the beginning or end of a list as an `anchorNode'.) 45 | 46 | --- linkedList.removeFromList(node, [list]): node 47 | --- node:removeFromList([list]): 48 | * Remove node from the specified list. If no list is specified it will use the last list it seen. 49 | 50 | --]] 51 | 52 | // Global table of linked list manipulation functions. 53 | local linkedList = {} 54 | 55 | // Functions that are also global, but also are part of nodes; node manipulation functions. 56 | local nodeFuncs = {} 57 | 58 | // Inserts a node to the beginning of a list. 59 | nodeFuncs.insertBeginning = function(node, list) 60 | // Remove from previous list (if any). 61 | if node.list then 62 | node:removeFromList(node.list) 63 | end 64 | 65 | rawset(node, "locked", false) // Unlock free access to node variables. 66 | 67 | node.list = list // Update list pointer. 68 | 69 | if list.first == nil then // List is empty? Fill with new entries! 70 | list.first = node 71 | list.last = node 72 | node.prev = nil 73 | node.next = nil 74 | else // Otherwise just insert this before the first list entry. 75 | nodeFuncs.insertBefore(node, list, list.first) 76 | end 77 | 78 | rawset(node, "locked", true) // Relock access to node variables. 79 | 80 | return node // Return node. 81 | end 82 | 83 | // Inserts a node to the end of a list. 84 | nodeFuncs.insertEnd = function(node, list) 85 | // Remove from previous list (if any). 86 | if node.list then 87 | node:removeFromList(node.list) 88 | end 89 | 90 | rawset(node, "locked", false) // Unlock free access to node variables. 91 | 92 | node.list = list // Update list pointer. 93 | 94 | if list.last == nil then // List is empty? Fill with new entries! 95 | list.first = node 96 | list.last = node 97 | node.prev = nil 98 | node.next = nil 99 | else // Otherwise just insert this after the last list entry. 100 | nodeFuncs.insertAfter(node, list, list.last) 101 | end 102 | 103 | rawset(node, "locked", true) // Relock access to node variables. 104 | 105 | return node // Return node. 106 | end 107 | 108 | // Inserts a node before another node (or the beginning in absentia) 109 | nodeFuncs.insertBefore = function(node, list, anchorNode) 110 | // Remove from previous list (if any). 111 | if node.list then 112 | node:removeFromList(node.list) 113 | end 114 | 115 | // Figure out a valid anchorNode. 116 | anchorNode = anchorNode == nil and list.first or anchorNode 117 | 118 | // No valid anchorNode? Just insert into the beginning then. 119 | if not anchorNode then 120 | return nodeFuncs.insertBeginning(node, list) 121 | end 122 | 123 | // node's the anchorNode? Don't continue, we can't insert the same node before itself! 124 | if anchorNode == node then 125 | return node 126 | end 127 | 128 | rawset(node, "locked", false) // Unlock free access to node variables. 129 | rawset(anchorNode, "locked", false) // Unlock free access to anchorNode variables. 130 | 131 | node.list = list // Update list pointer. 132 | 133 | node.next = anchorNode // Place anchorNode after node. 134 | 135 | if anchorNode.prev == nil then // If anchorNode is the first entry... 136 | list.first = node // Update list's first entry. 137 | else // Otherwise... 138 | anchorNode.prev.next = node // Place node after what used to go behind anchorNode. 139 | end 140 | 141 | node.prev = anchorNode.prev // Place anchorNode's previous node before node. 142 | anchorNode.prev = node // Place node before anchorNode. 143 | 144 | rawset(node, "locked", true) // Relock access to node variables. 145 | rawset(anchorNode, "locked", true) // Relock access to anchorNode variables. 146 | 147 | return node // Return node. 148 | end 149 | 150 | // Inserts a node after another node (or the end in absentia) 151 | nodeFuncs.insertAfter = function(node, list, anchorNode) 152 | // Remove from previous list (if any). 153 | if node.list then 154 | node:removeFromList(node.list) 155 | end 156 | 157 | // Figure out a valid anchorNode. 158 | anchorNode = anchorNode == nil and list.last or anchorNode 159 | 160 | // No valid anchorNode? Just insert into the end then. 161 | if not anchorNode then 162 | return nodeFuncs.insertEnd(node, list) 163 | end 164 | 165 | // node's the anchorNode? Don't continue, we can't insert the same node after itself! 166 | if anchorNode == node then 167 | return 168 | end 169 | 170 | rawset(node, "locked", false) // Unlock free access to node variables. 171 | rawset(anchorNode, "locked", false) // Unlock free access to anchorNode variables. 172 | 173 | node.list = list // Update list pointer. 174 | 175 | node.prev = anchorNode // Place anchorNode before node. 176 | 177 | if anchorNode.next == nil then // If anchorNode is the last entry... 178 | list.last = node // Update list's last entry. 179 | else // Otherwise... 180 | anchorNode.next.prev = node // Place node before what used to go ahead of anchorNode. 181 | end 182 | 183 | node.next = anchorNode.next // Place anchorNode's next node after node. 184 | anchorNode.next = node // Place node after anchorNode. 185 | 186 | rawset(node, "locked", true) // Relock access to node variables. 187 | rawset(anchorNode, "locked", true) // Relock access to anchorNode variables. 188 | 189 | return node // Return node. 190 | end 191 | 192 | // Removes a node from a list (using the node's last seen list in absentia) 193 | nodeFuncs.removeFromList = function(node, list) 194 | list = list or node.list // Figure out a valid list 195 | 196 | if not list then // No valid list? Don't continue, we don't want to mess up references! 197 | return node 198 | end 199 | 200 | rawset(node, "locked", false) // Unlock free access to node variables. 201 | 202 | if node.prev then // There's a previous node? 203 | rawset(node.prev, "locked", false) // Unlock free access to previous node's variables. 204 | node.prev.next = node.next // Make the previous node reference the next node. 205 | rawset(node.prev, "locked", true) // Relock access to previous node's variables. 206 | end 207 | 208 | if node.next then // There's a next node? 209 | rawset(node.next, "locked", false) // Unlock free access to next node's variables. 210 | node.next.prev = node.prev // Make the next node reference the previous node. 211 | rawset(node.next, "locked", true) // Relock access to next node's variables. 212 | end 213 | 214 | if list.last == node then // If the last node is referencing us, 215 | list.last = node.prev // make it reference the previous node instead. 216 | end 217 | 218 | if list.first == node then // If the first node is referencing us, 219 | list.first = node.next // make it reference the next node instead. 220 | end 221 | 222 | // Remove our own references to other nodes and the list. 223 | node.list = nil 224 | node.prev = nil 225 | node.next = nil 226 | 227 | rawset(node, "locked", true) // Relock access to node variables. 228 | 229 | return node // Return node. 230 | end 231 | 232 | // Allow the node functions to be accessible from linkedList 233 | setmetatable(linkedList, {__index = nodeFuncs}) 234 | 235 | // Creates a new list. 236 | linkedList.newList = function() 237 | return {first = nil, last = nil} // Not even a metatable needed. 238 | end 239 | 240 | // Iterates a list forward. 241 | linkedList.forward = function(list) 242 | local node = list.first // Start at the first node of the list. 243 | 244 | // Generate functions 245 | return function() 246 | local returnme = node // Get current node to return (or nil) 247 | 248 | if node then // If this is a node, then prepare the next node for usage on the next iteration 249 | node = node.next 250 | end 251 | 252 | return returnme // Return the current node. 253 | end 254 | end 255 | 256 | // Iterates a list backward. 257 | linkedList.backward = function(list) 258 | local node = list.last // Start at the last node of the list. 259 | 260 | // Generate functions 261 | return function() 262 | local returnme = node // Get current node to return (or nil) 263 | 264 | if node then // If this is a node, then prepare the previous node for usage on the next iteration 265 | node = node.prev 266 | end 267 | 268 | return returnme // Return the current node. 269 | end 270 | end 271 | 272 | // Creates a new node. 273 | linkedList.newNode = function(data) 274 | local keys = {prev = true, data = true, next = true, locked = true, list = true} // Usable keys 275 | local node = {prev = nil, data = data, next = nil, locked = true, list = nil} 276 | 277 | return setmetatable(node, { 278 | __index = nodeFuncs, // Allow the node functions to be accessible from a node 279 | __newindex = function(node, key, value) 280 | if (keys[key] and not node.locked) // Allow the creation of any usable key when unlocked 281 | or key == "data" then // but only "data" when locked. 282 | rawset(node, key, value) 283 | end 284 | end, // Don't allow new indices. 285 | __usedindex = function(node, key, value) // Only allow editing of data, unless unlocked. 286 | if key == "data" or not node.locked then 287 | rawset(node, key, value) 288 | end 289 | end, 290 | __shl = function(node, shift_amount) // Left shifting, quick way to move node left in list. 291 | // Keep shifting left until we either hit the beginning or don't need to continue shifting 292 | while shift_amount > 0 and node.prev != nil do 293 | node:insertBefore(node.list, node.prev) 294 | shift_amount = $ - 1 295 | end 296 | end, 297 | __shr = function(node, shift_amount) // Right shifting, quick way to move node right in list. 298 | // Keep shifting right until we either hit the end or don't need to continue shifting 299 | while shift_amount > 0 and node.next != nil do 300 | node:insertAfter(node.list, node.next) 301 | shift_amount = $ - 1 302 | end 303 | end, 304 | __metatable = true // Don't allow viewing or editing of metatable either... 305 | }) 306 | end 307 | 308 | rawset(_G, "linkedList", linkedList) // Globalise. 309 | 310 | return linkedList // Return. -------------------------------------------------------------------------------- /hud/l_textboxes.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_textboxes.lua 3 | * (sprkizard) 4 | * (October ‎26, ‎2020, 0:00) 5 | * Desc: A rewrite of textboxes used in sugoi2, with 6 | a more minimalistic graphical style 7 | 8 | * Usage: TODO: check wiki??? 9 | 10 | * Depends on: 11 | hudlayers 12 | EventStepThinker 13 | ]] 14 | 15 | rawset(_G, "TextBox", {debug=true}) 16 | local textboxes = { 17 | -- {id=1, name="Amy Rose", text="Demo 1\nDemo 1\nDemo 1", icon=true, strcnt=0, showbg=1}, 18 | -- {id=2, name=nil, text="Demo 2", icon=false, ax=320/2,ay=200/2, strcnt=0, showbg=1} 19 | } 20 | 21 | -- Show information when enabled 22 | function TextBox.debug(v,x,y,textbox) 23 | if not TextBox.debug then return end 24 | local str = textbox.text:gsub("\n", " ") 25 | local info_output = string.format("ID: %d | Name: %s | Text: %s | Icon: %s | Printed: %d/%d | Auto: [%d/%d]| rel: (%d,%d) | abs: (%d,%d)", 26 | textbox.id, tostring(textbox.name),str,tostring(textbox.icon),textbox.strcnt,#textbox.text,textbox.linetime,textbox.auto,textbox.rx or 0,textbox.ry or 0,textbox.ax or 0,textbox.ay or 0) 27 | v.drawString(x, y, info_output, V_ALLOWLOWERCASE|V_SNAPTOBOTTOM, "small-thin") 28 | end 29 | 30 | -- Creates a new textbox 31 | function TextBox.new(id, name, text, args) 32 | if type(id) == "string" then print("Textbox ID must be a number!") return end 33 | 34 | -- Wipe the last id and overwrites it with a new under the same id 35 | if textboxes[id] then textboxes[id] = nil end 36 | 37 | local new_tb = { 38 | id = id, 39 | name = name, 40 | text = text, 41 | icon = (args and args.icon), 42 | linetime = 0, 43 | showbg = 1, 44 | strcnt = 0,-- + startpos, 45 | -- textsfx = (settings and settings.sfx) or sfx_none, 46 | auto = (args and args.auto) or 3*TICRATE, 47 | -- startpos = (settings and settings.startpos) or 0, 48 | -- speed = (settings and settings.speed) or 1, 49 | -- delay = (settings and settings.delay) or 1, 50 | } 51 | table.insert(textboxes, id or 1, new_tb) 52 | end 53 | function TextBox.Setconfig(id, settings) 54 | textboxes[id].textsfx = (settings and settings.sfx) or sfx_none 55 | textboxes[id].auto = (settings and settings.auto) or 3*TICRATE 56 | textboxes[id].startpos = (settings and settings.startpos) or 0 57 | textboxes[id].speed = (settings and settings.speed) or 1 58 | textboxes[id].delay = (settings and settings.delay) or 1 59 | textboxes[id].rx = (settings and settings.rx) 60 | textboxes[id].ry = (settings and settings.ry) 61 | textboxes[id].ax = (settings and settings.ax) 62 | textboxes[id].ay = (settings and settings.ay) 63 | end 64 | 65 | function TextBox.textbox_update() 66 | 67 | for id,textbox in pairs(textboxes) do 68 | 69 | -- Play a sound on each letter, skipping spaces and nl 70 | --[[if (textbox.textsfx) then 71 | if (textbox.strcnt < textbox.text:len()) 72 | and not (textbox.text:sub(textbox.strcnt):byte() == 0 or textbox.text:sub(textbox.strcnt):byte() == 32) 73 | and (textbox.strcnt % textbox.speed == 0) then 74 | S_StartSound(nil, textbox.textsfx) 75 | end 76 | end--]] 77 | 78 | -- Check if we reached the end of the string, and clear textbox if we did 79 | if (textbox.strcnt >= textbox.text:len()) then 80 | 81 | -- We finish automatically on a set time, or end on button press 82 | if (textbox.auto and textbox.linetime < textbox.auto) then 83 | textbox.linetime = $1+1 84 | else 85 | TextBox.new(textbox.id, textbox.name, "Next") 86 | -- textboxes[id] = nil 87 | end 88 | -- if (textbox.auto and textbox.linetime < textbox.auto) then 89 | -- player.textbox.linetime = $1+1 90 | -- -- print(string.format("[text auto: %d/%d]", textbox.linetime, textbox.auto)) 91 | -- elseif not (textbox.auto) and (player.cmd.buttons & BT_JUMP) then 92 | -- textbox.text = nil 93 | -- else 94 | -- textbox.text = nil -- set to automatically end for auto if neither 95 | -- end 96 | else 97 | -- Increment string.sub 98 | -- if (leveltime % textbox.delay == 0) then 99 | textbox.strcnt = min($1 + 1, textbox.text:len()) 100 | -- end 101 | -- print(string.format("[text subcnt: %d/%d]", textbox.strcnt, textbox.text:len())) 102 | end 103 | end 104 | end 105 | 106 | 107 | function TextBox.textbox_drawer(a, v, stplyr, cam) 108 | 109 | -- TODO: center/right aligned math 110 | -- Screen settings 111 | local scrwidth = v.width() / v.dupx() -- screen width 112 | local scrheight = v.height() / v.dupy() -- screen height 113 | 114 | local boxheight = 52 -- textbox height (52:78) 115 | 116 | -- v.drawFill(320/2, 0, 1, 200, 35) 117 | -- v.drawFill(320/2, 200/2, 320, 1, 160) 118 | 119 | -- Prepare a textbox (snap to bottom) 120 | for _,textbox in pairs(textboxes) do 121 | 122 | local prompt_x = ((320-scrwidth)/2) -- origin x 123 | local prompt_y = (200-boxheight) -- origin y 124 | local textoffset = 4 -- icon offset 125 | 126 | -- Set custom coordinates of the textbox (relative/absolute) 127 | if (textbox.rx and textbox.ry) then 128 | prompt_x = $1 + (textbox.rx or 0) 129 | prompt_y = $1 - (textbox.ry or 0) 130 | elseif (type(textbox.ax) == "number" and type(textbox.ay) == "number") then 131 | prompt_x = (textbox.ax) 132 | prompt_y = (textbox.ay)-17 133 | textoffset = 0 134 | end 135 | 136 | TextBox.debug(v, prompt_x, prompt_y-4, textbox) 137 | 138 | -- Draw the background to stretch to the screen edges 139 | if (textbox.showbg) then 140 | v.drawStretched(prompt_x*FU, prompt_y*FU, scrwidth*FU, boxheight*FU, v.cachePatch("~031G"), V_30TRANS|V_SNAPTOBOTTOM) 141 | end 142 | 143 | -- Draw the icon (4:152) (+ text offset) 144 | if (textbox.icon) then 145 | v.drawScaled((prompt_x+4)*FU, (prompt_y+4)*FU, FRACUNIT/6+FU/160, v.cachePatch("AMYRTALK"), V_SNAPTOBOTTOM) 146 | textoffset = 52 147 | end 148 | 149 | -- Show name (52:153) 150 | if (textbox.name) then 151 | v.drawString(prompt_x+textoffset, prompt_y+4, "\x82"..textbox.name, V_ALLOWLOWERCASE|V_SNAPTOBOTTOM, "left") 152 | end 153 | 154 | -- Draw the text (52:165) 155 | v.drawString(prompt_x+textoffset, prompt_y+17, textbox.text:sub(0, textbox.strcnt), V_ALLOWLOWERCASE|V_SNAPTOBOTTOM, "left") 156 | end 157 | end 158 | 159 | addHook("ThinkFrame", TextBox.textbox_update) 160 | hud.add(function(v, stplyr, cam) TextBox.textbox_drawer(nil, v, stplyr, cam) end, "game") 161 | 162 | if true then return end 163 | 164 | function TextBox.new(event, player, name, text) 165 | 166 | if not ( (player and player.mo.valid) 167 | and (player and player.textbox) ) then return end -- both player nor textbox table exists 168 | 169 | -- Initialize text settings, and run the textbox 170 | if not player.textbox.text then 171 | -- local _tb = {} 172 | player.textbox.strcnt = 0 + player.textbox.startpos 173 | player.textbox.linetime = 0 174 | player.textbox.speaker = name 175 | player.textbox.text = text 176 | -- player.textbox = _tb 177 | end 178 | TextBox.textbox_update(event, player) 179 | end 180 | 181 | -- Sets textbox configuration settings in advance to keep the .new function cleaner 182 | -- (want to reset all? leave settings empty) 183 | function TextBox.Setconfig(player, settings) 184 | 185 | if not ( (player and player.mo.valid) 186 | and (player and player.textbox) ) then return end -- both player nor textbox table exists 187 | 188 | -- This should initialize once before the textbox contains text 189 | if not player.textbox.text then 190 | player.textbox.icon = (settings and settings.icon) or "NONEICO" 191 | player.textbox.textsfx = (settings and settings.sfx) or sfx_none 192 | player.textbox.auto = (settings and settings.auto) or 3*TICRATE 193 | player.textbox.startpos = (settings and settings.startpos) or 0 194 | player.textbox.speed = (settings and settings.speed) or 1 195 | player.textbox.delay = (settings and settings.delay) or 1 196 | -- player.textbox.offset = {} 197 | end 198 | end 199 | 200 | local function P_SetupTextboxes(player) 201 | 202 | -- Set textbox information 203 | player.textbox = { 204 | speaker = nil, -- the name inserted into speaker field (can be anything) 205 | text = nil, -- the current text 206 | icon = "NONEICO", -- icon to use to besides the text\ 207 | textsfx = sfx_none, -- text printing sound 208 | strcnt = 0, -- the amount of the string that is shown 209 | linetime = 0, -- the time spent on current block of text 210 | -- offset = {x = 0, y = 0}, -- the offset of the textbox 211 | -- textoffset = {x = 0, y = 0}, -- the offset of the text 212 | -- iconoffset = {x = 0, y = 0}, -- the offset of the icon 213 | auto = 2*TICRATE, -- set a timer to advance to the next block 214 | startpos = 0, -- modify the starting position 215 | speed = 1, -- text printing speed 216 | -- selection = nil, 217 | } 218 | -- TODO: in the future maybe we can put multiple text boxes on screen? 219 | -- or instead, works like movienight emoji (anything non-player floats above mobj) 220 | -- player.textboxes = {} 221 | -- player.renders = {} -- TODO: do we need renders if hudlayers can do casebycase? 222 | end 223 | 224 | -- TODO: this used to be a mapload hook, is playerspawn better? find out (probably is) 225 | addHook("PlayerSpawn", function(player) 226 | -- for player in players.iterate do 227 | P_SetupTextboxes(player) 228 | -- end 229 | end) 230 | 231 | -- Textbox controller 232 | function TextBox.textbox_update(event, player) 233 | 234 | if not ( (player and player.mo.valid) 235 | and (player and player.textbox) ) then return end -- both player nor textbox table exists 236 | 237 | local textbox = player.textbox 238 | 239 | -- TODO: how to use an event wait, and reset the textblock at the same time? 240 | -- Run when string exists in text 241 | if (textbox.text) then 242 | 243 | -- Play a sound on each letter, skipping spaces and nl 244 | if (textbox.textsfx) then 245 | if (textbox.strcnt < textbox.text:len()) 246 | and not (textbox.text:sub(textbox.strcnt):byte() == 0 or textbox.text:sub(textbox.strcnt):byte() == 32) 247 | and (textbox.strcnt % textbox.speed == 0) then 248 | S_StartSound(nil, textbox.textsfx) 249 | end 250 | end 251 | 252 | -- Check if we reached the end of the string, and clear textbox if we did 253 | if (textbox.strcnt >= textbox.text:len()) then 254 | 255 | -- We finish automatically on a set time, or end on button press 256 | if (textbox.auto and textbox.linetime < textbox.auto) then 257 | player.textbox.linetime = $1+1 258 | -- print(string.format("[text auto: %d/%d]", textbox.linetime, textbox.auto)) 259 | elseif not (textbox.auto) and (player.cmd.buttons & BT_JUMP) then 260 | textbox.text = nil 261 | else 262 | textbox.text = nil -- set to automatically end for auto if neither 263 | end 264 | else 265 | -- Increment string.sub 266 | if (leveltime % textbox.delay == 0) then 267 | player.textbox.strcnt = min($1 + 1*textbox.speed, textbox.text:len()) 268 | end 269 | -- print(string.format("[text subcnt: %d/%d]", textbox.strcnt, textbox.text:len())) 270 | end 271 | end 272 | waitUntil(event, textbox.text == nil) -- pause the event until text is nil, not empty 273 | end 274 | 275 | -- Text Box drawer 276 | function TextBox.textbox_drawer(a, v, stplyr, cam) 277 | 278 | if not stplyr and stplyr.textbox then return end -- both player nor textbox table exists 279 | 280 | -- Screen settings 281 | local scrwidth = v.width() / v.dupx() -- screen width 282 | local scrheight = v.height() / v.dupy() -- screen height 283 | 284 | local boxheight = 52 -- textbox height (52:78) 285 | 286 | -- Prepare a textbox (snap to bottom) 287 | if (stplyr.textbox and stplyr.textbox.text) then 288 | 289 | -- Draw the background to scale to the screen edges 290 | -- TODO: until drawfill gets alpha values, use a graphic... 291 | -- v.drawFill((320-scrwidth)/2, 200-boxheight, scrwidth,boxheight, 31|V_SNAPTOBOTTOM|V_40TRANS) 292 | v.drawScaled(((320-scrwidth)/2)*FRACUNIT, (200-boxheight)*FRACUNIT, FRACUNIT*v.dupx(), v.cachePatch("PRMPTBG"), V_30TRANS|V_SNAPTOBOTTOM) 293 | 294 | -- The text box has a speaker 295 | if (stplyr.textbox.speaker) then 296 | v.drawString(52, 153, "\x82"..stplyr.textbox.speaker, V_ALLOWLOWERCASE|V_SNAPTOBOTTOM, "left") 297 | end 298 | 299 | -- Draw the icon 300 | v.drawScaled(4*FRACUNIT, 152*FRACUNIT, FRACUNIT/6, v.cachePatch(stplyr.textbox.icon), V_SNAPTOBOTTOM) 301 | 302 | -- Draw the text 303 | v.drawString(52, 165, stplyr.textbox.text:sub(0, stplyr.textbox.strcnt), V_ALLOWLOWERCASE|V_SNAPTOBOTTOM, "left") 304 | end 305 | end 306 | 307 | 308 | R_AddHud("Player_TextBoxes", 1, nil, TextBox.textbox_drawer) 309 | --addHook("PlayerThink", TextBox.textbox_update) 310 | 311 | rawset(_G, "SetTextConfig", SetTextConfig) 312 | rawset(_G, "Speak", Speak) 313 | 314 | 315 | 316 | -------------------------------------------------------------------------------- /game/l_EventStepThinker.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_EventStepThinker.lua 3 | * (sprkizard) 4 | * (September 18, 2020, 3:39) 5 | * Desc: A set of functions to run map events in the same style of 6 | Pokémon Mystery Dungeon: Gates to Infinity. 7 | (Inspired by a script made by fickleheart and D00D64) 8 | 9 | * Usage: TODO: check wiki??? 10 | 11 | * Depends on: 12 | 13 | ]] 14 | 15 | -- TODO: Prevent duplicate loading 16 | if _G["EventList"] then 17 | print("(!)\x81 ALERT: EventStepThinker is already loaded. The script will be skipped.") 18 | return 19 | end 20 | 21 | rawset(_G, "Event", {debug=false}) 22 | 23 | -- The events created on initialization 24 | rawset(_G, "EventList", {}) 25 | 26 | 27 | -- global table for running events (disable for now) 28 | local Running_Events = {} 29 | 30 | -- Sets a metatable to get function data from 31 | local eventMT = { 32 | __index = function(t, key) 33 | if key == "states" then 34 | return EventList[t.name].states 35 | end 36 | end, 37 | __len = function(t, key) 38 | return #EventList[t.name].states 39 | end 40 | } 41 | 42 | registerMetatable(eventMT) 43 | 44 | 45 | -- Defaults to zero value or an alternative if value is nil 46 | function Event.default(value, altvalue) 47 | return (value and not nil) and value or altvalue 48 | end 49 | 50 | -- Attempts to sort pairs 51 | function Event.spairs(t, order) 52 | -- collect the keys 53 | local keys = {} 54 | for k in pairs(t) do keys[#keys+1] = k end 55 | 56 | -- if order function given, sort by it by passing the table and keys a, b, 57 | -- otherwise just sort the keys 58 | if order then 59 | table.sort(keys, function(a,b) return order(t, a, b) end) 60 | else 61 | table.sort(keys) 62 | end 63 | 64 | -- return the iterator function 65 | local i = 0 66 | return function() 67 | i = i + 1 68 | if keys[i] then 69 | return keys[i], t[keys[i]] 70 | end 71 | end 72 | end 73 | 74 | -- Reduce redundancy when looking through the event tables. Used internally 75 | function Event.searchtable(f, issubevent) 76 | 77 | for k,evclass in pairs(Running_Events) do 78 | 79 | -- Await status change 80 | if f(k, evclass) then 81 | break 82 | end 83 | -- TODO: return value 84 | -- Fallback 85 | -- (function() 86 | -- do f(k, evclass) end 87 | -- end)() 88 | end 89 | end 90 | 91 | -- Seeks an event if it exists, and allows reading its data (editing data: at your own risk) 92 | function Event.read(name, fn) 93 | 94 | for _, e in pairs(Running_Events) do 95 | -- Run under any event name 96 | if (e and (name == "any" or name == "all")) then 97 | do fn(e.vars, e) end 98 | -- Run under a specific name 99 | elseif (e and e.name == name) then 100 | do fn(e.vars, e) end 101 | end 102 | end 103 | end 104 | 105 | -- Checks if an event exists 106 | function Event.exists(name) 107 | for _, e in pairs(Running_Events) do 108 | if (e and e.name == name) then 109 | return true 110 | end 111 | end 112 | end 113 | 114 | -- Prints a debug message 115 | function Event.printdebug(msg) 116 | if (Event.debug) then print(msg) end 117 | end 118 | 119 | -- Runs a function (only for debugging) 120 | function Event.debugfunc(f) 121 | if (Event.debug) then f() end 122 | end 123 | 124 | -- Enable table contents preview for debugging 125 | if (Event.debug) then 126 | hud.add(function(v,stplyr,cam) 127 | local x = 0 128 | local y = 0 129 | v.drawString(0,55, string.upper("\x81Running Events: ")..#Running_Events, 0, "small-thin") 130 | Event.searchtable(function(i, ev) 131 | v.drawString(0+x,64+y, string.format("Event: %s\nState: %d/%d\nStatus: %s\nSignal: %s\nIdle: %d", ev.name, ev.step, #ev.states, ev.status, ev.signal, ev.sleeptime), 0, "small-thin") 132 | x = $1+64 133 | if (i % 4 == 0) then 134 | x = 0 135 | y = $1+32 136 | end 137 | end) 138 | end, "game") 139 | end 140 | 141 | 142 | 143 | 144 | -- Creates a new event 145 | function Event.new(eventname, ftable) 146 | 147 | -- Create an event object for each new event 148 | local _event = {} 149 | 150 | _event.name = eventname 151 | _event.status = "normal" -- mimic coroutine statuses (dead|suspended|running|normal) 152 | _event.step = 1 -- the current step position in the state list (might be easier than for-do?) 153 | _event.signal = "" -- signal to wait on if given one 154 | _event.tags = {} -- Tags for jumping around inside of a state (like goto) 155 | _event.sleeptime = 0 -- the event wait timer 156 | _event.looptrack = 0 -- the event looptracker (can be used for anything eg. doOnce) 157 | 158 | -- Create a container for variables 159 | _event.vars = {} 160 | 161 | _event.states = {} 162 | 163 | -- Reference our event list into the event object 164 | -- Assign arguments to the functions to access these fields (variable, event, [parentevent]) 165 | for k,v in pairs(ftable) do 166 | 167 | if type(v) == "function" then 168 | -- add the functions to the state list with arguments (container, event) 169 | _event.states[tonumber(k)] = ftable[tonumber(k)] 170 | elseif type(v) == "table" then 171 | 172 | -- Sub-Events in the ftable should be added into the subevents table separately 173 | -- (they will inherit the parent event's variables, etc but still act independently) 174 | local _subevent = {name=k,status="normal",step=1,signal="",vars={},tags={},sleeptime=0,looptrack=0,states={}} 175 | 176 | -- add the functions to the state list with arguments (container, subevent, [parentevent]) 177 | for i=1,#v do 178 | _subevent.states[i] = v[i] 179 | end 180 | 181 | -- Add subevents into the subevent table 182 | EventList[k] = _subevent 183 | -- EventList.subevents[k] = _subevent 184 | end 185 | end 186 | 187 | -- Add events into the event table 188 | EventList[eventname] = _event 189 | return _event 190 | end 191 | 192 | -- private functions to reset event properties 193 | function Event.__setupEvent(ev, eventname) 194 | ev.name = EventList[eventname].name 195 | ev.status = "running" 196 | ev.step = EventList[eventname].step 197 | ev.signal = EventList[eventname].signal 198 | ev.tags = {} 199 | ev.sleeptime = EventList[eventname].sleeptime 200 | ev.looptrack = EventList[eventname].looptrack 201 | ev.vars = {} 202 | end 203 | 204 | function Event.__endEvent(ev) 205 | ev.sleeptime = 0 206 | ev.looptrack = 0 207 | ev.status = "dead" 208 | end 209 | 210 | 211 | -- TODO: find a new use for this since anonymous functions are dead 212 | -- Creates a userdata block'name' (usually when printed from tostring) 213 | --[[local function randomUserBlockName() 214 | local str = "0" 215 | for i=1,7 do 216 | -- Randomize 217 | local bits = {P_RandomRange(65,70), P_RandomRange(48,57)} 218 | local RandomKey = P_RandomRange(1, #bits) 219 | str = str .. string.char(bits[RandomKey]) 220 | end 221 | return str 222 | end--]] 223 | 224 | -- Runs an event 225 | function Event.start(eventname, args, caller) 226 | 227 | -- Instead of breaking the entire script with a Lua error, just don't play it and print a warning instead 228 | if not EventList[eventname] then 229 | print(string.format("(?)\x81 Event [%s] does not exist!", eventname)) 230 | return 231 | end 232 | 233 | local ev = {} 234 | 235 | -- Setup the event from the beginning 236 | Event.__setupEvent(ev, eventname) 237 | 238 | -- (Get everything in args to be added to vars, killing the old _user variable) 239 | -- Reference a variable container if given any 240 | if (args) then 241 | for k,v in pairs(args) do 242 | (function() 243 | ev.vars[k] = v 244 | end)() 245 | end 246 | end 247 | 248 | -- Sets the caller of the event, which can be anything (except a function) 249 | if (caller) then 250 | ev.caller = caller 251 | end 252 | 253 | setmetatable(ev, eventMT) 254 | table.insert(Running_Events, ev) 255 | end 256 | 257 | -- TODO: seek and stop/resume all if needed 258 | function Event.stop(event) 259 | event.status = "stopped" 260 | end 261 | 262 | function Event.pause(event) 263 | event.status = "suspended" 264 | end 265 | 266 | function Event.resume(event) 267 | event.status = "running" 268 | end 269 | 270 | 271 | -- Destroys any event by found name 272 | -- (Deletion modes: [find] - Partial name matching / [hasvariable] - Match by variable name / [firstfound] - Only removes the first found event entry) 273 | function Event.destroy(eventname, mode) 274 | 275 | -- function wrapper to clear the data 276 | local function __set_ended(eventdata) 277 | Event.__endEvent(eventdata) 278 | Running_Events[eventdata] = nil 279 | Event.printdebug(string.format("(!)\x81 ALERT: Event [%s] was ended early by Event.destroy!", eventdata.name)) 280 | end 281 | 282 | mode = Event.default(mode, {}) 283 | 284 | Event.searchtable(function(_, ev) 285 | 286 | -- Finds a partial event name match 287 | if (mode.find) then 288 | if (string.find(ev.name, eventname)) then 289 | __set_ended(ev) 290 | Event.printdebug(string.format("(!)\x82 (------Find mode: Complete------)", ev.name)) 291 | return (mode.firstfound) and true or false 292 | end 293 | -- Finds a valid variable name inside of the event 294 | elseif (mode.var) then -- or mode.hasvar) then 295 | if (ev.vars[eventname]) then 296 | __set_ended(ev) 297 | return (mode.firstfound) and true or false 298 | end 299 | -- Finds a valid variable name inside of the named event 300 | -- ("ev_event1", {hasvar="varname",value=true}) 301 | elseif (mode.haskey) then 302 | if (ev.name == eventname and ev.vars[mode.haskey[1]] == mode.haskey[2]) then 303 | __set_ended(ev) 304 | return (mode.firstfound) and true or false 305 | end 306 | -- Default behavior (by name) 307 | else 308 | if (ev.name == eventname) then 309 | __set_ended(ev) 310 | Event.printdebug(string.format("(!)\x82 (------Default mode: Complete------)", ev.name)) 311 | return (mode.firstfound) and true or false 312 | end 313 | end 314 | end) 315 | end 316 | 317 | -- Destroys a group of states all in one go 318 | -- (Deletion modes: [find] - Partial name matching / [hasvariable] - Match by variable name / [firstfound] - Only removes the first found event entry) 319 | function Event.destroygroup(eventnamelist, mode) 320 | for i=1,#eventnamelist do 321 | Event.destroy(eventnamelist[i], mode) 322 | end 323 | end 324 | 325 | -- Ends the event that this is attached to without finding it 326 | function Event.destroyself(event) 327 | event.status = "dead" 328 | end 329 | 330 | -- Sets an event to be persistent between map changes 331 | function Event.persist(event, arg) 332 | event.persist = arg 333 | end 334 | 335 | -- Get the current state number number of the scope this is called in 336 | function Event.getcurrentstate(event, stepnum) 337 | return event.step 338 | end 339 | 340 | -- Sets a tag inside of the state 341 | function Event.settag(event, tagname) 342 | event.tags[tagname] = event.step 343 | 344 | Event.printdebug(string.format("\x82 -------[%s]: Set loop at Step %d-------", tagname, event.step)) 345 | 346 | return event.step 347 | end 348 | 349 | -- Go to the tag number given in the current state 350 | function Event.gototag(event, tagname) 351 | event.step = event.tags[tagname] 352 | event.status = "looped" 353 | 354 | Event.printdebug(string.format("\x82 -------[%s]: Returning to Step %d-------", tagname, event.tags[tagname] or -1)) 355 | end 356 | 357 | -- Go to a tag number if a condition is reached 358 | function Event.gototaguntil(event, tagname, cond) 359 | if not (cond) then 360 | Event.gototag(event, tagname) 361 | else 362 | -- do nothing 363 | end 364 | end 365 | 366 | -- Forces an event to wait until a set time 367 | function Event.wait(event, time, singleuse) 368 | 369 | -- Set time and suspended status 370 | if (event.sleeptime == 0 and not (event.status == "suspended")) then 371 | event.sleeptime = time 372 | event.status = "suspended" 373 | Event.printdebug(string.format("[%s] is now waiting. (%d)", event.name, time)) 374 | end 375 | 376 | -- Count down 377 | if (event.sleeptime > 0) then 378 | event.sleeptime = max(0, $1-1) 379 | event.looptrack = singleuse and $1+1 or 0 380 | else 381 | event.status = "resumed" 382 | end 383 | end 384 | 385 | local function waitcountdown(event) 386 | -- Count down 387 | if (event.sleeptime > 0) then 388 | event.sleeptime = max(0, $1-1) 389 | else 390 | event.status = "resumed" 391 | end 392 | end 393 | 394 | -- Waits until the condition is true, then unsuspend the event (with callback at end) 395 | function Event.waitUntil(event, cond, end_func) 396 | if not (cond) then 397 | event.status = "suspended" 398 | else 399 | event.status = "resumed" 400 | 401 | -- Run a callback function when the condition is reached if specified 402 | if (end_func) then 403 | end_func() 404 | end 405 | return true 406 | end 407 | end 408 | 409 | -- Pauses the entire state until its signal is responded to 410 | -- (attempted rewrite without waiting_on_signal) 411 | function Event.waitSignal(event, signalname, identifier) 412 | if (event.signal == "") then 413 | event.status = "suspended" 414 | event.signal = signalname 415 | Event.printdebug(string.format("[%s]: Seeking signal '%s'...", event.name, signalname)) 416 | elseif (event.signal == signalname .. "_resp" .. (identifier or "")) then 417 | event.status = "resumed" 418 | event.signal = "" 419 | end 420 | end 421 | 422 | -- Activates a signal 423 | function Event.signal(signalname, identifier) 424 | 425 | -- Seek all signals named similarly 426 | Event.searchtable(function(i, evclass) 427 | if (string.match(evclass.signal, signalname)) then 428 | 429 | evclass.signal = $1 .."_resp" .. (identifier or "") 430 | 431 | Event.printdebug(string.format("Signal '%s' found! Continuing...", signalname)) 432 | return 433 | end 434 | end) 435 | end 436 | 437 | -- Executes gallery image popup from a linedef 438 | function Event.LinedefExecute(line, trigger, sector) 439 | 440 | -- @ Flag Effects: 441 | -- [1] Block Enemies: 442 | -- [6] Not Climbable: 443 | -- [10] Repeat Midtexture (E5): 444 | -- @ Default: runs EV 'ev_[name]' with the texture of the floor/ceiling being the name of the EV to run 445 | -- 446 | -- if not (trigger and trigger.player and trigger.player.valid) then return end 447 | 448 | local player = trigger and trigger.player or nil 449 | 450 | if (player and player.bot > 0) then return end -- bots shouldn't be able to trigger this 451 | 452 | -- if (line.flags & ML_BLOCKMONSTERS) then 453 | -- elseif (line.flags & ML_NOCLIMB) then 454 | -- end 455 | -- if (line.flags & ML_EFFECT5) then 456 | -- end 457 | 458 | local evs_name = string.format("ev_%s", string.lower(line.frontsector.floorpic)) 459 | 460 | -- Event.start(seqname:gsub("%z", "")) 461 | -- Event.start(string.lower(evs_name), {execplayer=player, execmobj=trigger}) 462 | Event.start(evs_name, {execsector=sector, execlinedef=line, execmobj=trigger, execplayer=player}) 463 | end 464 | 465 | --[[-- Does a function once on an event during a wait() 466 | -- (for multiple, use doOrder instead) 467 | -- Waits until the condition is true, then unsuspend the event (wrapper ver.) 468 | rawset(_G, "doUntil", function(event, cond, while_func, end_func) 469 | if not (cond) then 470 | while_func() 471 | event.status = "suspended" 472 | else 473 | event.status = "resumed" 474 | 475 | -- Run a callback function when the condition is reached if specified 476 | if (end_func) then 477 | end_func() 478 | end 479 | 480 | Event.printdebug("doUntil has ended.") 481 | end 482 | end) 483 | 484 | -- Starts a list of functions per gameframe, and suspends itself on the last 485 | rawset(_G, "doOrder", function(event, funclist) 486 | 487 | -- Increase loop tracker until the end of the list 488 | if (event.looptrack < #funclist) then 489 | event.looptrack = $1+1 490 | event.status = "suspended" 491 | end 492 | 493 | -- Run the function type in the list 494 | if type(funclist[event.looptrack]) == "function" then 495 | do funclist[event.looptrack]() end 496 | end 497 | 498 | end)--]] 499 | 500 | -- This function progresses the state inside the event forward 501 | local function EventStateProgressor(e) 502 | 503 | -- Do not run if the status is normal or dead. Continue next iteration if so. 504 | if not (e.status == "normal" or e.status == "dead") then 505 | 506 | -- Set events to dead once they reach the end of the list 507 | if (e.step > #e.states) then 508 | e.status = "dead" 509 | return 510 | end 511 | 512 | -- Print some useful info to the log when enabled 513 | -- Event.printdebug(string.format("%d/%d %s[%s%s] - [w%d] [lt%d] [Action]:", e.step, #e.states, e.name, e.status, "|"..e.signal, e.sleeptime, e.looptrack)) 514 | 515 | -- the _entire_ state is stopped, nothing runs 516 | if e.status == "stopped" then return end 517 | 518 | -- TODO: what if suspended, and we want the step function to run only once? 519 | if not e.looptrack then 520 | do e.states[e.step](e.vars, e, e.caller or nil) end -- Run functions (: 521 | else 522 | waitcountdown(e) 523 | end 524 | 525 | -- the event is waiting/yielded 526 | if e.status == "suspended" then return end 527 | 528 | -- the event has looped backwards, and replaying itself 529 | if e.status == "looped" then e.status = "running" return end 530 | 531 | -- Progress the step of the list 532 | e.step = $1+1 533 | e.looptrack = 0 534 | 535 | end 536 | end 537 | 538 | -- Handles running events similar to coroutines, but only when they are activated 539 | function Event.RunEvents(event) 540 | 541 | for key,evclass in Event.spairs(Running_Events, function(t,a,b) return t[b].name < t[a].name end) do 542 | -- for key,evclass in pairs(Running_Events) do 543 | 544 | -- The status is dead, remove 545 | if (evclass.status == "dead") then 546 | Running_Events[key] = nil 547 | end 548 | 549 | EventStateProgressor(evclass) 550 | 551 | end 552 | end 553 | 554 | function Event.netvars(n) 555 | Running_Events = n($) 556 | end 557 | 558 | 559 | 560 | 561 | -- ===================== 562 | -- Hooks 563 | -- ===================== 564 | --[[ 565 | This is the structure of each container with fields we allow to be accessed: 566 | EventList: 567 | { 568 | name: {name, status, step, signal, tags, sleeptime, looptrack, vars{}, x--states{...}}, 569 | ... 570 | } 571 | 572 | Running: 573 | { 574 | {name, status, step, signal, tags, sleeptime, looptrack, x--states{...}}, 575 | ... 576 | } 577 | 578 | Vars: 579 | {...} 580 | --]] 581 | 582 | -- Syncs table data over netplay so players are not de-synced on join 583 | addHook("NetVars", function(network) 584 | Event.netvars(network) 585 | end) 586 | 587 | -- Destroy all events on map change 588 | function Event.MapReloadClearEvents() 589 | -- if an event does not want to be reset or ended, exclude it 590 | Event.searchtable(function(i, ev) 591 | if ev and ev.persist then return end 592 | Event.__endEvent(ev) 593 | end) 594 | end 595 | 596 | -- When specified, run an event by name on map load, and do other kinds of stuff 597 | function Event.h_MapLoad(gamemap) 598 | 599 | Event.MapReloadClearEvents() 600 | 601 | -- Run an event on map load per player 602 | if (mapheaderinfo[gamemap].loadevent) then 603 | for player in players.iterate do 604 | Event.start(mapheaderinfo[gamemap].loadevent:gsub("%z", ""), player) 605 | end 606 | end 607 | 608 | -- Run a singular event 609 | if (mapheaderinfo[gamemap].globalevent) then 610 | Event.start(mapheaderinfo[gamemap].globalevent:gsub("%z", "")) 611 | end 612 | 613 | end 614 | 615 | addHook("MapLoad", function(gamemap) 616 | Event.h_MapLoad(gamemap) 617 | end) 618 | 619 | addHook("MapChange", function() 620 | Event.MapReloadClearEvents() 621 | end) 622 | 623 | addHook("LinedefExecute", Event.LinedefExecute, "RUN_EVS") 624 | 625 | -- addHook("ThinkFrame", Event.RunEvents) 626 | -------------------------------------------------------------------------------- /hud/l_textboxesv2.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * l_textboxesv2.lua 3 | * (sprkizard) 4 | * (March 29, 2022, 0:00) 5 | * Desc: A re-rewrite of textboxes used in sugoi2, with 6 | a more minimalistic graphical style and more features 7 | 8 | * Usage: TODO: check wiki??? 9 | 10 | * Depends on: 11 | TODO: (none atm) 12 | ]] 13 | -- TODO: subtitle/cecho mode, center align (?), box background, icon frames, skin 14 | 15 | rawset(_G, "TextBox", {dialogs={}, debug=false}) 16 | 17 | -- textbox update/display table 18 | local textboxes = {} 19 | 20 | -- indexed functions that dialogs can access 21 | local textboxfunctions = {} 22 | 23 | -- Prints messy information on top of the dialog box 24 | function TextBox.debug_draw(v,x,y,textbox) 25 | if not TextBox.debug then return end 26 | local str = textbox.text:gsub("\n", " ") 27 | local info_output = string.format("ID: %d | Name: %s | Text: %s | Icon: %s | Printed: %d/%d | Auto: [%d/%d]| rel: (%d,%d) | abs: (%d,%d)", 28 | textbox.id or 0, tostring(textbox.name),str,tostring(textbox.icon),textbox.strpos,#textbox.text,textbox.linetime,textbox.auto,textbox.rx or 0,textbox.ry or 0,textbox.ax or 0,textbox.ay or 0) 29 | v.drawString(x, y, info_output, V_ALLOWLOWERCASE|V_SNAPTOBOTTOM, "small-thin") 30 | end 31 | 32 | function TextBox.isvalid(player) 33 | return (player and player.valid) 34 | end 35 | 36 | -- TODO: can i stop pasting this in multiple scripts every time i need it? :D 37 | function TextBox.randomchoice(choices) 38 | local RandomKey = P_RandomRange(1, #choices) 39 | if type(choices[RandomKey]) == "function" then 40 | choices[RandomKey]() 41 | else 42 | return choices[RandomKey] 43 | end 44 | end 45 | 46 | -- Adds a new dialog to the screen with a box identifier (TODO: ranges 10k-32k reserved for players with multiple ids?) 47 | function TextBox.add(boxid, args, playerslist) 48 | if type(boxid) == "string" then print("Box ID must be a number!") return end 49 | 50 | local new_tb = { 51 | id = boxid, 52 | name = (args and args.name) or "", 53 | text = (args and args.text) or " ", 54 | icon = (args and args.icon), 55 | button = (args and args.button), 56 | auto = (args and args.auto) or 3*TICRATE, 57 | nextid = (args and args.nextid), 58 | speed = (args and args.speed) or 0, 59 | delay = (args and args.delay) or 1, 60 | soundbank = (args and args.soundbank) or nil, -- Ex: {opensfx=_,printsfx=_,compsfx=_,nextsfx=_,endsfx=_} 61 | sb_atend = false, -- for compsfx 62 | rx = (args and args.rx), 63 | ry = (args and args.ry), 64 | ax = (args and args.ax), 65 | ay = (args and args.ay), 66 | hidebg = (args and args.hidebg), 67 | startpos = (args and args.startpos) or 0, 68 | strpos = 0,-- + startpos, 69 | func=(args and args.func), 70 | playerlist = playerslist or {}, 71 | linetime = 0, 72 | -- titmeout = 0, 73 | closing = false, 74 | persist = false, -- TODO: finish kept dialogs later 75 | } 76 | 77 | -- Wipe the last id and overwrites it with a new one under the same id (moved below def to prevent sound from playing each overwrite) 78 | -- perform a few tasks on overwrite: 79 | if textboxes[boxid] then 80 | 81 | -- Keeps the players used in the last dialog block if they exist 82 | if textboxes[boxid].playerlist then new_tb.playerlist = textboxes[boxid].playerlist end 83 | 84 | -- TOOO: Keeps the button prompt if given until it's set to zero 85 | -- if textboxes[boxid].button and new_tb.button ~= 0 then new_tb.button = textboxes[boxid].button end 86 | 87 | -- Keeps the soundbank if one was added 88 | if textboxes[boxid].soundbank and new_tb.soundbank == -1 then new_tb.soundbank = textboxes[boxid].soundbank end 89 | 90 | textboxes[boxid] = nil 91 | 92 | else 93 | -- Plays when opened for the very first time 94 | TextBox.playdialogsound(new_tb, "start") 95 | end 96 | 97 | textboxes[boxid] = new_tb 98 | 99 | end 100 | 101 | -- TODO: add 'players included' tables easily 102 | --[[function TextBox.addPlayer(boxid, args, player) 103 | end--]] 104 | 105 | -- Adds a new dialog 106 | function TextBox.newDialog(category, textid, icon, name, text, args, soundbank) 107 | 108 | local newargs = args or {} 109 | 110 | newargs.boxid = id 111 | newargs.name = name 112 | newargs.text = text 113 | newargs.icon = icon 114 | newargs.soundbank = soundbank 115 | 116 | -- TODO: ? print soundbank errors as dialog instead 117 | --[[if soundbank == -1 or type(soundbank) == "table" or not soundbank then 118 | newargs.soundbank = soundbank 119 | else 120 | newargs.soundbank = {} 121 | newargs.text = string.format("\x82WARNING IN DIALOG ID [%s]:\x80 \n-1 or table expected, got %s", tostring(textid), tostring(soundbank)) 122 | end--]] 123 | 124 | if (category) then 125 | if not TextBox.dialogs[category] then TextBox.dialogs[category] = {} end 126 | TextBox.dialogs[category][textid] = newargs 127 | else 128 | TextBox.dialogs[textid] = newargs 129 | end 130 | 131 | end 132 | 133 | -- Gets a dialog by id 134 | function TextBox.getDialog(textid, category) 135 | if (category) then 136 | if not (TextBox.dialogs[category]) then print("\x82WARNING:\x80 User-defined dialog does not exist!") end 137 | return TextBox.dialogs[category][textid] 138 | else 139 | return TextBox.dialogs[textid] 140 | end 141 | end 142 | 143 | -- Closes a dialog box 144 | function TextBox.close(boxid) 145 | if textboxes[boxid] then 146 | textboxes[boxid].closing = true 147 | end 148 | end 149 | 150 | -- Adds a dialog function that would run after or during a dialog (do NOT execute this during runtime) 151 | function TextBox.addDialogFunction(fun_name, fun) 152 | if (type(fun) == "function") then 153 | textboxfunctions[fun_name] = fun 154 | else 155 | textboxfunctions[fun_name] = nil 156 | print(string.format("\x82WARNING:\x80 Dialog function %s was not registered. (type is not a function)", tostring(fun_name))) 157 | end 158 | end 159 | 160 | function TextBox.refreshlist(removespecial) 161 | for _,t in pairs(textboxes) do 162 | if t and t.persist then return end 163 | t.closing = true 164 | end 165 | end 166 | 167 | local function seekplayers2(f, list) 168 | for i=1, #list do 169 | f(list[i]) 170 | end 171 | end 172 | 173 | -- Uses new text id for chaining dialogue together 174 | function TextBox.next_text(id, box) 175 | 176 | -- nextid is a number, find a new defined id in the dialogue table 177 | if (type(box.nextid) == "number") then 178 | -- TODO: nextid for secondary ids? 179 | TextBox.add(id, TextBox.dialogs[box.nextid]) 180 | 181 | -- nextid is a table, allows for searching a user-defined table that has stored dialogue 182 | elseif (type(box.nextid) == "table") then 183 | local userdef = box.nextid[1] -- id user-named 184 | local nextid = box.nextid[2] -- id 185 | TextBox.add(id, TextBox.dialogs[userdef][nextid]) 186 | end 187 | 188 | end 189 | 190 | -- Plays sound in the update function by type 191 | function TextBox.playdialogsound(txtbox, soundtype) 192 | 193 | local plyr = nil -- TODO: deleteme 194 | 195 | -- Ensures that sounds only play for the displayed player (incl. spectating) and not everybody at once 196 | -- also for players that do not have a dialog open by exclusion 197 | if (#txtbox.playerlist > 0) then 198 | for i=1,#txtbox.playerlist do 199 | local p = txtbox.playerlist[i] 200 | if (TextBox.isvalid(consoleplayer) and consoleplayer ~= p and displayplayer == consoleplayer) then return end 201 | -- if (TextBox.isvalid(consoleplayer) and not consoleplayer.indialog and displayplayer == consoleplayer) then return end 202 | end 203 | end 204 | 205 | if (soundtype == "start") then 206 | -- Plays when the dialog is opened 207 | if (txtbox.soundbank and txtbox.soundbank.startsfx) then 208 | S_StartSound(nil, txtbox.soundbank.startsfx, plyr) 209 | end 210 | elseif (soundtype == "open") then 211 | -- TODO: (played on a prompt) 212 | elseif (soundtype == "print") then 213 | 214 | -- Plays on each letter printed 215 | if (txtbox.soundbank and txtbox.soundbank.printsfx) then 216 | if (txtbox.strpos < txtbox.text:len()) 217 | and not (txtbox.text:sub(txtbox.strpos):byte() == 0 or txtbox.text:sub(txtbox.strpos):byte() == 32) then 218 | -- and (leveltime % txtbox.delay == 0) then 219 | 220 | -- Take a table if given, and mix the sounds around! 221 | if (type(txtbox.soundbank.printsfx) == "table") then 222 | S_StartSound(nil, TextBox.randomchoice(txtbox.soundbank.printsfx), plyr) 223 | else 224 | S_StartSound(nil, txtbox.soundbank.printsfx, plyr) 225 | end 226 | end 227 | end 228 | elseif (soundtype == "complete") then 229 | 230 | -- Plays at completion 231 | if (txtbox.soundbank and txtbox.soundbank.compsfx) then 232 | S_StartSound(nil, txtbox.soundbank.compsfx, plyr) 233 | end 234 | elseif (soundtype == "next") then 235 | 236 | -- Plays on the trigger to progress to a next set 237 | if (txtbox.soundbank and txtbox.soundbank.nextsfx) then 238 | S_StartSound(nil, txtbox.soundbank.nextsfx, plyr) 239 | end 240 | elseif (soundtype == "end") then 241 | 242 | -- Plays at the event which removes the dialog 243 | if (txtbox.soundbank and txtbox.soundbank.endsfx) then 244 | S_StartSound(nil, txtbox.soundbank.endsfx, plyr) 245 | end 246 | end 247 | end 248 | 249 | -- Updater 250 | function TextBox.textbox_update() 251 | 252 | if titlemap and titlemapinaction then return end 253 | 254 | for id,txtbox in pairs(textboxes) do 255 | 256 | -- Remove dialog from table if ended 257 | if (txtbox.closing) then 258 | textboxes[id] = nil 259 | continue 260 | end 261 | 262 | -- (The text string position has reached the end of the string length) 263 | if (txtbox.strpos >= txtbox.text:len()) then 264 | 265 | -- Wait for button press if no automatic, and a button is specified (if only a list exists) 266 | if (txtbox.button and #txtbox.playerlist > 0) then 267 | 268 | txtbox.linetime = 0 269 | 270 | -- Play a sound at the completion of dialog (triggers only once) 271 | if not txtbox.sb_atend then 272 | TextBox.playdialogsound(txtbox, "complete") 273 | txtbox.sb_atend = true 274 | 275 | -- Run a function at the end if there is one! 276 | if txtbox.func then textboxfunctions[txtbox.func]() end 277 | end 278 | 279 | -- TODO: find a better playerlist method 280 | -- close the textbox or turn to (nextid=) textbox text id 281 | for i=1,#txtbox.playerlist do 282 | 283 | local p = txtbox.playerlist[i] 284 | 285 | -- TODO: when all players in the list can't press a button or dont exist, we need to force a way out 286 | -- TextBox.close(txtbox.id) 287 | if not p.mo.valid then continue end 288 | 289 | -- Check for a button press. (TODO: if no players are able to press the button for x seconds, press it for them) 290 | if (p.cmd.buttons & txtbox.button) then 291 | if (txtbox.nextid) then 292 | TextBox.next_text(id, txtbox) 293 | TextBox.playdialogsound(txtbox, "next") 294 | else 295 | txtbox.closing = true 296 | TextBox.playdialogsound(txtbox, "start") 297 | TextBox.playdialogsound(txtbox, "end") 298 | end 299 | end 300 | end 301 | 302 | -- Automatic progression is enabled so use the user-defined or default value instead 303 | elseif (txtbox.auto and txtbox.linetime < txtbox.auto) then 304 | txtbox.linetime = $1+1 305 | 306 | -- Play a sound at the completion of dialog (triggers only once) 307 | if not txtbox.sb_atend then 308 | TextBox.playdialogsound(txtbox, "complete") 309 | txtbox.sb_atend = true 310 | 311 | -- Run a function at the end! 312 | if txtbox.func then textboxfunctions[txtbox.func]() end 313 | end 314 | else 315 | -- close the textbox or turn to (nextid=) textbox text id 316 | if (txtbox.nextid) then 317 | TextBox.next_text(id, txtbox) 318 | TextBox.playdialogsound(txtbox, "next") 319 | else 320 | txtbox.closing = true 321 | TextBox.playdialogsound(txtbox, "start") 322 | TextBox.playdialogsound(txtbox, "end") 323 | end 324 | end 325 | 326 | else 327 | if leveltime % txtbox.delay == 0 then 328 | txtbox.strpos = min($1 + 1 + txtbox.speed, txtbox.text:len()) 329 | 330 | -- Plays printing sounds (while ignoring spaces and nothing) 331 | TextBox.playdialogsound(txtbox, "print") 332 | end 333 | -- TODO: run a function during printing? 334 | end 335 | end 336 | end 337 | 338 | function TextBox.textbox_drawer(v, stplyr, cam) 339 | 340 | -- TODO: center/right aligned math 341 | -- Screen settings 342 | local scrwidth = v.width() / v.dupx() -- screen width 343 | local scrheight = v.height() / v.dupy() -- screen height 344 | 345 | local boxheight = 52 -- textbox height (52:78) 346 | 347 | -- v.drawFill(320/2, 0, 1, 200, 35) 348 | -- v.drawFill(320/2, 200/2, 320, 1, 160) 349 | 350 | -- (Not much can be done about splitscreen except prevent it from drawing twice) 351 | if splitscreen and stplyr == displayplayer then return end 352 | 353 | -- Prepare a textbox (snap to bottom) 354 | for _,textbox in pairs(textboxes) do 355 | 356 | -- Prevent players from seeing dialogs if they are not viewing their own (excluding viewing others) 357 | if (#textbox.playerlist > 0) then 358 | for i=1,#textbox.playerlist do 359 | local p = textbox.playerlist[i] 360 | if (TextBox.isvalid(consoleplayer) and stplyr ~= p) then return end 361 | end 362 | end 363 | -- Textbox origin point (x,y) (top-left) 364 | local prompt_x = ((320-scrwidth)/2) 365 | local prompt_y = (200-boxheight) 366 | local textoffset = 4 -- icon offset 367 | 368 | -- Set custom coordinates of the textbox (relative/absolute) 369 | if (textbox.rx ~= nil or textbox.ry ~= nil) then 370 | prompt_x = $1 + (textbox.rx or 0) 371 | prompt_y = $1 - (textbox.ry or 0) 372 | elseif (type(textbox.ax) == "number" and type(textbox.ay) == "number") then 373 | prompt_x = (textbox.ax) 374 | prompt_y = (textbox.ay)-17 375 | textoffset = 0 376 | end 377 | 378 | TextBox.debug_draw(v, prompt_x, prompt_y-4, textbox) 379 | 380 | -- Draw the background to stretch to the screen edges 381 | if not (textbox.hidebg) then 382 | v.drawStretched(prompt_x*FU, prompt_y*FU, scrwidth*FU, boxheight*FU, v.cachePatch("~031G"), V_30TRANS|V_SNAPTOBOTTOM) 383 | end 384 | 385 | -- Draw the icon (4:152) (+ text offset) 386 | if (textbox.icon) then 387 | v.drawScaled((prompt_x+4)*FU, (prompt_y+4)*FU, FRACUNIT/6+FU/160, v.cachePatch(textbox.icon), V_SNAPTOBOTTOM) 388 | textoffset = 52 389 | end 390 | 391 | -- Show name (52:153) 392 | if (textbox.name) then 393 | v.drawString(prompt_x+textoffset, prompt_y+4, "\x82"..textbox.name, V_ALLOWLOWERCASE|V_SNAPTOBOTTOM, "left") 394 | end 395 | 396 | -- Draw a graphic to show a button press (bottom-right) 397 | if (textbox.sb_atend and not textbox.linetime) then 398 | local blink = (leveltime % 10 >= 6) and 134 or 30 399 | v.drawFill(prompt_x+scrwidth-8, prompt_y+(boxheight-8), 6, 6, blink) 400 | end 401 | 402 | -- Draw the text (52:165) 403 | v.drawString(prompt_x+textoffset, prompt_y+17, textbox.text:sub(0, textbox.strpos), V_ALLOWLOWERCASE|V_SNAPTOBOTTOM, "left") 404 | 405 | end 406 | end 407 | 408 | hud.add(TextBox.textbox_drawer, "game") 409 | 410 | function TextBox.netvars(n) 411 | textboxes = n($) 412 | end 413 | 414 | 415 | -- Hooks 416 | addHook("MapLoad", function() 417 | TextBox.refreshlist() 418 | end) 419 | 420 | addHook("MapChange", function() 421 | TextBox.refreshlist() 422 | end) 423 | 424 | addHook("ThinkFrame", function() 425 | TextBox.textbox_update() 426 | end) 427 | 428 | addHook("NetVars", function(network) 429 | TextBox.netvars(network) 430 | end) 431 | 432 | 433 | 434 | --[[ 435 | -- Examples: 436 | 437 | -- * Dialog chains 438 | -------------------------- 439 | TextBox.newDialog(nil, 2, "AMYRTALK", "Amy", "Apples\noranges\nbananas", {nextid=3}, {printsfx=sfx_oratxt}) 440 | TextBox.newDialog(nil, 3, "AMYRTALK", "Amy", "(A soundbank is being\nused for printing)", {nextid=4}, {printsfx=sfx_oratxt}) 441 | TextBox.newDialog(nil, 4, "AMYRTALK", "Amy", "This is the third dialog\nchain which is also the end.\nGood-bye!", nil, {printsfx=sfx_oratxt}) 442 | TextBox.newDialog("Custom", 6, "AMYRTALK", "Amy", "This is a custom categorized\ndialoge! This is to prevent\noverwrites and conflicts!", nil, {printsfx=sfx_bttx5}) 443 | 444 | -- * Soundbanks 445 | -------------------------- 446 | local testsndbank = {startsfx=sfx_strpst,printsfx=sfx_radio,compsfx=sfx_menu1,nextsfx=sfx_appear,endsfx=sfx_addfil} 447 | 448 | TextBox.newDialog(nil, 1, "AMYRTALK", "Amy", "This is a test dialog that\nwill print sound effects on\ndifferent textbox events.", {nextid=9}, testsndbank) 449 | TextBox.newDialog(nil, 9, "AMYRTALK", "Amy", "This is the next set of\ndialog.", nil, testsndbank) 450 | 451 | -- * Button and sound trigger stuff 452 | -------------------------- 453 | TextBox.newDialog(nil, 10, "AMYRTALK", "Amy", "Press [jump] to continue.", {nextid=11, button=BT_JUMP}, {printsfx=sfx_oratxt,nextsfx=sfx_appear}) 454 | TextBox.newDialog(nil, 11, "AMYRTALK", "Amy", VERSIONSTRING.." is the latest SRB2 version.\n[jump]", {nextid=12, button=BT_JUMP}, -1) 455 | TextBox.newDialog(nil, 12, "AMYRTALK", "Amy", "Text 1 [jump]", {nextid=13, button=BT_JUMP}, -1) 456 | TextBox.newDialog(nil, 13, "AMYRTALK", "Amy", "Text 2", {nextid=14}, -1) 457 | TextBox.newDialog(nil, 14, "AMYRTALK", "Amy", "Text 3", {nextid=15}, -1) 458 | TextBox.newDialog(nil, 15, "AMYRTALK", "Amy", "Text 4 [jump]", {nextid=16, button=BT_JUMP}) 459 | TextBox.newDialog(nil, 16, "AMYRTALK", "Amy", "Press [spin] to end.", {button=BT_SPIN}, {printsfx=sfx_oratxt,endsfx=sfx_appear}) 460 | TextBox.newDialog(nil, 17, "AMYRTALK", "Amy", "New dialog 1", {button=BT_SPIN}) 461 | 462 | -- * Dialog functions 463 | -------------------------- 464 | TextBox.newDialog(nil, 18, "AMYRTALK", "Amy", "1: A function will be executed\nat the end of this dialog.", {nextid=19, auto=10*TICRATE, func="TestExec1"}, {printsfx=sfx_oratxt,nextsfx=sfx_appear}) 465 | TextBox.newDialog(nil, 19, "AMYRTALK", "Amy", "2: A function will be executed\nat the end of this dialog.", {auto=10*TICRATE, func="TestExec2"}, -1) 466 | 467 | -- Turns global map brightness down to 150 468 | TextBox.addDialogFunction("TestExec1", function() P_FadeLight(65535, 150, 10*TICRATE, true, true) end) 469 | TextBox.addDialogFunction("TestExec2", function() P_FadeLight(65535, 255, 10*TICRATE, true, true) end) 470 | 471 | 472 | -- Thinkframe sandbox 473 | -------------------------- 474 | addHook("ThinkFrame", function() 475 | 476 | -- # 1 various boxes 477 | -- if leveltime == 3*TICRATE then 478 | -- TextBox.add(1, {icon="AMYRTALK", name="Amy", text="Hello world!\n(I am editing directly)"}) 479 | -- elseif (leveltime == 9*TICRATE) then 480 | -- TextBox.add(12, {icon="AMYRTALK", name="Amy", text="Hello world!", rx=0, ry=100}) 481 | -- elseif (leveltime == 15*TICRATE) then 482 | -- TextBox.add(12, TextBox.getDialog(nil, 2)) 483 | -- elseif (leveltime == 30*TICRATE) then 484 | -- TextBox.add(12, TextBox.getDialog(6, "Custom")) 485 | -- elseif (leveltime == 40*TICRATE) then 486 | -- TextBox.add(12, TextBox.getDialog(nil, 1)) 487 | -- end 488 | 489 | -- # 2 per player 490 | -- if (leveltime == 5*TICRATE) then 491 | 492 | -- local table_player = {} 493 | 494 | -- for player in players.iterate do 495 | -- if TextBox.isvalid(player) and player.rings > 1 then table.insert(table_player, player) end 496 | -- print(string.format("Player List Count: %d | Player: %d - Rings: %d", #table_player or -1, #player, player.rings)) 497 | -- end 498 | 499 | -- TextBox.add(12, TextBox.getDialog(nil, 10), table_player) 500 | -- end 501 | 502 | -- # 3 Overwrite 503 | -- if (leveltime == 6*TICRATE) then 504 | -- TextBox.add(12, TextBox.getDialog(nil, 17), table_player) 505 | -- end 506 | 507 | -- # 4 Function 508 | -- if (leveltime == 3*TICRATE) then 509 | -- TextBox.add(1, TextBox.getDialog(18)) 510 | -- -- TextBox.add(1, TextBox.getDialog(18), {players[1] or nil}) 511 | -- end 512 | end) 513 | --]] 514 | -------------------------------------------------------------------------------- /game/Axis2D/l_Axis2D-UDMF.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | * l_Axis2D-UDMF.lua 4 | * (sprkizard varren, fickleheart, sphere) 5 | * (September 6, 2024) 6 | 7 | * Version: 1.0 8 | 9 | * Desc: Public release version 1.0 10 | Feel free to use it for your own purposes! In exchange, if you make a change that 11 | improves it, please share it with us! 12 | 1.0: Proper UDMF support, remove legacy system 13 | 14 | * Quick guide: 15 | 1. Add "Lua.Axis2DUDMF = true" to your level header. 16 | 2. Add an axis to your map with a unique Order value above 0. Make sure its Direction setting is set correctly! 17 | 3. Add a trigger sector (or intangible FOF) somewhere on the axis' path, triggering a linedef executor with action 443 (Call Lua Function), with P_AXIS2D as the function name. The linedef's parameters are as follows: 18 | - Linedef tag = The Order value of the axis to snap to 19 | - Linedef angle = Camera angle, relative to the player's movement direction, should usually be 90 20 | - Argument 1 = Camera distance, in fracunits (defaults to 448) 21 | - Argument 2 = Camera height, in fracunits (defaults to 0) 22 | - Argument 3 = Camera aiming, in degrees. Positive values point up, negative values point down. (defaults to 0) 23 | - Argument 4 = Camera angle (defaults to 0, relative to a side view of the player) 24 | - Argument 5 = Flags: 25 | 1 - If enabled, the camera angle becomes absolute and won't rotate with the player 26 | 2 - If enabled, use linedef angle to set the camera angle instead 27 | 4. It is also possible to place an Axis Transfer Line, to make the player go in a straight line. This only requires one Axis Transfer Line object, with its angle set in the direction the player should move in. 28 | 5. To exit Axis2D and go back into 3D, set up a linedef executor with tag 0. 29 | 30 | ]] 31 | 32 | 33 | 34 | -- Killswitch to avoid the script loading multiple times 35 | if axis2dudmf then 36 | print ("Axis2D for UDMF already loaded. Aborting...") 37 | return 38 | end 39 | 40 | rawset(_G, "axis2dudmf", {legacymode = false}) 41 | 42 | -- Toggle for spinning spring animation (for giggles) 43 | --local springspin = CV_RegisterVar({"springspin", 1, 0, CV_OnOff}) 44 | local springspin = CV_RegisterVar({"springspin", 0, 0, CV_OnOff}) 45 | 46 | -- Axes found, so we don't have to look them up later 47 | local axes = {lastmap = 0} 48 | 49 | 50 | 51 | 52 | 53 | -- Checks what control style is currently active 54 | function axis2dudmf.IsControlStyle(player) 55 | 56 | -- STRAFE [0,0] 57 | -- STANDARD [0,4] 58 | -- SIMPLE [2,4] sessionanalog/directionchar 59 | 60 | if (player.pflags & PF_ANALOGMODE and PF_DIRECTIONCHAR) then 61 | return "simple" 62 | elseif (player.pflags &~ PF_ANALOGMODE and player.pflags & PF_DIRECTIONCHAR) then 63 | return "standard" 64 | elseif (player.pflags &~ (PF_ANALOGMODE and PF_DIRECTIONCHAR)) then 65 | return "strafe" 66 | end 67 | 68 | end 69 | 70 | -- Wrapper for checking if player is on an axis 71 | function axis2dudmf.PlayerOnAxis(player) 72 | if not player and player.mo.valid then return end 73 | return (player.mo.currentaxis) and true or false 74 | end 75 | 76 | -- Refreshes the axis cache if needed 77 | function axis2dudmf.CheckAxes() 78 | 79 | -- 6-30-2022: Only check if used through map headers 80 | if not mapheaderinfo[gamemap].axis2dudmf then return end 81 | 82 | if axes.lastmap ~= gamemap then 83 | axes = {lastmap = gamemap} 84 | print("Preparing Axis2D cache...") 85 | for mo in mobjs.iterate() do 86 | if mo.type == MT_AXIS then 87 | --print("Axis found!") 88 | local axisinfo = {} 89 | axisinfo.x = mo.x 90 | axisinfo.y = mo.y 91 | axisinfo.radius = mo.spawnpoint.args[2] 92 | axisinfo.flipped = mo.spawnpoint.args[3] 93 | axes[mo.spawnpoint.args[1]] = axisinfo 94 | --print("Storing axis #" .. mo.spawnpoint.args[1] .. " in table...") 95 | --print(axisinfo.x .. " " .. axisinfo.y .. " " .. axisinfo.radius) 96 | elseif mo.type == MT_AXISTRANSFERLINE then 97 | --print("Line axis found!") 98 | local axisinfo = {} 99 | axisinfo.basex = mo.x 100 | axisinfo.basey = mo.y 101 | axisinfo.angle = mo.angle 102 | axes[mo.spawnpoint.args[1]] = axisinfo 103 | --print("Storing axis #" .. mo.spawnpoint.args[1] .. " in table...") 104 | --print(axisinfo.basex .. " " .. axisinfo.basey .. " " .. axisinfo.angle) 105 | elseif mo.type == MT_AXISTRANSFER then 106 | continue -- Ignore these, but keep going in the list 107 | else 108 | --print("End of list.") 109 | break -- Axis objects always start off the list, so now we know there are no more to parse 110 | end 111 | axes[mo.spawnpoint.args[1]].number = mo.spawnpoint.args[1] 112 | end 113 | end 114 | end 115 | 116 | addHook("MapLoad", axis2dudmf.CheckAxes) -- Refresh axes on new map 117 | 118 | -- Function to get the vector of a given object's axis 119 | function axis2dudmf.GetVector(mo) 120 | 121 | if mo.currentaxis == nil then 122 | return nil 123 | end 124 | 125 | if mo.currentaxis.angle ~= nil then 126 | 127 | mo.currentaxis.x = mo.x+cos(mo.currentaxis.angle-ANGLE_90) 128 | mo.currentaxis.y = mo.y+sin(mo.currentaxis.angle-ANGLE_90) 129 | mo.currentaxis.radius = 1 130 | mo.currentaxis.flipped = false 131 | 132 | return mo.currentaxis.angle-ANGLE_90 133 | else -- Circular axes 134 | return R_PointToAngle2(mo.currentaxis.x, mo.currentaxis.y, mo.x, mo.y) 135 | end 136 | end 137 | 138 | -- Function to switch object to a particular axis, or eject them from the Axis2D system 139 | function axis2dudmf.SwitchAxis(mo, axisnum) 140 | 141 | axis2dudmf.CheckAxes() -- For starters, make the axis table if it's not already done 142 | 143 | local player = mo.player -- Special handling for players 144 | local oldangle 145 | 146 | if player then 147 | oldangle = axis2dudmf.GetVector(mo) 148 | if mo.currentaxis and mo.currentaxis.flipped then 149 | oldangle = $1+ANGLE_180 150 | end 151 | 152 | -- Lactozilla: fix backwards spindash, part 1 153 | if mo.ax2d_angle == nil 154 | mo.ax2d_angle = mo.angle 155 | end 156 | if mo.ax2d_dashflags == nil 157 | mo.ax2d_dashflags = 0 158 | end 159 | if mo.ax2d_dashspeed == nil 160 | mo.ax2d_dashspeed = 0 161 | end 162 | end 163 | 164 | --print("Changing to axis " .. l.tag) 165 | --if axis.angle then 166 | -- print("Axis is a straight line.") 167 | --end 168 | 169 | if player and (oldangle ~= nil) then 170 | 171 | local newangle = axis2dudmf.GetVector(mo) 172 | 173 | if mo.currentaxis and mo.currentaxis.flipped then 174 | oldangle = $1+ANGLE_180 175 | end 176 | 177 | if mo.glidediff ~= nil then 178 | mo.glidediff = $1-newangle+oldangle 179 | end 180 | 181 | if -abs(newangle-oldangle) < ANGLE_270-(32<<16) then 182 | if mo.controlflip then 183 | mo.controlflip = 0 184 | elseif player.cmd.sidemove < 0 then 185 | mo.controlflip = -1 186 | else 187 | mo.controlflip = 1 188 | end 189 | end 190 | end 191 | end 192 | 193 | 194 | addHook("LinedefExecute", function(l, mo) 195 | 196 | local axis = axes[l.tag] 197 | local axisnum = l.tag 198 | local player = mo.player 199 | 200 | 201 | if axisnum == 0 then 202 | 203 | --print("Ejecting the player from the 2D track...") 204 | if player and mo.currentaxis then 205 | axis2dudmf.EjectPlayer(player) 206 | end 207 | 208 | mo.currentaxis = nil 209 | return 210 | end 211 | 212 | if mo.currentaxis and mo.currentaxis.number == axisnum then 213 | return -- We're already on this axis, so no need to reset everything 214 | end 215 | 216 | 217 | if not axis then 218 | print("ERROR: Axis " .. l.tag .. " does not exist! Please create it!") 219 | return 220 | end 221 | 222 | -- Set Camera distance 223 | if (l.args[0] > 0) 224 | axis.camdist = l.args[0]*FRACUNIT 225 | else 226 | axis.camdist = false 227 | end 228 | 229 | -- Set Camera height 230 | if (l.args[1] > 0) then 231 | axis.camheight = l.args[1]*FRACUNIT 232 | else 233 | axis.camheight = false 234 | end 235 | 236 | -- Set Camera viewaiming 237 | if (l.args[2]) then 238 | axis.camaiming = FixedAngle(l.args[2]*FRACUNIT) 239 | else 240 | axis.camaiming = 0 241 | end 242 | 243 | -- Flags: 244 | 245 | -- 1 - Use absolute angle instead of relative, not rotating with the player 246 | if (l.args[4] & 1) then 247 | axis.camangleabs = true 248 | else 249 | axis.camangleabs = false 250 | end 251 | 252 | -- 2 - Set camera angle with linedef angle, instead of argument 4 253 | if (l.args[4] & 3) then 254 | axis.camangle = R_PointToAngle2(0, 0, l.dx, l.dy) - ANGLE_180 255 | elseif (l.args[4] & 2) 256 | axis.camangle = R_PointToAngle2(0, 0, l.dx, l.dy) 257 | else 258 | axis.camangle = FixedAngle(l.args[3]*FRACUNIT) 259 | end 260 | 261 | 262 | mo.currentaxis = axis 263 | axis2dudmf.SwitchAxis(mo, l.tag) 264 | end, "P_Axis2D") 265 | 266 | 267 | -- Snap mobj to axis 268 | function axis2dudmf.SnapMobj(mo) 269 | 270 | -- Safety precaution! 271 | if not mo.currentaxis then return end 272 | 273 | local angle 274 | 275 | -- Straight line axes 276 | if mo.currentaxis.angle ~= nil then 277 | angle = mo.currentaxis.angle-ANGLE_90 278 | mo.currentaxis.x = mo.x+cos(angle) 279 | mo.currentaxis.y = mo.y+sin(angle) 280 | mo.currentaxis.radius = 1 281 | mo.currentaxis.flipped = false 282 | else -- Circular axes 283 | angle = R_PointToAngle2(mo.currentaxis.x, mo.currentaxis.y, mo.x, mo.y) 284 | end 285 | 286 | -- Snap player to position on axis 287 | local snapx, snapy 288 | 289 | angle = axis2dudmf.GetVector(mo) 290 | 291 | if mo.currentaxis.angle ~= nil then 292 | 293 | local pangle = R_PointToAngle2(mo.currentaxis.basex, mo.currentaxis.basey, mo.x, mo.y) 294 | pangle = $1-mo.currentaxis.angle 295 | 296 | local pdist = R_PointToDist2(mo.currentaxis.basex, mo.currentaxis.basey, mo.x, mo.y) 297 | pdist = FixedMul(cos(pangle), pdist) 298 | 299 | snapx = mo.currentaxis.basex + FixedMul(pdist, cos(mo.currentaxis.angle)) 300 | snapy = mo.currentaxis.basey + FixedMul(pdist, sin(mo.currentaxis.angle)) 301 | else 302 | local distfactor = R_PointToDist2(mo.currentaxis.x, mo.currentaxis.y, mo.x, mo.y)/mo.currentaxis.radius 303 | 304 | snapx = mo.currentaxis.x + FixedDiv(mo.x - mo.currentaxis.x, distfactor) 305 | snapy = mo.currentaxis.y + FixedDiv(mo.y - mo.currentaxis.y, distfactor) 306 | -- Replace old cos/sin to prevent drifting 307 | end 308 | 309 | if P_AproxDistance(mo.x-snapx, mo.y-snapy) < FRACUNIT*2 then 310 | mo.oldpos = { 311 | x = mo.x, 312 | y = mo.y--, 313 | --z = mo.z 314 | } 315 | return -- Close enough, let's just not worry about moving them around 316 | end 317 | 318 | if mo.oldpos and not P_TryMove(mo, snapx, snapy, true) then 319 | -- There was an issue adjusting the player to the axis. Figure this part out later! 320 | P_MoveOrigin(mo, mo.oldpos.x, mo.oldpos.y, mo.z) 321 | --mo.momx = $1/-5 322 | --mo.momy = $1/-5 323 | --print("HIT") 324 | end 325 | 326 | mo.oldpos = { 327 | x = mo.x, 328 | y = mo.y 329 | } 330 | 331 | 332 | if mo.player then return end -- The player mobj handles this already! 333 | 334 | -- Normalize momentum to angle 335 | local vectordist = R_PointToDist2(0, 0, mo.momx, mo.momy) 336 | local vectorang = R_PointToAngle2(0, 0, mo.momx, mo.momy)-angle 337 | if vectorang > 0 then 338 | vectorang = ANGLE_90 339 | else 340 | vectorang = -ANGLE_90 341 | end 342 | P_InstaThrust(mo, angle+vectorang, vectordist) 343 | end 344 | 345 | 346 | -- Made to eject the player and reset any changes since these lines are used multiple times here 347 | function axis2dudmf.EjectPlayer(player) 348 | 349 | player.mo.currentaxis = nil 350 | 351 | -- Reset status 352 | if not (player.mo.currentaxis) then 353 | player.normalspeed = skins[player.mo.skin].normalspeed 354 | player.thrustfactor = skins[player.mo.skin].thrustfactor 355 | player.accelstart = skins[player.mo.skin].accelstart 356 | player.acceleration = skins[player.mo.skin].acceleration 357 | if player.charability == CA_THOK then 358 | player.actionspd = skins[player.mo.skin].actionspd 359 | end 360 | player.runspeed = skins[player.mo.skin].runspeed 361 | player.jumpfactor = skins[player.mo.skin].jumpfactor 362 | player.pflags = $1&~PF_FORCESTRAFE 363 | end 364 | end 365 | 366 | 367 | local function SetCamera(player, x, y, z) 368 | 369 | local mo = player.mo 370 | 371 | if not (player.camera and player.camera.valid) then 372 | player.camera = P_SpawnMobj(mo.x, mo.y, mo.z, MT_GFZFLOWER1) 373 | player.camera.flags = MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY|MF_NOTHINK 374 | player.camera.flags2 = MF2_DONTDRAW 375 | P_MoveOrigin(player.camera, x, y, z) 376 | end 377 | 378 | P_MoveOrigin(player.camera, player.camera.x+(x-player.camera.x)/4, player.camera.y+(y-player.camera.y)/4, player.camera.z+(z-player.camera.z)/4) 379 | end 380 | 381 | addHook("AbilitySpecial", function(player) 382 | -- Lactozilla: fix backwards thok, part 1 383 | if player.mo.currentaxis then 384 | player.mo.angle = player.mo.ax2d_angle 385 | end 386 | end) 387 | 388 | 389 | -- Player management! 390 | addHook("PlayerThink", function(player) 391 | 392 | local mo = player.mo 393 | 394 | if mo.flags2 & MF2_TWOD then 395 | SetCamera(player, mo.x, mo.y - 448*FRACUNIT, mo.z + 20*FRACUNIT) 396 | if player.awayviewtics <= 2 then 397 | player.awayviewtics = 2 398 | player.awayviewmobj = player.camera 399 | player.awayviewmobj.angle = R_PointToAngle2(player.awayviewmobj.x, player.awayviewmobj.y, mo.x, mo.y) 400 | end 401 | player.awayviewmobj.momz = 0 402 | end 403 | 404 | if mo.currentaxis then 405 | player.pflags = $1|PF_FORCESTRAFE 406 | local sidemove = player.cmd.sidemove 407 | 408 | if mo.controlflip then -- Flip controls on axis flip, until the player stops holding their direction 409 | if mo.controlflip == -1 and sidemove >= 0 then 410 | mo.controlflip = 0 411 | elseif mo.controlflip == 1 and sidemove <= 0 then 412 | mo.controlflip = 0 413 | else 414 | sidemove = -$ 415 | end 416 | end 417 | 418 | if not player.climbing then 419 | if sidemove < 0 then 420 | mo.isfacingleft = true 421 | elseif sidemove > 0 then 422 | mo.isfacingleft = false 423 | end 424 | if player.onwall and (player.cmd.buttons & BT_JUMP) and not (player.onwall & BT_JUMP) then 425 | mo.isfacingleft = not mo.isfacingleft 426 | end 427 | player.onwall = false 428 | else 429 | player.onwall = 1|(player.cmd.buttons & BT_JUMP) 430 | -- Remove all horizontal momentum to prevent player from falling off walls when pushing horizontal input as climbing starts 431 | mo.momx = 0 432 | mo.momy = 0 433 | end 434 | 435 | local angle 436 | 437 | -- Straight line axes 438 | if mo.currentaxis.angle ~= nil then 439 | angle = mo.currentaxis.angle-ANGLE_90 440 | mo.currentaxis.x = mo.x+cos(angle) 441 | mo.currentaxis.y = mo.y+sin(angle) 442 | mo.currentaxis.radius = 1 443 | mo.currentaxis.flipped = false 444 | else -- Circular axes 445 | angle = R_PointToAngle2(mo.currentaxis.x, mo.currentaxis.y, mo.x, mo.y) 446 | end 447 | 448 | -- Snap player to position on axis 449 | axis2dudmf.SnapMobj(mo) 450 | 451 | -- Handle camera 452 | if player.mo.health then -- Don't move the camera when the player's dead! 453 | --local factor = 1 454 | local camangle = angle 455 | if mo.currentaxis.flipped then 456 | --factor = -1 457 | camangle = $1+ANGLE_180 458 | end 459 | if mo.currentaxis.camangle ~= nil then 460 | if mo.currentaxis.camangleabs then 461 | camangle = 0 462 | end 463 | camangle = $1+mo.currentaxis.camangle 464 | end 465 | if not mo.currentaxis.camdist then 466 | mo.currentaxis.camdist = 448*FRACUNIT 467 | end 468 | if not mo.currentaxis.camheight then 469 | mo.currentaxis.camheight = 32*FRACUNIT 470 | end 471 | if not mo.currentaxis.camaiming then 472 | mo.currentaxis.camaiming = 0 473 | end 474 | 475 | -- Attempt to track if your distance from the camera and act if far above tolerance value (eg. teleporting) 476 | if player.camera and player.camera.valid then 477 | 478 | local axiscamdist = FixedHypot(mo.currentaxis.x-player.camera.x, mo.currentaxis.y-player.camera.y) 479 | local playercamdist = FixedHypot(mo.currentaxis.x-mo.x, mo.currentaxis.y-mo.y) 480 | 481 | --print("camera dist from axis: "..axiscamdist/FRACUNIT) 482 | --print("p dist from axis: "..(playercamdist+ mo.currentaxis.camdist)/FRACUNIT) 483 | 484 | if axiscamdist > (playercamdist+mo.currentaxis.camdist)+128*FRACUNIT then 485 | P_MoveOrigin(player.camera, 486 | mo.currentaxis.x+(cos(angle)*mo.currentaxis.radius)+FixedMul(cos(camangle), mo.currentaxis.camdist), 487 | mo.currentaxis.y+(sin(angle)*mo.currentaxis.radius)+FixedMul(sin(camangle), mo.currentaxis.camdist), 488 | mo.z+20*FRACUNIT+(mo.currentaxis.camheight) 489 | ) 490 | end 491 | end 492 | 493 | SetCamera(player, 494 | mo.currentaxis.x+(cos(angle)*mo.currentaxis.radius)+FixedMul(cos(camangle), mo.currentaxis.camdist), 495 | mo.currentaxis.y+(sin(angle)*mo.currentaxis.radius)+FixedMul(sin(camangle), mo.currentaxis.camdist), 496 | mo.z+20*FRACUNIT+(mo.currentaxis.camheight) 497 | ) 498 | end 499 | if player.awayviewtics <= 2 then 500 | player.awayviewtics = 2 501 | player.awayviewmobj = player.camera 502 | player.awayviewmobj.angle = R_PointToAngle2(player.awayviewmobj.x, player.awayviewmobj.y, mo.x, mo.y) 503 | player.awayviewaiming = mo.currentaxis.camaiming -- Awayviewaiming property 504 | end 505 | player.awayviewmobj.momz = 0 506 | 507 | -- Set player angle 508 | if (player.pflags & PF_GLIDING) then 509 | local tangle = angle 510 | if mo.currentaxis.flipped then 511 | tangle = $1^^ANGLE_180 512 | end 513 | 514 | if not mo.glidediff then 515 | mo.glidediff = mo.angle-tangle 516 | end 517 | 518 | if abs(sidemove) < 3 then -- Default angle to what it's at now 519 | mo.isfacingleft = mo.glidediff < 0 520 | end 521 | 522 | if mo.isfacingleft then 523 | mo.glidediff = $1-(ANG10/2) 524 | if mo.glidediff < ANGLE_270 then 525 | mo.glidediff = ANGLE_270 526 | end 527 | else 528 | mo.glidediff = $1+(ANG10/2) 529 | if mo.glidediff > ANGLE_90 then 530 | mo.glidediff = ANGLE_90 531 | end 532 | end 533 | 534 | mo.angle = tangle+mo.glidediff 535 | 536 | -- Fuck this game's shitty latching-on code! I'm rewriting it myself! 537 | local waterfactor = 1 538 | if mo.eflags & MFE_UNDERWATER then 539 | waterfactor = 2 540 | end 541 | 542 | if not player.skidtime then 543 | P_InstaThrust(mo, mo.angle, FixedMul(FixedMul(player.actionspd + player.glidetime*1500, mo.scale)/waterfactor, abs(sin(mo.angle-angle)))) 544 | if P_TryMove(mo, mo.x+mo.momx, mo.y+mo.momy, true) then -- Check if this will send the player into a wall 545 | P_SetOrigin(mo, mo.x-mo.momx, mo.y-mo.momy, mo.z) -- Now put them back for reasons. 546 | elseif not player.lastglideattempt or abs(player.lastglideattempt.x-mo.x)+abs(player.lastglideattempt.y-mo.y) > FRACUNIT then -- Don't check if we've already checked, for optimization reasons 547 | player.lastglideattempt = { 548 | x = mo.x, 549 | y = mo.y 550 | } 551 | --print("Climb, damn you!") 552 | 553 | -- Move player as close as we can to the wall 554 | mo.momx = $1/32 555 | mo.momy = $1/32 556 | local moves = 31 557 | while P_TryMove(mo, mo.x+mo.momx, mo.y+mo.momy, true) and moves do moves = $1-1 end 558 | 559 | if moves then 560 | -- Look for climbable wall 561 | local line, dist, x, y = nil, 40< l.v1.x+20 and xtest > l.v2.x+20) 570 | or (ytest < l.v1.y-20 and ytest < l.v2.y-20) 571 | or (ytest > l.v1.y+20 and ytest > l.v2.y+20) then 572 | continue -- Closest point is outside of the line! 573 | end 574 | local dangle = R_PointToAngle2(mo.x, mo.y, xtest, ytest)-mo.angle 575 | if dangle > ANGLE_90 or dangle < ANGLE_270 then 576 | continue -- Closest point is not in front of us! 577 | end 578 | local newdist = P_AproxDistance(abs(mo.x-xtest), abs(mo.y-ytest)) 579 | if newdist > 12*FRACUNIT and newdist < dist then 580 | dist = newdist 581 | x = xtest 582 | y = ytest 583 | line = l 584 | end 585 | end 586 | --print(("#%s %su away at %s,%s (angle: %s)"):format(#line, dist/FRACUNIT, x/FRACUNIT, y/FRACUNIT, AngleFixed(abs(R_PointToAngle2(mo.x, mo.y, x, y)-mo.angle))/FRACUNIT)) 587 | if line and not (line.flags & ML_NOCLIMB) then 588 | S_StartSound(player.mo, sfx_s3k4a) 589 | P_ResetPlayer(player) 590 | player.lastlinehit = #line 591 | player.climbing = 5 592 | mo.momx, mo.momy, mo.momz = 0, 0, 0 593 | else 594 | player.climbing = 0 595 | mo.momx, mo.momy = 0, 0 596 | end 597 | end 598 | end 599 | else 600 | P_InstaThrust(mo, mo.angle, FixedMul(FixedMul(player.actionspd - player.glidetime*FRACUNIT, mo.scale)/waterfactor, abs(sin(mo.angle-angle)))) 601 | end 602 | elseif mo.state == S_PLAY_SPRING and springspin.value then 603 | if mo.isfacingleft then 604 | player.drawangle = angle-ANG20*leveltime 605 | else 606 | player.drawangle = angle+ANG20*leveltime 607 | end 608 | else 609 | mo.glidediff = 0 610 | if player.climbing then -- Actually, set isfacingleft based on the climbing angle! 611 | if mo.currentaxis.flipped then 612 | mo.isfacingleft = (angle-mo.angle) < 0 613 | else 614 | mo.isfacingleft = (angle-mo.angle) > 0 615 | end 616 | end 617 | 618 | -- Handle player angle direction when rotating around an axis + flipped 619 | if mo.isfacingleft then 620 | player.drawangle = angle-ANGLE_90 621 | mo.angle = angle-ANGLE_90 622 | else 623 | player.drawangle = angle+ANGLE_90 624 | mo.angle = angle+ANGLE_90 625 | end 626 | -- Flipped axis 627 | if mo.currentaxis.flipped then 628 | player.drawangle = $1+ANGLE_180 629 | mo.angle = $1+ANGLE_180 630 | end 631 | end 632 | 633 | -- Lactozilla: fix backwards thok, part 2 634 | mo.ax2d_angle = mo.angle 635 | 636 | -- Lactozilla: fix backwards spindash, part 2 637 | if (P_IsObjectOnGround(mo) 638 | and not (mo.ax2d_dashflags & PF_SPINDOWN) 639 | and (mo.ax2d_dashflags & PF_STARTDASH) 640 | and (mo.ax2d_dashflags & PF_SPINNING)) 641 | -- Correct spindash angle 642 | P_InstaThrust(mo, mo.ax2d_angle, FixedMul(mo.ax2d_dashspeed, player.mo.scale)) 643 | end 644 | 645 | mo.ax2d_dashflags = player.pflags & (PF_SPINDOWN|PF_STARTDASH|PF_SPINNING) 646 | mo.ax2d_dashspeed = player.dashspeed 647 | 648 | -- Rip out normal movement and do it ourselves! Muahaha! 649 | player.normalspeed = 0 650 | player.thrustfactor = 0 651 | player.accelstart = 0 652 | player.acceleration = 0 653 | player.runspeed = 2*skins[mo.skin].runspeed/3 654 | player.jumpfactor = 11*skins[mo.skin].jumpfactor/10 655 | if player.charability == CA_THOK then 656 | player.actionspd = 2*skins[mo.skin].actionspd/3 657 | end 658 | 659 | -- Normalize momentum to angle 660 | local newmag = R_PointToDist2(0, 0, mo.momx, mo.momy) 661 | local oldmag = R_PointToAngle2(0, 0, mo.momx, mo.momy)-angle 662 | if oldmag > 0 then 663 | oldmag = ANGLE_90 664 | else 665 | oldmag = -ANGLE_90 666 | end 667 | P_InstaThrust(mo, angle+oldmag, newmag) 668 | 669 | -- Referencing player movement code and kinda recreating it here 670 | if not player.climbing and not (player.pflags & PF_GLIDING) 671 | and not player.exiting and not (player.pflags & PF_STASIS) 672 | and not P_PlayerInPain(player) and player.mo.health then 673 | local m = skins[mo.skin] 674 | local topspeed = (2*m.normalspeed)/3 675 | local thrustfactor = m.thrustfactor 676 | local acceleration = m.accelstart + (FixedDiv(player.speed, mo.scale)/FRACUNIT) * m.acceleration 677 | if player.powers[pw_tailsfly] then 678 | topspeed = $1/2 679 | thrustfactor = $1*2 680 | elseif mo.eflags & (MFE_UNDERWATER|MFE_GOOWATER) then 681 | topspeed = $1/2 682 | acceleration = 2*$1/3 683 | end 684 | if player.powers[pw_super] or player.powers[pw_sneakers] then 685 | thrustfactor = $1*2 686 | acceleration = $1/2 687 | topspeed = $1*2 688 | end 689 | 690 | local movepushside = sidemove*thrustfactor*acceleration 691 | if not P_IsObjectOnGround(mo) then 692 | movepushside = $1/2 693 | if player.powers[pw_tailsfly] and player.speed > topspeed then 694 | player.speed = topspeed-1 695 | movepushside = $1/4 696 | end 697 | end 698 | if player.pflags & PF_SPINNING then 699 | if not (player.pflags & PF_STARTDASH) then 700 | movepushside = $1/48 701 | else 702 | movepushside = 0 703 | end 704 | end 705 | movepushside = FixedMul(movepushside, mo.scale) 706 | 707 | local oldmag = R_PointToDist2(0, 0, mo.momx, mo.momy) 708 | if mo.currentaxis.flipped then 709 | P_Thrust(mo, angle-ANGLE_90, movepushside) 710 | else 711 | P_Thrust(mo, angle+ANGLE_90, movepushside) 712 | end 713 | 714 | local newmag = R_PointToDist2(0, 0, mo.momx, mo.momy) 715 | if newmag > topspeed then 716 | if oldmag > topspeed then 717 | if newmag > oldmag then 718 | mo.momx = FixedMul(FixedDiv($1, newmag), oldmag) 719 | mo.momy = FixedMul(FixedDiv($1, newmag), oldmag) 720 | end 721 | else 722 | mo.momx = FixedMul(FixedDiv($1, newmag), topspeed) 723 | mo.momy = FixedMul(FixedDiv($1, newmag), topspeed) 724 | end 725 | end 726 | end 727 | 728 | end 729 | end) 730 | 731 | local function IterateMobjSectors(mo, func) 732 | 733 | local sec = mo.subsector.sector 734 | 735 | func(mo, sec) 736 | 737 | local tag = sec.tag 738 | local foftypes = {223} -- Referenced from the SRB2DB 2.1 config 739 | 740 | for i = 1, #foftypes do 741 | 742 | local foftype = foftypes[i] 743 | local linedefnum = P_FindSpecialLineFromTag(foftype, tag, -1) 744 | local last = -1 745 | 746 | while linedefnum ~= last do 747 | 748 | local fof = lines[linedefnum] 749 | 750 | if not fof then continue end 751 | 752 | fof = fof.frontsector 753 | if mo.z <= fof.ceilingheight and mo.z+mo.height >= fof.floorheight then 754 | func(mo, fof) 755 | end 756 | --last = linedefnum 757 | linedefnum = P_FindSpecialLineFromTag(foftype, tag, linedefnum) 758 | end 759 | end 760 | end 761 | 762 | 763 | addHook("MobjThinker", function(mo) 764 | 765 | -- Set lost rings to the player's axis on spawn 766 | -- (Do this in MobjThinker instead of MobjSpawn because object mom hasn't initialized by the 767 | -- time MobjSpawn hook is called!) 768 | if not mo.spawnchecked then 769 | mo.spawnchecked = true 770 | if mo.target and mo.target.currentaxis then 771 | mo.currentaxis = mo.target.currentaxis 772 | 773 | -- Set horizontal momentum based on player's angle 774 | P_InstaThrust(mo, mo.target.angle, mo.momx) 775 | end 776 | end 777 | 778 | -- Snap rings to axes 779 | if not (mo.fuse & 3) then -- Make it not run every frame for performance's sake 780 | axis2dudmf.SnapMobj(mo) 781 | end 782 | 783 | end, MT_FLINGRING) 784 | 785 | -- SeventhSentinel: Fail-safe to eject and reset players when they respawn 786 | -- TODO: I don't know what I'm doing, there is probably a better way to do this 787 | addHook("PlayerSpawn", axis2dudmf.EjectPlayer) --------------------------------------------------------------------------------