Stop and take a look around

Post your example zscripts/ACS scripts/etc here.
Forum rules
The Projects forums are only for projects. If you are asking questions about a project, either find that project's thread, or start a thread in the General section instead.

Got a cool project idea but nothing else? Put it in the project ideas thread instead!

Projects for any Doom-based engine (especially 3DGE) are perfectly acceptable here too.

Please read the full rules for more details.
User avatar
Sir Robin
Posts: 537
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Stop and take a look around

Post by Sir Robin »

Background
I'm working on writing an RPG/Adventure game-play mod for GZDoom. One thing I've been working is a "Look" feature where the user can spawn a 3d cursor into the world and it will describe what it is pointed at. I'm designing it modular and portable so it can be used in another mods as well. This probably isn't too useful in most doom mods which are more focused on run-and-gun and less on examining the environment. But would be great for an RPG, adventure, puzzle, exploration, horror, etc, or any mod where you want the player to stop and look around and even give them hints about things they see.

Usage
To use it, load the pk3 like you would any other. It only requires gzdoom.pk3 resources, so it should be iwad independent. Bind a key to +Look either at the console or through the usual options menu. I use the key "Q". In game, hold this key to spawn a 3d cursor, then move/look around until it lands on what you want to look at, then release the key. The description will be printed to the console. I've included descriptions for all Doom 1,2, and Plutonia things. Tell me if I got something wrong or something that could be better.

How it works
There is a main describer that does a simple look up for a description of the item under the cursor in the language file. There are also describer add-ons to give additional details. If someone wants to use this in their mod and has custom information to present that is more complex than can be handled in the lookup, they only need to write that logic in a describer add-on and the rest is automatic. The describer also runs everything through a language handler which keeps track of the object's grammatic gender and number so that it picks the correct pronouns and adjectives and articles and whatnot. This isn't too important in English where most words are gender-indiscriminate, but if you're including translations in your mod, this will be helpful to keep the language correct.

Uses
What this is useful for:
  • Modding, obviously. Just include it in your mod and it's good to go. If you have custom graphics or actors, see the language files for examples of how to add descriptions for your mod.
  • Debugging - Trying to figure out why your actor isn't doing what you want it to, and wish you had an easy way to inspect it's variables in-game? Easy - just write a describer add-on to print those values you're interested in and now you've got a useful inspection tool. I use this in combination with Nash's RadiusDebug mod (and my mod of that which color-codes the 3d frame depending on the actor) and it's super useful to see where the actor is and what it is doing.
  • Playing - I use this even on plain doom levels. Two biggest things I use it for: looking at floors to see if they're damaging - it saves me the trouble of save-check-load, and looking at switches to see if they're a level exit. I hate unmarked level exits, now they're less of a surprise.
  • Cheating - I tried to make this not give out too much information, but any additional information you give the player that the level author didn't know about could potentially spoil the game. One thing I did was that if the cursor hits a line marked as 1-sided on the map, I consider this a secret and give less information about it than usual. Another thing is that the cursor passes through mid-textures, so it can be used to detect fake walls.
Screenshots
Spoiler: Screenshots
Download
Try it out:
MWR_LookCursor.pk3
Works on all Doom maps, also includes a testing map with a variety of doors and floors and other things to look at. Load Test_MwrLook and have a look around.

Credits
And I have to give credit to Player701 on this forum who answered lots of questions and even took the time to provide detailed examples. Without that help I wouldn't have got this code anywhere near where it is now.

Known Issues
  • Spawning a cursor on a 2-sided linedef attached to either a floor moving up or down or a ceiling moving down causes the cursor to jitter for the first few frames. I have no idea what's causing this or how to fix it.
  • I have no logic to determine if the sight check should go through a mid-texture or stop at it. For now it always goes through.
  • When looking at geometry moving towards you (floor coming up, ceiling coming down, poly-object approaching) the cursor gets spawned behind the geometry. I think my cursor spawns before the geometry is adjusted, but I don't know how to fix that.
  • The language handler tracks the grammatical gender and number for the object in a sentence like "You see a cacodemon." but not for the subject. This is fine for English and the vast majority of languages out there, but for languages that conjugate in agreement with the subject, this would be a problem for translating. But I'll worry about that if and when it ever comes up.
