#Starlancer AIFix v3.13.2 | EnemyEscape v3.0.0
1 messages ยท Page 5 of 1
which is just a .clear at the start of the round?
idk, up to you guys 
I would personally avoid that kind of thing if I possibly can, because it's a lot easier to mess it up than to make something fit into the component model Unity uses
the only other reason to do that is if you actually are doing this kind of thing in performance-critical code, but this should only run a few times when the AI is near the doors
basically it would just be a component where you can check the distance to the entrance yourself on a similar interval to the one the AI uses, then teleport the AI when you want to
no patches needed hopefully
except for instantiating it at Start()
I may have completely misunderstood the direction you were guiding me in initially, but here's what I've got so far
So I could specify to check it every second or whatever in that case?
that's about right, but that function doesn't need to exist, you can just do (Time.time - [...].timeAtTeleport) < x to check it, and [...].timeAtTeleport = Time.time; to set it
also you have the operands backwards for getting the delta
getcomponent in update is cursed
wait maybe I misread
DoAIInterval, not update
oopsie
how often is that
this is why I would suggest making this code run in the separate component instead of a patch
For the general EnemyAI component it's whenever it picks a new node to move to
GetComponent<>() and Dictionary.TryGetValue() are probably not that different in performance if there are so few components as the AI tend to have
if you really want to do a dictionary, though, there's this: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.conditionalweaktable-2?view=net-8.0
I don't know if it cleans up the table so that the buckets aren't occupied
they're both functions
does more ig
they both do essentially the same thing
[] just throws an error if there is no item instead of returning false
Dictionaries are another thing I haven't dabbled in lol
but if I move the patch inside the component, then the timer can just be set and read completely internally?
yeah
accessing the timestamp from inside and outside is the same when it's public or internal though
yee, but no GetComponent needing to be ran
so the main thing is that you don't have to associate your timer to the EnemyAI manually
exactly
you can cache the EnemyAI in your component and act on it without any extra cost
Does this method have to be a coroutine in order to implement a Wait?
Or is that the wrong way to go about this, I want to call on it every 3 seconds
same way you check whether you can teleport is how you can determine whether to run your interval function
good ol Time.time
you could make it a coroutine, but that has to go through an event loop and stuff, idk if it's better or worse for this type of thing
that would be pretty difficult to profile since both events are handled in the runtime
In order to properly reference the enemy with the component would it still just be passing EnemyAI __instance to the method?
to which method?
For example I'm calling it TeleportMethod()
moving everything into the StarlancerTimer component (which I'll rename)
you shouldn't be calling any methods from outside your component, just use the void Update() Unity event to do everything
ahh I see, my question still stands of should I be passing EnemyAI __instance to that? Or how do I access the game object that the component belongs to?
public class TheComponent : MonoBehaviour {
private const float UpdateInterval = 0.2f;
private EnemyAI enemy;
private float lastTeleportCheck = float.NegativeInfinity;
private float lastTeleportTime = float.NegativeInfinity;
private void Awake() {
enemy = GetComponent<EnemyAI>();
}
private void Update() {
if ((Time.time - lastTeleportCheck) < UpdateInterval)
return;
lastTeleportCheck = Time.time;
[do magic]
}
}
it's so simple when I see it laid out, and my brain is like "I already knew how to do that why couldn't I think of ittttt"
yeah so basically you just hook into EnemyAI.Awake() and add this component, then it does everything else
you could also make subclasses and override some virtual methods if you want to make specific conditions for different enemy types (like current behavior state etc)
I'll be borrowing those variable names btw ty
I will say though, subtracting negative infinity might result in NaN, so I'm not sure if that condition will actually trigger at first (I always forget the comparison rules for NaN)
np lol
Well I certainly wouldn't know lol
Should I do all the setup in Awake rather than Start?
it doesn't really matter here
either will work, they'll essentially run at the same point for your purposes
Awake() gets called immediately inside AddComponent() iirc, but either one will run before Update()
okie dokie
In comparison operations, positive infinity is larger than all values except itself and NaN, and negative infinity is smaller than all values except itself and NaN. NaN is unordered: it is not equal to, greater than, or less than anything, including itself. x == x is false if the value of x is NaN. You can use this to test whether a value is NaN or not, but the recommended way to test for NaN is with the isnan function (see Floating-Point Number Classification Functions). In addition, <, >, <=, and >= will raise an exception when applied to NaNs.
So I think it'll be okay?
honestly I think I was overthinking it by thinking that it was necessary to use infinity
I'm pretty sure Time.time starts at 0, so by the time you're in-game, it'll be quite a bit larger than 0
those can just be assigned 0
The time at the beginning of the current frame in seconds since the start of the application (Read Only).
Yeaaaa lol
That's the default float value right? So no assignment necessary
yeah sounds good to me
it is in C#, yeah
I have ptsd from the undefined behavior of uninitialized variables in C++
lmao
strings in scriptableobjects created at runtime being null and not empty be like
lastTeleportTime never gets used in this, is it actually necessary? Because all I care about is when the last check was, right?
oh wait, 1.0f - float.NegativeInfinity is float.PositiveInfinity, no NaN! that's cool
that's a C# thing, strings are reference types
anything that's a class is nullable by default and default(class X) is always null
not exactly sure why strings are reference types, but they probably had a good reason for that (I hope)
lastTeleportTime is for your [magic] where you check if the enemy is close enough to use the teleport first
you said you wanted a cooldown on the actual teleport, right? that's what that's for
oh right
the lastTeleportCheck is just to make it not run this code every single frame
riiiight
It might be bad syntax, but I'm curious, does this set both variables to Time.time ?
lastTeleportTime = lastTeleportCheck = Time.time;
yeah
it's not a magic special syntax, though
the expression x = (expression y) evaluates to the evaluated value of (expression y)
when you chain those, you're really saying
x = (y = (z = (w = 1)));
so by extension, you can do
Thing x;
if ((x = GetThing()) != null) {}
How are you getting it to show up like actual code in discord?
hrm
you have to put a newline after the triple apostrophe and its language
@gleaming robin Working on a big update for AI Fix are you?
I saw you ping me earlier
๐
bool thing;
if (thing) {}
private void Update()
{
if (((Time.time - lastTeleportCheck) > UpdateInterval) && (Time.time - lastTeleportTime) > CooldownTime)
{
if (enemy.isOutside)
{
for (int i = 0; i < outsideTeleports.Length; i++)
{
if ((outsideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 1 && UnityEngine.Random.Range(0, 100) < 100) //Replace with variable chance to enter facility
{
enemy.transform.position = insideTeleports[i].entrancePoint.transform.position;
enemy.SetEnemyOutside(false);
int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
lastTeleportTime = Time.time;
lastTeleportCheck = Time.time;
logger.LogInfo($"{enemy.gameObject.name} teleported inside; Switching to interior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
}
}
}
else
{
for (int i = 0; i < insideTeleports.Length; i++)
{
if ((insideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 1 && UnityEngine.Random.Range(0, 100) < 100)
{
enemy.transform.position = outsideTeleports[i].entrancePoint.transform.position;
enemy.SetEnemyOutside(true);
int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
lastTeleportTime = Time.time;
lastTeleportCheck = Time.time;
logger.LogInfo($"{enemy.gameObject.name} teleported outside; Switching to exterior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
}
}
}
}
}```
yup lol
separate mod actually :3c
I'm surprised it let you send, it normally interprets Enter as a newline until you close it
it didn't, I was clicking send
Oh? ;o
I don't have a send button lmao
My own take on LethalEscape
hopefully the only one if LethalEscape isn't maintained and you can come to feature parity where it matters, honestly
the fact that SetEnemyOutside() isn't called in LethalEscape will inevitably become a problem for compatibility for me and possibly other mods
Oooh good
Cus LethalEscape is kind of buggy
c;
We need the Starlancer version Starlancer is the goat
I am nothing without the community that helped teach me lol
How does that look btw @faint copper ?
your Time fields are both doing the same thing
you need to set lastTeleportCheck immediately upon discovering that (Time.time - lastTeleportCheck) > UpdateInterval
and also set it even if !((Time.time - lastTeleportTime) > CooldownTime)
as it is currently, they're both just acting as a teleport cooldown
yep yep
and all of the code will otherwise run every single frame
also use the early return so you don't have to indent so far!
(like #1206494982521753620 message)
skipping code via control flow statements like return, continue, break will save you a lot of tabs
yee, though I don't manually indent lol, VS auto-indents for ifs, fors, etc.
oh sure, but it still pushes your code closer and closer to that scroll bar
yee
Slightly confused here, but this would take care of that, right?
yeppers, that looks about right to me
the cooldown check can also be an early return
yee, also I didn't actually do a calculation there lol
two tabs down, one to go
oh yeah
I missed that lol
I'm not in the code review mentality
I can make it sneaky by putting return on the same line as the checks as well lol
private void Update()
{
if ((Time.time - lastTeleportCheck) < UpdateInterval) {return;}
lastTeleportCheck = Time.time;
if ((Time.time - lastTeleportTime) < CooldownTime) {return;}
if (enemy.isOutside)
{
for (int i = 0; i < outsideTeleports.Length; i++)
{
if ((outsideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 1 && UnityEngine.Random.Range(0, 100) < 100) //Replace with variable
{
enemy.transform.position = insideTeleports[i].entrancePoint.transform.position;
enemy.SetEnemyOutside(false);
int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
lastTeleportTime = Time.time;
lastTeleportCheck = Time.time;
logger.LogInfo($"{enemy.gameObject.name} teleported inside; Switching to interior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
}
}
}
else
{
for (int i = 0; i < insideTeleports.Length; i++)
{
if ((insideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 1 && UnityEngine.Random.Range(0, 100) < 100)
{
enemy.transform.position = outsideTeleports[i].entrancePoint.transform.position;
enemy.SetEnemyOutside(true);
int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
lastTeleportTime = Time.time;
lastTeleportCheck = Time.time;
logger.LogInfo($"{enemy.gameObject.name} teleported outside; Switching to exterior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
}
}
}
} ```
i might be misunderstanding the code but you could put returns here to avoid iterating through teleports when you've already found one you like right?
you're 100% correct lol
it wouldn't really matter since there's usually <4 teleports, but you're right
@rocky fable you wanna test it while I go do chores?
Do you feel it's ready?
Good enough lol
lemme just test that the game will load rq actually
test on hold
@rocky fable still some bugs, but I also have an easy method of testing, so I'll let you relax :P
The blob is the one of the best candidates for the test lol
I will say I don't think the Blob can actually die in V50, I did test hitting it 3 times and nothing happened
@gleaming robin would you want the cooldown to start even if the random chance fails? it the enemy stays near the exit, your current code reroll regularly and eventually exit, not sure if you intend that
also, while I'm here nitpicking, it's not necessary to set lastTeleportCheck in the loops
I'll find out for certain
Good point!
I'm gonna move the random variable up so the rest of the function depends on it being true instead of rolling it in the for loop
Use some more of that early returning
Okay, so the method is being called, but the enemy's position isn't changing
oh wait I might know what the issue is
yuuup okay time for another test
nope, not the issue
Something is forcing them to stay at the facility height for some reason
What're you testing?
Enemy escaping
The method is being called, and the various AI settings I want to change are taking effect, but the transform fails to take
Is their local position not changing?
Correct
But the exact same code works for like, masked?
No I'm using different code
I'm adding a component to enemies at Start() that runs Update() every x seconds
My logs show that it gets to the code for being teleported, but the enemy doesn't actually go outside, they stay at the dungeon root
Well from what I know the navmesh gets pretty weird when you try changing the position of the enemy, usually doesn't let you know and whatnot
Can I see the code?
private void Update()
{
if ((Time.time - lastTeleportCheck) < UpdateInterval) {return;}
lastTeleportCheck = Time.time;
if ((Time.time - lastTeleportTime) < CooldownTime) {return;}
if (UnityEngine.Random.Range(0, 100) > 99.99) { return; }
if (enemy.isOutside)
{
for (int i = 0; i < outsideTeleports.Length; i++)
{
if ((outsideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 5) //Replace with 1
{
enemy.gameObject.transform.position = insideTeleports[i].entrancePoint.transform.position;
enemy.SetEnemyOutside(false);
int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
lastTeleportTime = Time.time;
logger.LogInfo($"{enemy.gameObject.name} teleported inside; Switching to interior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
return;
}
}
}
else
{
logger.LogInfo($"Enemy is inside, checking teleport positions.");
for (int i = 0; i < insideTeleports.Length; i++)
{
logger.LogInfo($"Check #{i}.");
if ((insideTeleports[i].transform.position - enemy.transform.position).sqrMagnitude < 5)
{
enemy.gameObject.transform.position = outsideTeleports[0].entrancePoint.transform.position;
enemy.SetEnemyOutside(true);
int nodeIndex = UnityEngine.Random.Range(0, enemy.allAINodes.Length - 1);
enemy.favoriteSpot = enemy.allAINodes[nodeIndex].transform;
lastTeleportTime = Time.time;
logger.LogInfo($"{enemy.gameObject.name} teleported outside; Switching to exterior AI. Setting Favorite Spot to {enemy.favoriteSpot}.");
return;
}
}
}
}```
Maybe I need to set the transform after altering the AI properties
Could try that
Nope, even if I try to use UnityExplorer I can't alter their position
have you tried instead doing it through the rigidbody?
I have a dumb idea, I'm gonna try disabling them, moving them, and then reenabling them
enemy.gameObject.GetComponent<Rigidbody>().MovePosition(insideTeleports[i].entrancePoint.transform.position); something like that
how so?
oh, I'll try that
i could be getting my variables a lil wrong since i havent looked at it explicitly, but something like that
yeah thats what i was afraid of, finding the rigidbody lol
if (UnityEngine.Random.Range(0, 100) > 99.99) { return; } also wut
well its on the component, shouldn't that automatically sync it?
i dont believe so? you'll probably need to make your own random, i just generally dont trust having a system.random or unityengine.random that i didnt make from the start lol
still no luck on the moveposition
I'll keep at it and report my findings
https://clips.twitch.tv/ColdCrispyBeanHoneyBadger-Z6WESRcnwxC6Rc7e @gleaming robin Tmw I was gonna kill it with the knife but the Spike Trap had plans ๐
clearly you make bad mods
Lol well they've been toned down a lot and they're nowhere near as bad as they were in the beta so I don't feel as annoyed by them anymore
Oh no
Starlancer's mod was an essential before Zeekerss fixed their damn spawns
i like my theory more
Nah
I love Starlancer's mods too much for that to ever be the case
Man makes goated quality mods
Although the bugs with Warehouse were funny
I can't wait for LLL to hopefully be stable in the next few days or so depending on how experimental testing goes, don't take this as a given though
people read this and assume its like a thing btw
"goated quality" being 3 lines of code ๐คญ
I said hopefully
Depending on how the experimental testing keeps going
LOL
I very well know it could be longer
ye but the people reading it do not
WHY WON'T THE GAME LET ME MOVE THE ENEMYYY
try disabling the agent or just doing whtaever :p
you tried to stray me from the path
oh I just straight up disabled the gameobject lol
smh
๐ค i guess that probably works
i just dunno how that would affect the AI
it tried to kill me, so seemed fine
coilhead is like the last one i'd test for its AI changing ๐ญ
more worried about custom enemies tho lol
well I still need to iterate on this a bit anyways, but I'll try out Jester next :P
Also tried a blob, but it didn't take too well to the transform. I think I might know why, but I'll keep working on it tomorrow. Still, at least I have a proof-of-concept
I'm gonna patch your giant and force it to go on a rampage through the interior
you'd have to change the navmesh but it's prolly possible
You're 100% right about it being a navmesh issue, but Unity did miraculously consider this situation, so now I know what to use instead
lol nice
and ye, i know its navmesh cuz i've tried to mess with my enemy's position before
Ty for your help Xu
its weird, if u repeatedly change the position of an enemy in like Update() the position will change
or atleast the "body"
but not if you only do it once
Good ol engine quirks
it might help if you find the closest navmesh node
i.e. RoundManager.Instance.GetNavMeshPosition(targetPos, RoundManager.Instance.navHit, 2.7f) is used by vanilla to find the closest valid position on the navmesh within 2.7 meters
if it fails, it'll return targetPos, and if that happens setting the position will likely fail
but if Warp() works then you're probably good anyway
I'll keep it in mind!
@golden basin @faint copper
gonna try implementing the GetNavMeshPosition so the blob won't be in the floor to start
Have blobs and coilheads always spammed the log with "Setting Destination[...]" messages?
Don't think so?
oh, that
Also from what I can tell this blob weirdness is due to their fluid simulation or something preventing the whole mesh from warping, so the mesh is stuck in the ground until it arbitrarily frees itself
Ugghh I don't wanna make a config for this ๐ญ
Yea it has 9 rigidbodies ; w ;
Well for the rest of the enemies it seems like a success! This is without StarlancerAIFix present. I'm gonna end up making AIFix a hard dependency though, since certain enemy behavior can still mess up when outside, like the Springman slide and Jester not properly attacking
you're a saint
Initial release will probably just not support blobs until I can figure out a good way to shift it in its entirety
(i doubt there's enough people on the whole earth who would want to let blobs out haha)
btw can they also go inside?
lemme just make sure, but it's the same code inverted so should be able to
๐
the genius strikes again ๐ฅ
actually huge
is that one day's work?
ye
you're fucking goated
technically 6-ish hours work I guess?
idr when I started last night
but also credit to Zaggy, Batby and Xu for their advice
weird that the blob doesn't go all together, I thought that those colliders were all children so they would move with it
I'll screenshot its weirdness
okay well it seems to be inconsistent actually
but here, managed to catch it stuck in its weirdness. Eventually it's like that point got unstuck and it just schlorped into place
but that's also proof that it went back inside
oh, this is it entering?
yes, but it usually does the same thing on exit
oh god
oh god
does anyone remember that one mod that would make the transition between inside and outside seamless?
what I would do is collect all those rigid bodies when teleporting blobs and just set all their positions to the navmesh point
yeye that one
the blob will start as one tiny blob and then expand
I'll give that a try
you can make a virtual function on your component that does the teleporting and then implement a subclass that overrides it for blobs only to use
Even better, record their offsets before teleport and put them down with those same offsets to get a proper transfer
not really a good idea if they have trouble with being inside the collision
then they may spawn partially in the wall and get stretched out as they move
One suggestion, I feel like certain enemies should try to return inside once they lose agro
they'll expand very quickly if they are all in one point and it'll look more like they entered through the door I think
just gonna test in->out rq since that's necessary for the AI conditions
hm, not the solution
I'mma try hooking into the BlobAI component
if (enemy.enemyType.name == "Blob")
{
foreach (Rigidbody bone in enemy.gameObject.GetComponent<BlobAI>().SlimeBones)
bone.transform.position = outsideTeleports[i].entrancePoint.transform.position;
}
so this seems to work a little better? I think what it might be doing is still attempting to move to my position inside when it teleports, bc it seems like sitting inside allows it to reorient faster
so lemme try resetting its AI on teleport
๐
3 for 3 on the new code
let's gooooooooo all I had to do was
enemy.DoAIInterval(); and it works perfectly, no blob-specific code needed
@faint copper
oh?
wait, so it was only messed up for less than 200ms after a teleport?
I'm not sure why DoAIInterval fixes the bones 
no clue, but it does lol
also I would tend to say you should just do
if (enemy is BlobAI blob) {
foreach (var bone in blob.SlimeBones)
bone.transform.position = teleportPosition;
}
also, remember to cache your reused transform positions!
that's the blob DAII
oh I see
not using that anymore lol
I just call DoAIInterval instead
strange...
definitely doesn't hurt to immediately update the AI target though
I'm also gonna set it to use the transform of the telepoint instead of the entrance teleport itself, as I feel like that could be buggy with the way SpawnSyncObjects works
I'm so hype this is working lmao
it's such a beautifully simple way of doing it
yeah, it really doesn't take much, it's very nice
might be nice to make that RNG be deterministic though btw
[HarmonyPatch(typeof(EnemyAI), "Start")]
[HarmonyPostfix]
private static void EscapeSetup(EnemyAI __instance)
{
if (__instance.gameObject.GetComponent<StarlancerEscapeComponent>() == null)
{
StarlancerEscapeComponent EscapeComponent = __instance.gameObject.AddComponent<StarlancerEscapeComponent>();
logger.LogInfo($"Adding EscapeComponent to {__instance.gameObject.name}. It may now roam freely.");
UnityEngine.Random.InitState(StartOfRound.Instance.randomMapSeed);
}
}```
you could store a Random instance inspired by the vanilla EnemyAI search coroutine's
wait
don't be doing that

no?
that's global to everything
it won't just affect your class, it'll affect anyone else that uses it
I didn't even know that was a thing, I hate it
lmao
making RNG static is kinda gross
So if I wanted to implement that vanilla code for this, I'd make a new system random on component awake and then use that for my rng stuff?
exactly
I'm not sure exactly what making it deterministic in this case entails
and base it on either directly on the seed, or based on the location of the enemy
it means that the map seed when landing exactly dictates the behavior
base it on either directly on the seed
that's what I was doing before yes? The only issue with it is that it would apply globally?
so if all player input were the same on the same seed, the enemies would do the exact same thing again
indeed
kk
also it turns out that Zeekers uses the static RNG functions, so by setting the global seed you were changing those things
I don't really like that vanilla makes use of that but since it does, it's best not to touch it
So once I have the variable of the Random, how do I use it exactly? Intellisense isn't being super helpful here
like say I wanna pick a value from 0 to 100
with UnityEngine.Random it's just Range(0, 100)
oh is that what Next is for?
okay nvm that's the ticket ^^;
kk
Making enemies escape: Simple
Making the escape chance configurable per enemy: Hell
I released a small fix removing the possibility of bumping off steep angles at least for now, and fixing the masked enemy. Also brought Adamance scrap spawn amounts to be still slightly better than March but not on par with the ice moons.
new LC update
Nice
@faint copper if I put this in the Awake(), would this actually work for disabling the component and thus removing the Update() calls?
if (chanceToEscape <= 0) { enemy.gameObject.GetComponent<StarlancerEscapeComponent>().enabled = false; }```
Or should I destroy it instead?
it almost didn't occur to me that I don't want to roll the random chance if it's not even close enough to a teleport
yeah that's fine
if you're not using it, though, you may as well remove it or not add it in the first place
Mmk. I'll see if I can reconfigure to not add it, I was just not finding immediate results with that so I pivoted :P
don't forget you can check if it's working with UnityExplorer
ye
if (EnemyEscapeConfigDictionary[__instance.enemyType.name].Value > 0 && __instance.gameObject.GetComponent<StarlancerEscapeComponent>() == null)```
There we go, that should prevent it from adding the component
@faint copper My understanding was that assigning a value to a dictionary entry would make a key if it didn't already exist, but I'm getting this error. I have a preset list of vanilla enemies and I'm trying to use this code to loop through and generate the config entries
static readonly string insideEnemyList = "Blob, Bunker Spider, Butler, Butler Bees, Centipede, Crawler, Flowerman, Hoarding bug, Jester, Nutcracker, Puffer, Spring";
static readonly string outsideEnemyList = "Baboon hawk, Earth Leviathan, ForestGiant, Manticoil, MouthDog, RadMech, Red Locust Bees";
static readonly string[] vanillaEnemies = (insideEnemyList + "," + outsideEnemyList).Split(',');
internal static Dictionary<string, ConfigEntry<float>> EnemyEscapeConfigDictionary = new Dictionary<string, ConfigEntry<float>>();
//In Awake();
foreach (string enemy in vanillaEnemies)
{
float probability = configEscapeChanceDefault.Value;
if (EnemyEscapeConfigDictionary[enemy] == null)
{
EnemyEscapeConfigDictionary[enemy] = EnemyEscapeConfig.Bind("StarlancerEnemyEscape", $"{enemy}EscapeChance", probability, new ConfigDescription($"Chance for {enemy} to go into or out of the facility.", new AcceptableValueRange<float>(0, 100)));
}
}```
Do you see any issues??
the error would be happening when you EnemyEscapeConfigDictionary[enemy] == null, you need to use a different method to check if the entry exists
ah okay
the indexing operator doesn't return default when no entry is found, it throws an error
if (!EnemyEscapeConfigDictionary.ContainsKey(enemy))
yep
noice
and when you need to use the value immediately, you can do
if (!dict.TryGetValue(key, out var thing))
dict[key] = (thing = [your default value here]);
So that code creates a key and assigns a default value to the entry if it fails to get the key?
yeah
Sort of shorthanding my current code?
if the value exists, then TryGetValue will assign it to thing, if not, you will create the key, assign it to the key and thing at the same time
ohh
well, you don't immediately need to use the value you're assigning, so no
Contains is fine if you don't need tou se your bound config entry
one thing I'll say, I'm pretty sure there's a point at which the game knows about every enemy in the game, or at least every enemy that modders are allowing to spawn through the vanilla spawn system
if you use that list instead to bind your configs, most modded enemies will show up
you'll have to do some digging in ILSpy to find that, but if you search for references to EnemyAI in RoundManager and StartOfRound you'll probably see it in there
most likely RoundManager
@tulip vault might know exactly where to look and when to hook into that
wassup
Is there a way to grab the name of every loaded enemy?
Usecase?
SelectableLevel.Enemies - Inside
SelectableLevel.OutsideEnemies & SelectableLevel.DaytimeEnemies - Outside
Dunno how youโd do butler bees
oh damn, so there's no list of all the enemy types? for some reason I thought there was
in that case, perhaps it would be good to populate from the SelectableLevel and then lazily bind the rest as enemies spawn?
I think it's fine to just leave the option available to manually input extra enemies
That way I'm not messing with other peoples custom enemies too
but if the default is 0 then you won't be, no?
I also have no idea how the Butler Bees function, I just figured I'd throw them in lol
That's fair
I'm guessing it's pretty likely that you'll get questions about what the internal names for custom enemies are if you don't make it automatic
the main difference is that they don't spawn naturally
same thing with the circuit bees
could just check the highest point of the dungeon and check if the enemy is above or below that when it spawns?
or actually, I guess it's the nest that doesn't spawn naturally, nvm
as far as whether the enemy spawns inside or outside, I don't think it matters for his use case
Correct, this is all for EnemyEscape, not AIFix
yeah i know, i meant more for the inside enemy list and outside enemy list
ohhh config
uh
iterate through StartOfRound.levels on ig like RoundManager.Start() ye
ohh ok I see
I assume that it should be lowest priority so that any modded enemies get registered first?
and binding config options late into the game won't cause issues, I assume
I only ever bind them at Plugin.Awake() so I'm not sure, but it would be good to bind any config entries that are missing when such enemies spawn in so that they can be configured as well
im suggesting roundmanager.start so it's the opposite
do it too early and you'll miss them
i wish i knew
docs suck
in the longterm you'd have to reference LLL for this to work fully though
true, I just looked here and it doesn't even say lmao
yup
upcoming LLL lets enemies dynamically inject into enemy pools depending on dynamic contexts
some might not be in the levels when audio knight checks for them
because eg. it's not stormy weather rn
right
that's why I think it would be good to lazily bind in both a low-priority RoundManager.Start() postfix and on EnemyAI.Awake()
shouldn't require specific LLL dependency if that's how it's done
i dont think you understand
you can't check for them in roundmanager.start if their not in the pools yet
I could still have a conditional on EnemyAI.Awake() for binding a config if none exists, it's just... messier than being able to pre-emptively cycle through a list on game start and generate the configs right away
I want as few dependencies as possible, ideally just BepInEx and my AIFix
if they're not there in RoundManager.Start() then it's taken care of in EnemyAI.Awake()
it's not really messy, just make a function to ensure that a config entry exists for an EnemyAI and call it on every enemy in every SelectableLevel on RoundManager.Start() and then on every enemy spawned via EnemyAI.Awake()
one single function call in EnemyAI.Awake() and you have your code nicely split up in your config manager
would you want onnetworkspawn instead or is that not relevant here
Start() should be called on all clients if that's what you mean
Sorry, by messy I meant moreso that I'd prefer the immediate generation versus a delayed one that might cause players to do like you said and run asking for internal names lol (even though custom internal names aren't reeeeeally my problem)
But I can just put in the readme (that no one will read) that any modded enemies will populate the configuration upon first spawn
at least you have the option to say "load into a level and see" instead of having to direct them somewhere else
the only other way I can think of is to use this function: https://docs.unity3d.com/ScriptReference/Resources.FindObjectsOfTypeAll.html
that should find prefabs, so if you run that after all assets are loaded, it should find every EnemyAI instance in the game, unless someone loads an asset after the game starts
gross
https://clips.twitch.tv/RefinedRockyManateeNononoCat-WAQ1IWc0G-9a5lN4 @gleaming robin I love how I barely live to it, then I back up against that wall and it goes "Okay bye" and Nyoooms into the damn void
you'd have to ensure thats running after lethallevelloader stuff
Is till dunno how that happened
otherwise your gonna get the duplicates from levels
enemies would be deduplicated by name (or type?) anyway
as far as knowing when to run that scan, yeah, it is a little ugly
still gonna be in lobby
you mean you do it asynchronously and join any tasks upon opening a lobby?
no i mean to reliably find those objects you'd need to be in a lobby
hmm? why?
it would include loaded assets
nothing has to be known by the game systems
on main menu they should be loaded in memory because they happen to exist in the networkmanager prefab list but idk feels little sketchy
the assets would be loaded in regardless of whether they're referenced, wouldn't they?
no
or does Unity lazily load assets from within a package?
ye
oh hmm
thats why a lot of stuff can only be accessed when you enter a lobby
the only real exceptions is the stuff that happens to be referenced in stuff in the networkmanager
You went off the navmesh I guess? Not sure, but pretty funny lol
it seems like the best way to ensure that at least most enemies are found as early as possible
fresh load with Scopophobia
(it has to finish loading the main menu, though)
So if I did wanna do that, when would I call it?
I don't know too much about the menu preroll animation but maybe Batby would know?
Sorry chef, not trying to pull you away from cooking too much lol
not low priority GameNetworkManager.Start()?
otherwise gamenetworkmanager.start postfix as late as possible
ah yeah
I would tend towards low priority Start() since then it doesn't have to run the code every frame
no need to add any extra overhead for this
when you talk about priority, is there some property to delay code and allow other stuff to go before it in a method?
Or are you just referring to Start() as low priority
[HarmonyPriority(Priority.[...])]
Ahh okay
it defines where it falls in the order of the hooks that Harmony generates for you
I'm pretty sure that lower priorities run later
ah yeah it does
found my old code in OpenBodyCams
I really want Scopophobia to get updated
๐ฆ
So priority 0 then? :P
sure yeah lol
[HarmonyPatch(typeof(GameNetworkManager), "Start")]
[HarmonyPostfix]
[HarmonyPriority(0)]
private static void BindRegisteredEnemies()
{
EnemyAI[] enemyAIs = Resources.FindObjectsOfTypeAll<EnemyAI>();
foreach (EnemyAI enemy in enemyAIs)
if (!EnemyEscapeConfigDictionary.ContainsKey(enemy.enemyType.name))
{
EnemyEscapeConfigDictionary[enemy.enemyType.name] = EnemyEscapeConfig.Bind("StarlancerEnemyEscape", $"{enemy.enemyType.name}EscapeChance", configEscapeChanceDefault.Value, new ConfigDescription($"Chance for {enemy.enemyType.name} to go into or out of the facility.", new AcceptableValueRange<float>(0, 100)));
}
}```
I would tend to say you should use Type as a key instead of the name
in case anyone creates enemies with the same name
not a huge deal though
I'd also separate the meat of that loop into a separate function and call it from EnemyAI.Awake() as well
that way if the enemies somehow aren't loaded immediately you'll still get them in the config eventually
both fair points, but then I have to go and rename the preset lists lmao
preset lists?
static readonly string outsideEnemyList = "Baboon hawk,Earth Leviathan,ForestGiant,Manticoil,MouthDog,RadMech,Red Locust Bees";```
not actually a big deal
just an annoyance :P
So players can pick from "all, list, inside, outside" as presets
and bc I was following a piece of someone else's code and didn't think about it :P
yea
and arguably even more so if you do Types
only problem is you'd have to differentiate your config entries for duplicates
private static Dictionary<string, int> DuplicateEnemyNames = [];
public static float RegisterEnemy(EnemyAI enemy) {
var enemyType = enemy.GetType();
if (!EscapeChances.TryGetValue(enemyType, out var escapeChance)) {
var configNameBuilder = new StringBuilder(enemy.enemyType);
if (DuplicateEnemyNames.TryGetValue(enemy.enemyType.name, out var currentValue)) {
configNameBuilder.Append(" ");
configNameBuilder.Append(currentValue);
DuplicateEnemyNames[enemy.enemyType.name] = currentValue + 1;
}
else
{
DuplicateEnemyNames[enemy.enemyType.name] = 1;
}
configNameBuilder.Append(" Escape Chance");
EscapeChances[enemyType] = Config.Bind(..., configNameBuilder.ToString(), escapeChance = defaultEscapeChance, ...);
}
return escapeChance;
}
void EnemyAI_Awake(EnemyAI __instance) {
var escapeChance = ConfigManager.RegisterEnemy(__instance);
if (escapeChance > 0) {
[do component]
}
}
oops I just wrote a bunch of code on discord
I think something is wrong with me
yeah smoething is 100% wrong with you
it's true
Sorry, you're saying I'd need that code if I wanted to stick with using names for keys to allow compatibility with custom enemy names overlapping? Or that I need something to that effect regardless?
that was for if you switch to using EnemyAI subclass type
but also I just realized that maybe some will reuse the class for different AI prefab instances with different behaviors so maybe that's not the best idea
I think it'd be better (read: Less stress for me) to just do it via enemyType.name. If multiple enemy mods come out with the same name, they can easily just append their author name to its enemyType.name like StarlancerForestGiant or w/e
yeah, agreed
Which they should do anyways for ease of identification in the ecosystem
sure but the ecosystem at that point is lethallevelloader
you're an ecosystem
idr if you said already, but if you did remind me or point to it, what function would I hook into in LLL to where I could seamlessly bind every registered enemy to my configs?
bc you're right, all of the custom content mods are shifting to LLL as it is so I might as well make things simple ig
thats only in the experimental rn if your still keen
can do vanilla for now tbh won't be hard to redirect
okay, so pretty much like we've already discussed will be a solid foundation?
ye more or less
If I switch them from a string to a string[], how do I keep the configuration method simple for the user? Right now in its list-style the user would just have to follow the format presented for adding enemies, but wouldn't I need to implement some back-and-forth conversions from array to not-array to not make it complicated to modify for the end-user? Or am I just missing a piece of knowledge here
well I guess it depends on how your config is set up
are you planning on having a list of enemies in your config for some reason? I thought you were just binding options for each enemy type
That was another thought in my head was to forego any manual addition of enemies entirely and just do the grab method
Which if it's what I'll migrate to with LLL then I might as well cement the methodology
I mean there's no reason to add them manually
tru tru
you already have it generating config bindings for every enemy in the game, at least if you did the thing in EnemyAI.Awake() like I suggested
the whole point is to remove the need for it to have two layers to it in the config
Yep, I just love/hate deleting code I've spent a decent chunk of time on :P
also I wonder if it's necessary to hook into LLL? maybe as an additional layer to allow custom enemies to be registered in the main menu reliably it could make sense, but why depend on LLL when you don't know whether there will even be custom enemies?
it's good to get used to doing that
the more okay you are with deleting bad code the better your code will be
I used to be that way too, but since I started to let go of that things have been much easier
I'll write whole libraries and then realize I have to throw out half the code because it just doesn't work well enough, even if it was something small it's still worth tossing if you know something you didn't before
coding is learning
any code you're writing without fully understanding the problem, you can prepare for the worst case scenario where you'll need to start over and tehn it won't feel nearly so bad
Yee, all valid.
Hooking into LLL would present a surefire way to grab everything at once, no need to wait for spawn-binding, so the config could be fully tweaked sooner from the user's end
But you're right that if someone isn't using any custom enemies (or moons/interiors with custom enemies) that there'd be no need to
Hooking into LLL would present a surefire way to grab everything at once
I'm not sure how that would work any differently than theResources.FindAllObjectsOfTypeAll(), that function just needs to be called at the right point in the loading sequence and then it'll catch everything without any dependency
ยฏ_(ใ)_/ยฏ
I also am not sure ๐คญ
if there's a point at which LLL can guarantee that all the EnemyAI prefabs have been loaded, then there should also be a point in the vanilla code where that would be the case
so making a dependency, whether it be soft or hard, seems pointless to me
Regardless, the good news is that my manual config assignment is now only two things ๐คญ
configManualEscapeChance = Config.Bind("EnemyEscapeSettings", "ManualEscapeChance", false, new ConfigDescription("Assign escape values manually?"));
configEscapeChanceDefault = Config.Bind<float>("EnemyEscapeSettings", "EscapeChanceDefault", 10, new ConfigDescription("The default chance for enemies to go into or out of the facility.", new AcceptableValueRange<float>(0,100)));```
actually I'll just remove the "assign values manually" toggle and just let people go ham
yeah I was gonna say, I'm not sure what that one is for
and what about having a Dictionary of certain enemies that should have a default non-zero escape chance so that you can have it do something you consider to be fairly balanced out of the box?
i.e.
Dictionary<string, float> DefaultEscapeChances and Config.Bind(..., DefaultEscapeChances.GetValueOrDefault(enemy.enemyType.name, 0), ...)
that way, if you feel like it's nice to have say only the blob and yippee leave by default, you can do
Dictionary<string, float> DefaultEscapeChances = {
{ "Blob", 0.25f },
{ "Hoarding bug", 0.25f },
};
and every other enemy will have zero chance
you could even make selectable presets if you had a Dictionary<string, Dictionary<string, float>> if you really wanna go nuts
Preset: "None (Why are you here?)" 0% chance
Preset: "FREEEEEEEEEDOMMMMMMM" 100% chance
ohh default float is 0
read the docs! it will return the second parameter if the key isn't present though
I am!
nice nice
the Microsoft docs are actually good unlike the Unity ones so it's definitely worth checking them if you wonder whether you can do something fancy shmancy
I can't seem to use GVoD on a Dictionary, is that more recent framework addition?
bc if so then I'm heavily annoyed bc .net 8.0 in VS doesn't like me for some reason
Framework 4.8
idr why that specific one exactly
that would make sense
.NET Standard 2.1 is what the game targets though, and it supports a whole lot of nice syntactic sugar that you won't get on Framework 4.8
mmk, can I just retarget it in the csproj?
and you'll also be able to use .pdb files to get line numbers if you target Standard 2.1
probably? I actually converted my projects over by manually editing the .csproj
can't hurt to try just switching it in the UI though, you should be able to undo if it blows up
especially if you're on version control
all it shows me is framework versions lol
make a local Git repository 
manual csproj targetframework change lol
oh I see I see
in case you want to reference what I did to mine
looks like git didn't want to generate a very pretty diff for you though...
that's certainly a lot of words, characters, and numerals
I guess a configurable "default chance" is actually redundant
if I'm gonna have a preset for vanilla anyways, and people will wanna tweak custom enemies themselves
what you could do is have an invalid value as a placeholder for using the preset values too
so like
normal values range from 0.0 to 1.0, and anything < 0 is considered invalid and will take the preset values
that way the presets won't require you to erase your config
I'm not understanding you. Each Config.Bind() I generate has an acceptablevaluerange property (currently 0 to 100 just cuz), so how would an invalid value get assigned?
When you say "erase your config" do you mean the presets would have to regenerate the config, or are you referring to replacing code?
ohhhhhhhhhhhhhhhhhhh
I hardly config stuff to much degree, I didn't realize it would let you put in something incorrect lmao
So what I could do is set the default value as -1, and if the value is not in the (0, 100) range, reassign the default preset?
I'm not sure if it lets you load it if it's outside the acceptable range, but you can certainly change your acceptable range to allow -1
That would make sense lol
what would happen if you made an offmeshlink between the entrance of an interior to the outside?
instead of doing any sort of moving and whatnot
I guess it would work? But it wouldn't be configurable per enemy, it'd be global
but in theory sure, you could plop OMLs into a scene at runtime connecting EntranceTeleports of matching IDs
i vote for all enemy rights ๐ญ
I think an OffMeshLink might look a little weird since they're designed to interpolate their agent across them, but I may be wrong
I believe you are correct
Was looking at the wiki about Butler Bees
Fortunately, though, they cannot leave the facility.
oh no
wesley have you seen what I've been working on?
.
StarlancerEnemyEscape
A fun little add-on mod for the AIFix
lmaooo
if only it could be
this all sorta happened as a result of Zaggy wanting to be able to hook into SetEnemyOutside for things, so really the blame lies on them
What have you done
I'd like some input on balancing a default preset.
I feel like it's at an okay spot here, especially since most of the time enemies don't wander right by an entrance/exit
but if anything feels like it should be increased (or further reduced) lemme know
I'm gearing up for another test
so hopefully I'll be releasing this soon
What the numbers mean
% chance to use an EntranceTeleport when close enough to it
It checks every 1s, and every time it actually rolls the dice to use it, it initiates a 10s cooldown
Why no centipedes? To not rely on PathfindingLagFix by default or something?
Lore :P
EnemyEscape will rely on AIFix tho
Around you, there are a blob and a dog, they are in the wrong spots
Also I'll just assume no input on the balance means it's good 'nuff
Yeah I see no issue with it
maybe bump up Baboon Hawks since they have the whole pack and pair thing going on but that could be argued either way
did u make sure to only allow the coilhead to teleport when its not being looked at
Good idea
I can't wait to see someone lead a coilhead outside when their friends don't know about it
if (enemy.enemyType.name == "Spring" && enemy.agent.speed == 0f) { return; }
easy coilhead condition
oh no ๐
um... might be better to check state rather than speed
So make sure it doesn't have a target at all, is what you're suggesting?
yeah
Cuz I was just conditionalizing it for not being looked at
no cuz sometimes people deliberately move the coilhead around
but yknow maybe it would be a funny feature
lol
up to you
That's what this was in reference to ๐คญ
ahh icic
although
it'd be even funnier if they think they made it out safely only for the coilhead to come screaming through the door behind them
so I will check state
if (enemy.enemyType.name == "Spring" && enemy.currentBehaviourStateIndex != 0) { return; }
I thought about checking for if 1, but this allows for the (albeit slim) possibility for someone to mod extra behaviour states in
"if it's a coilhead and it's not idle, don't do things"
Zaggy taught me the value of early returns to prevent running unnecessary code lol
@golden basin
huh, I thought the ceiling fan in facility had a killbox
it was still in its spawn animation when it teleported, but idk :P
@gleaming robin perhaps the EscapePreset should be an enum? that way, you can allow people to select a default with no escape chance for anything or your default preset, and add new ones later
(you should be able to omit the 0 entries here for brevity, though I suppose maybe you want to keep all vanilla enemies for completeness?)
dangit Zaggy, I was gearing up for release :P
I haven't messed with Enums yet though, so this would be as good a time as any to learn ; w ;
it's not too complicated
they work out of the box with config
it's better to make it an enum now than later, because if you change the type of a config option it'll break compatibility with old configs
basically, you'll just want to define all the presets within the enum and bind the config to the type of the enum instead of bool
then when you load the config option, you'll map each enum value to a preset with a switch statement
i.e. if you have an enum ConfigPreset { Disabled, NiceDefaults } make a function
Dictionary<string, float> GetPresetValues(ConfigPreset preset) {
switch (preset) {
case ConfigPreset.Disabled:
return DisabledPresetValues;
case ConfigPreset.NiceDefaults:
return NiceDefaultsValues;
}
}
Completeness is the main reason yea
So I would want to keep the bool for whether or not to use a preset, right? And then on my component's Awake(), I would check for whether or not it was set to Preset or Manual (true/false), and if Manual, call
GetPresetValues(configEscapePreset.Value);
if you allow the actual per-enemy values to override the preset, then I don't see the point in an option to enable the presets
but if you do need that, just make that another of the enum options
no need to add an extra config option when its state nullifies the state of the enum
(the point of my suggestion to add a "disabled" config was to have a default where every chance value is zero, so that people can create their own options from scratch)
Would it be able to update the config outside of the game??
uh
because this kinda conflicts with your first setence in that group
I mean, the point is for the presets to be a valid state of the config rather than something that changes all your options
at least in my understanding
- Load all config options from the file
- For any options where the value == -1, replace that internally-used value (not the value in the config) with the value from the selected preset
- ???
- Profit
Ahh
That way, people can start from a preset, modify whatever values they see fit, and then they have whatever specific behavior they want
like say they think "wow this preset is almost perfect, but this one enemy going outside is stupid and I hate it" then they can just select that preset, and change the one value from -1 to 0
ez
audio knight be skilling up like crazy
the critical part is that the config is never modified by the presets
only the values that are used within your scripts
Something about this is hurting my brain lol
Is this method meant to prevent the user from being able to actually specify a value in the config?
Okay, right, that's where I had gotten to before trying to implement the Enum (which I still want to do bc I like multiple presets)
if you have
Preset=SaneDefaults // where Coilhead escape chance is 10
Coilhead escape chance=0
then it takes all the values from SaneDefaults, except that it will never allow the coilhead specifically to ever escape instead
(all other omitted options would be set to the default, -1)
I'm sorry, this must be frustrating for you bc I'm clearly just missing something lmao
So what you're suggesting is defaulting the manual values to -1 on the first run so that presets take precedence by default, yes?
Would I just want to list the preset values in the readme for the player to actually know what each preset is?
In this manner?
Preset "Default" = [enemyName:escapeChance], [enemyName:escapeChance], etc.
y'know what, that's all aesthetic stuff, I have an actual issue lol
I have the Preset method
internal static Dictionary<string, float> GetPresetValues(ConfigPreset preset)
{
switch (preset)
{
case ConfigPreset.Disabled:
{
return DisabledPresetValues;
}
case ConfigPreset.NiceDefaults:
{
return DefaultEscapeChances;
}
}
return DefaultEscapeChances;
}```
I bind it
configEscapePreset = Config.Bind<Enum>("EnemyEscape", "EscapePreset", ConfigPreset.NiceDefaults, new ConfigDescription("Which preset to use. If an enemy has a value manually set below, it will take priority."));```
but I cannot for the life of me figure out how to actually access that value
Bind<ConfigPreset>, not Bind<Enum>
I would think anyway
and to access the value do you mean access the configEscapePreset.value, or access the preset values?
if you mean access the preset values, just call the function with the value of the config option
and you can wrap the currently-selected preset in another function like
static Dictionary<string, float> GetCurrentlySelectedPreset()
{
return GetPresetValues(configEscapePreset.Value);
}
And for the preset list I would do this, right? Or is this where I'm going wrong?
internal enum ConfigPreset { Disabled, NiceDefaults, Minimal, Chaos, Manual }
looks fine to me
although it's better to put the options on multiple lines, I just formatted it that way in Discord because I'm lazy
I don't want to imagine what some of my enum declarations would look like if they had to be formatted like that
I had the declaration of the ConfigEntry set to Enum which is why it was yelling at me ^^;
public static ConfigEntry<Enum> configEscapePreset;
oh yeah, that'll do it
So for custom enemies, I think the best way to go about registering them would be to default to 0 on first run and let the user adjust it later. Since they won't be included in any preset, I can dedicate a config section to mod enemies and restrict their value from 0-100 with a separate conditional binding on GameNetworkManager.Start()
Okay, I think I'm nearing completion. Hoping to get it released in the next hour
I've had the mechanics done since this morning, most of my coding today has been on this config ; w ;
Opinions wanted: Are there any other enemies besides the Coilhead that should only teleport if they're in their "passive" state (currentBehaviourStateIndex == 0)?
there's not much reason to change the default for custom enemies, -1 will default to 0 if it's not in the preset anyway
I'd make it global, but some enemies like the Crawler don't immediately revert to state 0 upon losing their target, and I'd like for enemies to have a chance to sort of follow the player out
and that opens you up to including mod enemies in the presets as well
fair fair
entry and exit you mean?
yea if they happen to get close enough to the entrance teleport
do you have a size limit on that lol
Size limit on what specifically?
I see
Custom enemies will also default to 0 as well, so the out-of-the-box state of the mod is fairly reasonable
@tulip vault do you think it would cause any problems to instantiate an empty game object at each EntrancePoint tagged AINode/OutsideAINode at runtime so that enemies have a chance to actually path to where they could teleport?
If I hook into RoundManager.FinishedGeneratingLevel() I could maybe slide the instantiation into a prefix and then let the original code run, which would ensure that the game finds them for its own node arrays
Or better yet, I could just postfix it and add them to the arrays myself
Even better, rather than instantiating anything, I can just change the tag of the EntrancePoint
change the tag?
yeah, just change whatever the EntrancePoint (usually telepoint in vanilla scenes) is from Untagged to AINode/OutsideAINode
It occurs to me that I might wanna instantiate an empty child object tagged SpawnDenialPoint and add it to the RoundManager's spawnDenialPoint array, otherwise a forest giant might spawn in front of the door
foreach (EntranceTeleport entranceTeleport in entranceTeleports)
{
if (entranceTeleport.isEntranceToBuilding)
{
entranceTeleport.entrancePoint.tag = "OutsideAINode";
var spawnDenial = new GameObject();
spawnDenial.tag = "SpawnDenialPoint";
spawnDenial.transform.position = entranceTeleport.entrancePoint.transform.position;
RoundManager.Instance.outsideAINodes.AddItem(entranceTeleport.entrancePoint.gameObject);
RoundManager.Instance.spawnDenialPoints.AddItem(spawnDenial);
}
else
{
entranceTeleport.entrancePoint.tag = "AINode";
RoundManager.Instance.insideAINodes.AddItem(entranceTeleport.entrancePoint.gameObject);
}
}```
oops, forgot to add them to the arrays
that's better
could i possibly suggest making a new gameobject for the ainode tag rather than setting the preexisting one?
and explicitly naming both new objects to indicite its from your mod
sure
for context on that first request, even though its probably never going to matter and your def in a position to make that change, it is technically a destructive edit to the basegame, so if you don't need to do it it's nicer not to
gotcha
hm, just used a blob to test and it kinda got funky again, I really though Warp took care of that
I'll add it to the readme as a note
I was hooked into FinishGeneratingNewLevelClientRPC and kept getting duplicates, but hooking into SetLevelObjectVariables instead made it work perfectly โจ
Starlancer AIFix v3.5.0 | EnemyEscape v1.0.0
Go nuts, I'm going to bed
(Also, big thank you to @faint copper and @tulip vault for all their help!)
if your AI nodes are children of the teleports, you could just check if those tagged objects already exist before adding them
that way you never have dupes
but it's good if you found somewhere that runs on all clients and the host where you can set those up
(definitely make sure of the clients part, though, run it in LAN and verify that those get created)
Starting in LAN is a way to check client sync?? TIL
you can have multiple LAN instances at once
you have to start the game twice in LAN
yeah
then join yourself
ohhh that makes more sense
you could also do multiplayer but much easier to do lan
I'll verify in the morning (or some kind soul can test and let me know)
But it should be good, since FinishGeneratingNewLevelClientRPC() calls SetLevelObjectVariables()
Ooough this is beautiful
we're eating good today ๐
this is same like LethalEscape but different using the Door for out?
oh boy I cant wait to fucking die
peak
Oh yea itโs obviously a bit buggy rn
In a test with @wary bough we discovered that hoarding bugs when aggroโd inside the facility will follow you outside perfectly fine, but once they get outside they tend to freeze and donโt move in place until like 2-10 seconds of rethinking their life, then they start acting normal again
But when trying to lure them back inside using aggro
Almost never worked
Now my main question about how this mod works is the config itself
Letโs say I have a thumper
And I set the chance for it to path outside is 30%
Does this mean that without aggro on any player, a thumper has a 30% to get basically a pathing command to go outside/inside the facility
Or
For example it starts charging at a player who then leaves the facility
Does that thumper have a 30% of following the player outside
If what I said made any sense to anyone
@spiral ermine heard on the bug bugginess
the percent chance is purely for it to actually use the teleport, so in your case if it gets close enough to the teleport during it's charge after you leave, it has a 30% chance of using the teleport IF it's not on cooldown
Cooldown is a 10 second timer currently, but I can adjust that
The system is that, every second while not on cooldown, the enemy checks some logic and checks the distance between each teleport point and themselves, if they're close enough, they roll the dice and try to leave
I think the issue you were seeing with the lootbug is a design flaw in my code actually
I currently have it go on full cooldown if it fails to leave, and that's probably too heavy-handed
Here's my current thinking:
Reduce cooldown to 3 seconds, but make it so an enemy only teleports if it doesn't have a target.
I still have to investigate the lootbug behavior tho
How I thought it worked, and maybe should work
Is every [Insert Check Cooldown] the enemy will check if within [Insert Configurable Distance] of an entrance or exit point have a [Configurable Chance] to travel to that location and teleport
Then
A separate check for if enemy has the ability to aggro onto a player
If enemy is aggroโd or is following the player within the last [Configurable Time in Seconds] have a separate [Configurable Chance] to use the nearest teleport
Professional idiot speaking here
If anything Iโm spitting is absolutely horse water let me know
But with the nonsense I just spouted letโs say The Hoarding Bug has a 40% chance if itโs at least 20? ||(I really donโt fucking know the scale system of lethal company now that I think about it)|| units away from either a fire exit or main entrance it will then path towards that teleport and use
But then letโs say if you the Brilliant player decides to steal the hoarding bugs favorite key, and it aggros on you.
The hoarding bug then has a 80% chance to follow you out of the exit / entrance you used if it saw you within the past 5 seconds
finally, good lethal escape
so, how does this work? does it teleport enemies outside when you go outside like lethal escape? or do enemies actually properly go to the doors, like masked?
and can enemies go back inside?
When close to the door
actually I think that's a good idea
lemme see if I can replicate the hoarding bug issue
Okay I think I fixed the hoarding bug, lemme see about implementing a chance to path to the node
I suppose the enemyescape doesnt work with modded enemies yet?
It'll automatically detect any enemy that's registered in the game when you get into the main menu and add it to the config, from there if you have LethalConfig you can set the chance in game, else you can quit and modify the config in r2 (or manually if that's your style)
I just don't offer any presets for modded enemies :3
i am getting this error, which i got for spectateenemies too once, and its because of "dont touch me" enemy
this is the full error
Alright, gonna test adding this to my update interval
if ((Time.time - lastPathToTeleport) > PathCooldownTime) //Attempt to path to a nearby entrance.
{
if (random.Next(0, 100) < chanceToPath)
{
foreach (EntranceTeleport teleport in entranceTeleports)
{
if (Vector3.Distance(teleport.transform.position, enemy.transform.position) < PathRange)
{
agent.SetDestination(teleport.transform.position);
lastPathToTeleport = Time.time;
break;
}
}
}
}
//Upon successfully telporting
enemy.SwitchToBehaviourState(0);
enemy.StartSearch(enemy.transform.position);```
not really sure what the lethal things gremling enery error is
uhhh I'm not seeing anything of mine in there
ah I missed it, ty
i think its because of DTM enemy at least
@edgy topaz your apostrophe is killing mods again ๐ญ
I'll attempt to implement the following in the config bindings next update, should hopefully fix that
Bind[...](enemy.enemyType.enemyName.Replace("[^0-9A-Za-z _-]", ""))```
Not again hahaha
Hmm, I wonder if the special logic for coilheads would be still needed with this implementation
Because pathing to the exit would be needed before they are able to exit. A coilhead would be naturally unable to path while held by looking
I'm replacing that local logic with a global logic check for the targetplayer being null, that way if someone is being mauled to death at the entrance, the enemy doesn't just decide "oh hey let's go outside"
So like I said, this is a good idea, but it might take a bit to figure out how to reliably set a destination, as my test so far has just kinda broken the pathing when I try to set it to the entrance ๐คญ
Itโs ok man down the testing I did it works fine with everything else like butlers
Actually I do have another question
If this pathing logic ends up working out. Maybe we can get another set of such configs for returning
Because I had 3 major reasons I didnโt use lethal escape
- Once and enemy escapes, it cannot reenter which you fixed
I'd like to set a high range and chance to return, for enemies that would be at a disadvantage outside
Or like lootbugs, they would know that there is no loot outside and return if able
- Didnโt escape to the actual door itself and no fire exit support which you have done perfectly
- My concern on how the power is calculated
If a bracken is spawned inside, it takes 3 power level from the inside enemy power level
If that bracken leaves, does it still count towards that inside power level?
Or does it count towards that outside power level
As far as configs go, I think it'd be best to only allow users to adjust the chance to use the teleport. Cooldowns/ranges I'd prefer to keep things internal for balance reasons, but I'm always open to suggestions for enemyAI conditions
I don't alter any power levels, since that could get messy

So hypothetically
Letโs say we are on assurance
And I have every single enemy spawned until the power level caps inside
If I then lure all the creatures outside or they all escape
And then go down from the platform
I do not believe they can path back up that ladder
Meaning
Depends on the enemy :3c
Inside is now free
Sure, but outside is now hell
The Jetpack Certified Employees reaction to the disgusting waste of time referred to as โwalkingโ:
Hmm, then if we don't get configs access to range. Would you set the outside range to be quite large? Like within the unobstructed view distance of an entrance?
The idea being that if they get farther than view distance, they have gotten lost on the outside
And don't know the way back
Thatโs if they can even access the facilities doors once outside
The pathing should be able to determine that
I do not believe any vanilla enemy can path up ladders on experimentation other than masks
As Piotrenewicz is suggesting, if I figure out how to reliably give it a chance to directly set its path to the entrance, I could increase the outside range
lemme check
I know this for fact
Because Iโm making it the fuck up
But like Iโm at least 80% sure Iโm correct
Hoarding bugs, spiders, thumpers, snare fleas
Cannot use ladders
Jesters 100% canโt use ladders
Slimes ๐
Coilheads cant use the ladders
I havenโt seen butlers interact with ladders yet
Iโll test that
And when it's unable to path to the closest entrance, it should have a way to consider the second closest, ignoring the first
That would allow the emergency exits to bypass the ladder problem
On god?
I'd like to see a jester trying to use a ladder
Me awaiting Knight to provide the source that destroys my baseless information
Oh yea on the other half of my entrance discussion
Outside enemies would normally never be able to enter the facility because of how the vanilla maps are
Enemies that can climb according to their NavMeshAgent:
Baboon Hawks
Butler (and bees)
Centipedes
Docile Locust Bees
Manticoils
Ghost girl
Bracken
Flower Snake
Hoarding Bugs
Jester
Masked
Nutcracker (that surprised me)
Spore Lizards
RadMech (dear god)
Circuit Bees
Hm
I think the issue is
They have the ability
But the maps were never intended for it
If possible I want you to go to experimentation
Spawn a nutcracker
Aggro it
Set the chance to like 100 just for measure
Go outside
It will follow you outside
Go down the ladders
It can path down the ladder
But I donโt think it will be able to path up the ladder
They probably climb ladders the same way they jump over gaps. Just interpolate through without proper animation
@turbid wave
yea that's how OffMeshLinks work
Proof right there ๐คฃ
the ladders on experimentation are provided with both jump and climb OffMeshLinks :3c
iirc an OffMeshLink can trigger a special animation to make the agent look proper as it's crossing the link. It's just that Zeekers didn't bother
ยฏ_(ใ)_/ยฏ
Put it on jah rn
Test it
Iโm not home rn
But I swear them little creatures got no hops
I'll test it later, I'm COOKIN rn
or maybe I'll test it with this next escape test
if I'm feeling nice
I don't feel like recording a vid tho, so you'll have to take my word for whatever the result is
@spiral ermine both of these silly lads came up that ladder
IMO the inside and outside range should be configurable per enemy. Cooldown is fine to be internal.
We'll see
I make no promises ๐คญ
Okay I see the issue that I made with my set-pathing attempt, I was calling on the navmesh agent when EnemyAI already has a function for this
Holy fucking shit
Also may have just figured out the weirdness with some enemies on teleporting outside
Only thing I have to figure out is the power balancing
I don't think it'll be an issue tbh, I just have to figure out how to make them path back to the entrance reliably
Which I'm close to
hrmmmm the puffer is really fighting me on this pathing business
Starlancer this happend mid game / Enemy Escape
didn't get any change in the way the plugin works
but apeared
Seems like losing the player in a chase doesn't necessarily set targetPlayer to null, so I'm gonna switch back to checking for currentBehaviourStateIndex != 0. Should function roughly the same
custom non yet
only lethal casino and it happend after it
@tall merlin do you have anything with an EnemyAI component in your mod?
from what i understand, it just being a random chance to teleport if an enemy is near a door, couldn't enemies get warped outside mid-chase if the player went near the door? and wouldn't enemies be unlikely to chase players outside?
also, is it possible to configure the range to the door?
I'm implementing a way to prevent them warping during chase. Some enemies will go to last known position, which would allow them to teleport and effectively follow the player outside (also maybe will implement a scaling chance where
if (closeToEntrance) {chanceToPathScaled = chanceToPath * howCloseToEntrance}
range config is a maybe
well do you mean range as in for pathing or teleport?
teleport but eh
nah, no config for that
also why can radmechs go in
anything can go in, but the default for radmechs is 0
dearie me
will enemies that always track players like bracken navigate outside if nobody is inside?
As of right now no, but it's something I'll look into implementing
enemy escape seems to be killing my game if i have dont touch me installed
straight up annihilates it
clients dont see custom scraps, custom enemies and cant die
and it only started happening after i installed enemy escape
@edgy topaz pls just get rid of this apostrophe i really like the mod but it killed my game 3 times already ๐ญ
that's big bizarre
thats the second mod affected by the apostrophe
i was just eyeing this thread all day cause enemy escape was the only mod i added into my pack and it just outright broke everything
Sorry, I'm just trying to get some good stuff implemented
oh nws lol
OKAY I finally have enemies pathing outside properly (except for hoarding bugs, they just go their nest and chill, though they'll take scrap if you put it near them)
I'm curious, why is AIFix necessary alongside EnemyEscape? does SetEnemyOutside not suffice in v50?