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
-
-- Click the
Use this template
button on the top right of the repo's landing page (here)
-- Give the new repo a name and make sure it's set to
public
--
In the settings
tab, under actions
-> general
, set Actions permissions
to Allow all actions and reusable workflows
-
-
--
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)
-
-
-
-
- -
Edit .github/workflows/publish.yml
~line 43 to add a description for your mod
-
-
-
-
-- Update this README and
icon.png
as they will be used by Thunderstore as well
-- Write your mod! (HINT: Find the docs here)
-- 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
-
-
-
+#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