diff --git a/README.md b/README.md index 1b43a5e..c2347b7 100644 --- a/README.md +++ b/README.md @@ -1,26 +1 @@ -# NSModTemplate -A template repository for Northstar mods with a ~~mostly~~ pre-configured github action for publishing to Thunderstore - -## Usage -
    -
  1. Click the Use this template button on the top right of the repo's landing page (here)
  2. -
  3. Give the new repo a name and make sure it's set to public
  4. -
  5. In the settings tab, under actions -> general, set Actions permissions to Allow all actions and reusable workflows - -
  6. -
  7. Also in settings, under secrets -> actions, add your Thunderstore token as a secret named TS_KEY (Steps for getting a token can be found here) - - -
    -
  8. -
  9. Edit .github/workflows/publish.yml ~line 43 to add a description for your mod - -
    -
  10. - -
  11. Update this README and icon.png as they will be used by Thunderstore as well
  12. -
  13. Write your mod! (HINT: Find the docs here)
  14. -
  15. Before pushing large files (100mb or larger), run concat_assets.sh and commit the archives instead. Your archives will be automatically concatted and extracted when creating a github release so the mod is downloadable from thunderstore without any extra steps
  16. -
- - +#idk \ No newline at end of file diff --git a/mod.json b/mod.json index 8a9e103..5c90bb1 100644 --- a/mod.json +++ b/mod.json @@ -1,17 +1,25 @@ { - "Name": "Example.Mod", - "Description": "A cool mod that does cool stuff", - "Version": "1.0.0", - "DownloadLink": "https://northstar.thunderstore.io/package/download/Example/Mod/", - - "LoadPriority": 0, - "ConVars": [], - "Scripts": [{ - "Path": "example.nut", - "RunOn": "(CLIENT || SERVER) && MP" - "ClientCallback": { - "Before":"example_callback" - } - }], - "Localisation": [] -} + "Name": "Odd.Holosprays", + "Description": "A cool mod that does cool stuff", + "Version": "1.0.0", + "DownloadLink": "https://northstar.thunderstore.io/package/download/Example/Mod/", + "LoadPriority": 0, + "ConVars": [], + "Scripts": [ + { + "Path": "holosprays.nut", + "RunOn": "SERVER", + "ServerCallback": { + "Before": "InitHoloSpray" + } + }, + { + "Path": "playerPings.nut", + "RunOn": "SERVER", + "ServerCallback": { + "Before": "InitPlayerPings" + } + } + ], + "Localisation": [] +} \ No newline at end of file diff --git a/mod/scripts/vscripts/holoSprays.nut b/mod/scripts/vscripts/holoSprays.nut new file mode 100644 index 0000000..2e82b5c --- /dev/null +++ b/mod/scripts/vscripts/holoSprays.nut @@ -0,0 +1,175 @@ +global function InitHoloSpray +global function CreateSprite +global function TraceFromEnt +struct SprayInfo { + asset material + float scale + string color + vector offset // extra position offset from the base +} + +SprayInfo function _SprayInfo( asset material, float scale = 0.75, string color = "200 200 200", vector offset = <0,0,30> ) +{ + PrecacheSprite( material ) + SprayInfo s + s.material = material + s.scale = scale + s.color = color + s.offset = offset + return s +} + + +table< entity, array > holoSpraysOfPlayer +array sprayInfos + +void function InitHoloSpray() +{ + sprayInfos = [ + _SprayInfo( $"materials/ui/scoreboard_mcorp_logo.vmt", 0.5, "200 200 200", <0,0,60> ) + _SprayInfo( $"materials/ui/scoreboard_imc_logo.vmt", 0.5, "200 200 200", <0,0,60> ) + ] + AddCallback_OnClientConnected(OnPlayerConnected) + AddCallback_OnClientDisconnected(OnPlayerDisconnected) +} + +void function OnPlayerConnected( entity player ) +{ + AddButtonPressedPlayerInputCallback( player, IN_USE, OnUseHoloSpray ) + holoSpraysOfPlayer[player] <- [] + thread void function() : ( player ) { + wait RandomFloatRange( 0.0, 0.5 ) + NSSendInfoMessageToPlayer( player, "You can use HOLOSPRAYS on this server, simply press %%use%% to throw yours" ) + }() + +} + +void function OnPlayerDisconnected( entity player ) +{ + foreach( entity spray in holoSpraysOfPlayer[ player ] ) + { + if( !IsValid( spray ) ) + continue + spray.Destroy() + } + delete holoSpraysOfPlayer[ player ] +} + + +void function OnUseHoloSpray( entity player ) +{ + array sprays = holoSpraysOfPlayer[ player ] + if(sprays.len() >= 5) //spam is not cool + { + sprays[0].Destroy() // destroy the base + holoSpraysOfPlayer[player] = sprays.slice(1) // remove the reference + } + + const float force = 500.0 // initial force of the base pad + entity base = CreatePropPhysics( $"models/gameplay/health_pickup_small.mdl", player.EyePosition() - <0,0,20>, <0,0,0> ) + base.kv.solid = 0 + + base.SetOwner( player ) + + thread SpawnHoloSprite( base ) + + holoSpraysOfPlayer[player].append( base ) + base.SetVelocity( ( player.GetViewVector() ) * force ) +} + +void function SpawnHoloSprite( entity base ) +{ + base.EndSignal( "OnDestroy" ) + entity sprite + entity light + WaitFrame() + + while( IsValid( base ) ) + { + + TraceResults hit = OriginToFirst( base ) + if( Length( base.GetOrigin() - hit.endPos ) <= 10 ) //is object close to the floor + { + //make sure the object doesnt roll + base.SetVelocity( <0,0,0> ) + //adjust angles to surface, <-90,0,0> is needed because we went the medkit to lie down flat + base.SetAngles( < -90,0,0 > + AnglesOnSurface( hit.surfaceNormal, AnglesToForward( base.GetAngles() ) ) ) + + entity mover = CreateExpensiveScriptMover( base.GetOrigin(), base.GetAngles() ) + base.SetParent( mover ) + mover.NonPhysicsMoveTo( hit.endPos + <0,0,5.5>, 0.3, 0.0, 0.0 ) + + base.SetParent( hit.hitEnt ) + + //make sure the object doesnt rotate too much + base.StopPhysics() + + SprayInfo info = sprayInfos.getrandom() + sprite = CreateSprite( base.GetCenter() + info.offset, <0,0,0>, info.material, "200 200 200", info.scale ) + sprite.SetParent( base ) + light = CreateSprite( base.GetCenter() + <0,0,6.5>, <0,0,0>, $"sprites/glow_05.vmt", "200 200 200", 0.75 ) + light.SetParent( base ) + + break + } + WaitFrame() + } +} + +// Trace straight down from the provided origin until the trace hits the world, a mover or a Titan (Titans break idk) +TraceResults function OriginToFirst( entity base ) +{ + entity lastHit + TraceResults traceResult + array ignore = [] + vector origin1 = base.GetOrigin() + vector origin = origin1 + <1,1,1> + printt( "TEST " , origin) + printt( "TEST2 " , ) + printt( "BASE ", base) + printt( "OWNER ", base.GetOwner() ) + printt( "NAME ", base.GetOwner().GetPlayerName() ) + + do + { + vector endOrigin = + traceResult = TraceLine( origin, endOrigin, ignore, TRACE_MASK_NPCWORLDSTATIC, TRACE_COLLISION_GROUP_NONE ) + lastHit = traceResult.hitEnt + if(!IsValid(lastHit)) + continue + ignore.append( traceResult.hitEnt ) + } while( IsValid( lastHit ) && !lastHit.IsWorld() && !lastHit.IsTitan() && !(lastHit.GetClassName() == "worldspawn") && !(lastHit instanceof CNPC_Titan) && !(lastHit.GetClassName() == "script_mover") ) + return traceResult +} + +// Create a 2D sprite at position +entity function CreateSprite( vector origin, vector angles, asset sprite, string lightcolor = "255 0 0", float scale = 0.5, int rendermode = 9 ) +{ + // attach a light so we can see it + entity env_sprite = CreateEntity( "env_sprite" ) + env_sprite.SetScriptName( UniqueString( "molotov_sprite" ) ) + env_sprite.kv.rendermode = rendermode //these do NOT follow any pattern, trial an error is your friend, as you dont have any others anyway (they go from like 1 to 10 or sth, I hontely dont know) + env_sprite.kv.origin = origin + env_sprite.kv.angles = angles + env_sprite.kv.rendercolor = lightcolor + env_sprite.kv.renderamt = 255 + env_sprite.kv.framerate = "10.0" + env_sprite.SetValueForModelKey( sprite ) + env_sprite.kv.scale = string( scale ) + env_sprite.kv.spawnflags = 1 + env_sprite.kv.GlowProxySize = 16.0 + env_sprite.kv.HDRColorScale = 1.0 + DispatchSpawn( env_sprite ) + EntFireByHandle( env_sprite, "ShowSprite", "", 0, null, null ) + + return env_sprite +} + +// Trace to the point an entity looks at +TraceResults function TraceFromEnt( entity p ) +{ + TraceResults traceResults = TraceLineHighDetail( p.EyePosition(), + p.EyePosition() + p.GetViewVector() * 10000, + p, TRACE_MASK_SHOT, TRACE_COLLISION_GROUP_NONE ) + return traceResults +} diff --git a/mod/scripts/vscripts/playerPings.nut b/mod/scripts/vscripts/playerPings.nut new file mode 100644 index 0000000..cf043fb --- /dev/null +++ b/mod/scripts/vscripts/playerPings.nut @@ -0,0 +1,114 @@ +global function InitPlayerPings + +struct Ping { + entity sprite + float time +} + +table< entity, array > playerPings +const PING_DURATION = 15.0 + +void function InitPlayerPings() +{ + PrecacheSprite($"materials/ui/hud/attacker_offscreen.vmt") + PrecacheSprite($"materials/vgui/hud/weapons/target_ring_arc_tool_inner.vmt") + RegisterSignal( "PingDestroyed" ) + + AddCallback_OnClientConnected(OnPlayerConnected) + AddCallback_OnClientDisconnected(OnPlayerDisconnected) +} + +void function OnPlayerConnected( entity player ) +{ + AddButtonPressedPlayerInputCallback( player, IN_PING, OnUsePing ) + playerPings[ player ] <- [] +} + +void function OnPlayerDisconnected( entity player ) +{ + //avoid a crash by removing their pings + delete playerPings[player] +} + +string function GenerateRGBValueByIndex( int index ) +{ + //if somehow the player was not found in the array of his team it doesnt crash + if( index == -1 ) + return "100 100 100" + + int[3] RGBValue + + //rotates the columns around for each index + int changeRGBindex = index % 3 + + int potentialNewColour = RGBValue[ changeRGBindex ] + index * 50 + RGBValue[ changeRGBindex ] = potentialNewColour > 255 ? potentialNewColour % 255 : potentialNewColour + + //following column of the first one LOL + changeRGBindex = (index+1) % 3 + RGBValue[ changeRGBindex ] = 200 // fix value for RGB that constantly roates infront of the changine value + + return format( "%i %i %i", RGBValue[0], RGBValue[1], RGBValue[2] ) +} + + +void function OnUsePing( entity player ) +{ + thread SpawnPing( player ) +} + +bool function isEnemyPing( entity player, vector newPing ) +{ + foreach( Ping p in playerPings[ player ] ) + { + if( Time() - p.time <= 1.0 && LengthSqr( p.sprite.GetOrigin() - newPing ) <= 2000.0 ) + { + Signal( p, "PingDestroyed" ) + p.sprite.Destroy() + playerPings[ player ].fastremovebyvalue( p ) + return true + } + } + return false +} + +void function SpawnPing( entity player ) +{ + //all pings for a player so far + array pings = playerPings[player] + if(pings.len() >= 5) //spam is not cool + { + Signal( pings[0], "PingDestroyed" ) + if( IsValid( pings[0].sprite ) ) + pings[0].sprite.Destroy() + playerPings[player] = pings.slice(1) + } + + TraceResults trace = TraceFromEnt( player ) + bool enemyPing = isEnemyPing( player, trace.endPos ) + entity sprite = CreateSprite( trace.endPos, <0,0,0>, enemyPing ? $"materials/ui/hud/attacker_offscreen.vmt" : $"materials/vgui/hud/weapons/target_ring_arc_tool_inner.vmt", enemyPing ? "255 0 0" : GenerateRGBValueByIndex( GetPlayerArrayOfTeam( player.GetTeam( ) ).find(player) ), enemyPing ? 0.6 :0.3 , 5 ) // 5 + SetTeam( sprite, player.GetTeam() ) + //set it so only you can your team can see them + sprite.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY | ENTITY_VISIBLE_TO_OWNER + // + sprite.SetOwner( player ) + //makes the ping attach to the object that moves + sprite.SetParent( trace.hitEnt ) + //track the time a ping was send + Ping p + p.sprite = sprite + p.time = Time() + playerPings[player].append( p ) + + //ping gets detroyed after a set time + thread DestroyPingDelayed( p, PING_DURATION ) +} + +void function DestroyPingDelayed( Ping p, float duration ) +{ + EndSignal( p, "PingDestroyed" ) + wait duration + if( IsValid( p.sprite ) ) + p.sprite.Destroy() + playerPings[ p.sprite.GetOwner() ].slice( 1 ) +} \ No newline at end of file