Updates
Spoiler: updates
Last edited by Sir Robin on Tue Mar 15, 2022 2:27 pm, edited 9 times in total.
User avatar
Enjay
 
 
Posts: 26498
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Stop and take a look around

Post by Enjay »

That's really neat and seems to work very well.

I only gave it a quick run but the one thing I noticed it can't highlight are 2-sided walls (as you mentioned). You described this as an advantage (won't reveal secrets), however, for example, you can't get any information about the bars in the little open area behind the player start of map01 of Doom2. I can see several reasons why catering for that situation might be problematic though.
User avatar
Sir Robin
Posts: 537
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Re: Stop and take a look around

Post by Sir Robin »

Yeah the thing with 2-sided mid-textures is that I had to decide to either stop and describe them or continue past and describe what's behind them. Either technique is going to have pros and cons, but I decided that the latter was more common and useful than the former. Other than fake walls, the only real reason to use 2-sided mid-textures is for transparent textures, like bars, rails, vines, spider webs, etc. How often do I expect the player to want to look at those, vs want to look at things past them? I figured past was the better way to go.
I'd like to be able to hit-detect pixels in the mid-texture to know if I should look past it or not, but to do that I would get a getpixel() function on the texture, and I'm told ZScript doesn't have that.

Oh and I forgot to mention that I have included a testing map in the package, so edited the original post. Also found a few bugs and posted a new release.
User avatar
Enjay
 
 
Posts: 26498
Joined: Tue Jul 15, 2003 4:58 pm
Location: Scotland

Re: Stop and take a look around

Post by Enjay »

Understood. There is certainly no way for either option to be the best one 100% of the time. A getpixel function of some sort would be the best way forward if such a thing existed. Personally, I think "bars block your way", "the gate is rusted shut", "a cobweb covers the entrance" etc are just as likely to be useful as the cursor being able to see through such structures.
User avatar
ramon.dexter
Posts: 1506
Joined: Tue Oct 20, 2015 12:50 pm
Graphics Processor: nVidia with Vulkan support
Location: Kozolupy, Bohemia

Re: Stop and take a look around

Post by ramon.dexter »

Wow, this is nice! I was actually thinking about something like this, but my coding skills are too thin to make it viable. I'll try to play with this. Thanks!
User avatar
Sir Robin
Posts: 537
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Re: Stop and take a look around

Post by Sir Robin »

Just added some improvements:
  • Added descriptions for gibbed actors
  • Added indicator for actor friendly/hostile
  • Added indicator for actor target
  • Players are gendered and named
  • Added color coding
I'll update the first post as well
User avatar
Sir Robin
Posts: 537
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Re: Stop and take a look around

Post by Sir Robin »

Update: overhaul of the back-end code. Not much change that the player will notice, but it's much easier to add and change things in code now.

The biggest difference the player will notice is that the language handler can now build lists of adjectives instead of putting them all into separate sentences. For example it used to say "You see a zombie. It is hostile. It is healthy. It is targeting you." Now it can say "You see a zombie. It is hostile, healthy, and targeting you."

Also took the health describer out and built a generic proxy percentage describer. You give it two numbers like x/y and it looks up the percentage proxy in a table. So the health table might say "healthy", "wounded", "badly wounded", "near death", "dead", and it picks the one based on the percentage.
I'm using that in my UW mod to describe things like how full my lantern is: "full", "nearly full", "three quarters full", "half full", "a quarter full", "nearly empty", "empty"
And the condition of my armor: "new", "like new", "used", "worn", "badly worn", "falling apart", "ruined"

There's also a boolean for extended description to tell the user more info if the item is in inventory vs on the ground.
User avatar
Sir Robin
Posts: 537
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Re: Stop and take a look around

Post by Sir Robin »

Update: Another big code overhaul. Previously I was making each describer a separate static event handler. Now there is only one (the describer provider) and the rest are just basic classes.

Added a new feature to add notes for extended descriptions. Can be attached to textures or to actor name/tag/class generically, or to a specific one using a user variable to identify it.
Examples:
Can be applied generically to an actor:
Spoiler:
Can be applied generically to a texture:
Spoiler:
Can be applied to a line:
Spoiler:
Can be applied to both a texture and a line:
Spoiler:
The "This is door #1" text is attached to the texture while the "It is opened[...]" is attached to the line.
User avatar
ramon.dexter
Posts: 1506
Joined: Tue Oct 20, 2015 12:50 pm
Graphics Processor: nVidia with Vulkan support
Location: Kozolupy, Bohemia

Re: Stop and take a look around

Post by ramon.dexter »

This is just wonderful addon. But there is one thing that would make it even better: It would be nice to make it little bit random. Like, I would like to define like 2-3 strings for an object, and the parser would randomly select one of the defined string for the said object.

Example:
DESC_ACT_niceChair = "OSan office chair"
DESC_ACT_niceChair = "OSan office chair. Looks comfortable"

Would this be possible to implement?
User avatar
Caligari87
Admin
Posts: 6174
Joined: Thu Feb 26, 2004 3:02 pm
Preferred Pronouns: He/Him

Re: Stop and take a look around

Post by Caligari87 »

Multiple strings are clunky in my opinion, and rather inflexible.

I wrote a relatively simple string function that allows defining sub-strings which can be picked from randomly.

Code: Select all

// Make a string from a variable template
// By Caligari87
static string BuildVariableString(string msg){
	// some input example strings:
	// "an empty {beer|soda|wine|champagne} bottle. {It is {cracked|broken|unlabeled}.}"
	// "a {small|large|||} plush {emoji|cacodemon|teddy bear|furbie} toy."

	// Collapse substrings, repeat until no more { }
	while(true){
		int LeftBrace = msg.RightIndexOf("{"); // find the rightmost left-brace {
		int RightBrace = msg.IndexOf("}",LeftBrace+1); // find the nearest matching right-brace }
		if (LeftBrace == -1 || RightBrace == -1) { break; } // stop looping if no more {}

		string substring = msg.Mid(LeftBrace+1, (RightBrace-LeftBrace)-1); // get inner text string, without {} braces
		msg.Remove(LeftBrace+1, (RightBrace-LeftBrace)-1); // remove the inner text string, leave the {} braces
		
		// build an array of substrings from the extracted string, separated by |
		array<string> substrings;
		substring.Split(substrings,"|");

		// pick a random sub-string from the {|||} set
		// replace the leftover {} with the picked substring
		// empty || pairs can be used for random null results
		msg.Replace("{}", substrings[random(0,substrings.Size()-1)]);
	}

	// Remove double-spaces, repeat until no more
	while (true) {
		if (msg.IndexOf("  ", 0) > -1) { msg.Replace("  ", " "); }
		else { break; }
	}

	return msg;
}
I use it in Ugly as Sin to generate descriptions for random loot items. This mod seems like it'd be a good application for it as well.

8-)
User avatar
Sir Robin
Posts: 537
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Re: Stop and take a look around

Post by Sir Robin »

ramon.dexter wrote:This is just wonderful addon. But there is one thing that would make it even better: It would be nice to make it little bit random. Like, I would like to define like 2-3 strings for an object, and the parser would randomly select one of the defined string for the said object.

Example:
DESC_ACT_niceChair = "OSan office chair"
DESC_ACT_niceChair = "OSan office chair. Looks comfortable"

Would this be possible to implement?
First, a clarification: The base describer's job is to take in a passed game object and return a describer - in code I call this a GDO for Grammatic Describer Object. The GDO should only contain a noun phrase, in essence a noun and possibly an article and/or adjective(s), but no sentence structures or additional information. That additional information belongs in the addon describer. The reason being is that this GDO will be used by the language handler to construct a sentence using the GDO as the object (or possibly the subject).
Example: When the player looks at a nice chair, you first call the describer to get a GDO for that chair object. Then call the language handler to describe what the player sees, using the template "You see [object]."
If the GDO contains "an office chair" then the sentence becomes "You see an office chair."
If the GDO contains "an office chair. Looks comfortable" then the sentence becomes "You see an office chair. Looks comfortable." That seems to work, but it's just luck.
For example if you create a new monster that can pickup nearby objects and fling them, you might want to describe that to the player with the language handler, so you'd create a template like "[actor] picks up [object] and hurls it toward you!"
A GDO of "an office chair" will work in that sentence, but "an office chair. Looks comfortable" would break the sentence structure.
So that's why the extended information belongs in the addon describer, to be called when appropriate.

So all that being said, what you want to do it write an addon describer that will provide that additional information and you can put your random code in there.
It works like this: The Describer handler houses the base describer, and also a list of registered addon describers. When you code an addon describer and register it with the describer handler, it gets added to the front of that describer addon list. The addon describer will be passed an object and will return true if it is able to describe that object or false if not. When the handler gets a call for an addon describer, it goes down that list calling each addon describer with the passed object until it gets a true or gets to the end of the list. That means that the last registered addon describer gets the first chance to describe the object, effectively overriding previous describers. So if you have conflicting addon describers, the load order matters. The most correct one should always be loaded last.

I hope that all makes sense?

So your addon describer would look something like this:

Code: Select all

	override bool TryDescribeActor(out string description, in GramDescObj TheObject, actor a, actor observer, bool examine)
	{
		if (a is "niceChair")
		{
			//TODO: Get list of random descriptions
			//TODO: Pick one
			//TODO: Assign it to 'description' variable
			return true;
		}

		return false;
	}
Note that string description should hold a complete sentence, it will be concatenated on the end of the "You see [object]." sentence. So this would be a good place to put your "It looks comfortable." line.

Also note that the function has an input called "actor observer" that tells you who called this describer. So you can give a different description depending on who is looking at the object. For example in a game like Hexen the description can be different depending if the observer is a fighter, cleric, or mage. Or if it's a deeper RPG with a lore/id system, you can do a lore skill check on the observer to know if they are able to properly identify the object and return a description accordingly.
User avatar
Sir Robin
Posts: 537
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Re: Stop and take a look around

Post by Sir Robin »

I forgot to mention, you don't even need to provide the entire "It looks comfortable." sentence. The language handler can build that sentence, you just need to provide the adjective(s). There are 3 adjective lists, for "is", "was", and "looks". You add your adjectives into the appropriate lists and then call the language handler to build the sentences.

So for example:

Code: Select all

	override bool TryDescribeActor(out string description, in GramDescObj TheObject, actor a, actor observer, bool examine)
	{
		if (a is "niceChair")
		{
			clear(); //clear the sentence builder

			string adjComfortable=//TODO: get the "comfortable" adjective
			AddAdjLooks(adjComfortable); //add this adjective to the "looks" list
			description=BuildText(TheObject); //build the sentences

			return true;
		}

		return false;
	}
This code will also return "It looks comfortable."

The advantage to doing it this way is you can hand it multiple adjectives:

Code: Select all

clear();
string adjExpensive=//TODO: get the "expensive" adjective
string adjComfortable=//TODO: get the "comfortable" adjective
AddAdjLooks(adjExpensive); //add this adjective to the "looks" list
AddAdjLooks(adjComfortable); //add this adjective to the "looks" list
description=BuildText(TheObject); //build the sentences
This will return "It looks expensive and comfortable."

Or even a third one:

Code: Select all

clear();
string adjExpensive=//TODO: get the "expensive" adjective
string adjComfortable=//TODO: get the "comfortable" adjective
string adjInviting=//TODO: get the "inviting" adjective
AddAdjLooks(adjExpensive); //add this adjective to the "looks" list
AddAdjLooks(adjComfortable); //add this adjective to the "looks" list
AddAdjLooks(adjInviting); //add this adjective to the "looks" list
description=BuildText(TheObject); //build the sentences
This will return "It looks expensive, comfortable, and inviting."

I wrote this so I could just hand it adjectives and not have to worry about building the sentences manually each time.
What you could do with that for example is come up with a list of 10 adjective that describe that chair, then randomly pull 1, 2, or 3 of them from that list and feed them into this sentence builder.
That could look something like this:

Code: Select all

clear();

//Read the adjective list string, abort if empty
AdjList=MWR_LanguageHandler.LookupGrammaticMatch("DESC_ADJLIST_"..a.GetTag(),TheObject,"");
if (AdjList==""){return false;}

//split the string to an array and shuffle it
AdjArray=StringSplit(AdjList,"|");//pseudocode, I'm not sure offhand what the proper function call is
AdjArray=ArrayShuffle(AdjArray);//again, pseudocode

//pick a few adjectives and add them to the builder's list
AdjCount=clamp(random(1,3),1,AdjArray.size());
for (int i=0; i<count; i++){AddAdjLooks(ArrayAdj[i]);}

//call the builder
description=BuildText(TheObject); //build the sentences

return true;
Then in your LANGUAGE file you have a line like this:
DESC_ADJLIST_NICECHAIR_XX="expensive|comfortable|cozy|inviting";
Put as many adjectives as you want and it will pick a few of them and build the sentence.

There are 3 adjective lists - one for "looks", "is", and "was". So if the chair is a shootable object, you don't want it to still look comfortable after it is destroyed. So you could do this:

Code: Select all

			if (a.health>0){
				AddAdjLooks(adjective);
			} else {
				AddAdjWas(adjective);
			}
If you look at the chair before it is destroyed it will say "It looks expensive and comfortable." but looking at it after it is destroyed will say "It was expensive and comfortable."
User avatar
Sir Robin
Posts: 537
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Re: Stop and take a look around

Post by Sir Robin »

Caligari87 wrote:I use it in Ugly as Sin to generate descriptions for random loot items. This mod seems like it'd be a good application for it as well.
That is some useful string manipulation code. But wouldn't it be better to randomly generate those attributes and store them on the actor? If you're doing it at the string level, then doesn't the description of an object change every time you look at it? Or was that your intention?
User avatar
Caligari87
Admin
Posts: 6174
Joined: Thu Feb 26, 2004 3:02 pm
Preferred Pronouns: He/Him

Re: Stop and take a look around

Post by Caligari87 »

In my use case, it only ever gets called once for flavor, it's not required to be persistent. Actually I lied. I use the output of the function to place a persistent message on an "you looted this enemy body and found X" inventory marker.

For consistency you could use some unique data about the object as a seed for the random call, such as position or something. Alternatively, you could use the output of the function to overwrite the descriptive string variable on the actor, so it gets "set in stone" the first time you examine it.

Code: Select all

// example
LookAtDescription[37] = "A {metal|wood|plush} chair."

void LookedAtSomething(int objIndex) {
  LookAtDescription[objIndex] = BuildVariableString(LookAtDescription[objIndex]);
  console.printf(LookAtDescription[objIndex]);
}

// Output: "A metal chair."
// Overwrites the description at objIndex with the final string.
Since the BuildVariableString function returns a "fully collapsed" version of the input text, something like this will always return the same description if you use it to overwrite wherever the original "uncollapsed" description is stored on the unique instance. Calling the BuildVariableString on an already collapsed string won't change it again.

Of course you could also repurpose this with different separators for other things built into the same string, since the function will ignore stuff it doesn't recognize.
"A {metal|wood|plush} chair. It is [in perfect condition|partially damaged|totally destroyed]"

8-)
User avatar
Sir Robin
Posts: 537
Joined: Wed Dec 22, 2021 7:02 pm
Graphics Processor: Intel (Modern GZDoom)
Location: Medellin, Colombia

Re: Stop and take a look around

Post by Sir Robin »

Caligari87 wrote:Of course you could also repurpose this with different separators for other things built into the same string, since the function will ignore stuff it doesn't recognize.
"A {metal|wood|plush} chair. It is [in perfect condition|partially damaged|totally destroyed]"
That makes sense. So the first one would be collapsed and stored once per object, but the second one would collapse on each call, so it could be adjusted for the object's changing health. So 2 layers, a hard collapse and a soft collapse, as it were.

Return to “Script Library”