Page 1 of 1

Stop and take a look around

Posted: Tue Mar 01, 2022 4:15 pm
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

Re: Stop and take a look around

Posted: Tue Mar 01, 2022 7:07 pm
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.

Re: Stop and take a look around

Posted: Tue Mar 01, 2022 10:16 pm
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.

Re: Stop and take a look around

Posted: Wed Mar 02, 2022 1:36 am
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.

Re: Stop and take a look around

Posted: Wed Mar 02, 2022 11:13 am
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!

Re: Stop and take a look around

Posted: Thu Mar 03, 2022 1:05 pm
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

Re: Stop and take a look around

Posted: Fri Mar 11, 2022 11:42 am
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.

Re: Stop and take a look around

Posted: Tue Mar 15, 2022 2:24 pm
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.

Re: Stop and take a look around

Posted: Fri Apr 29, 2022 6:24 am
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?

Re: Stop and take a look around

Posted: Fri Apr 29, 2022 7:44 am
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-)

Re: Stop and take a look around

Posted: Sat Apr 30, 2022 1:53 am
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.

Re: Stop and take a look around

Posted: Sat Apr 30, 2022 1:05 pm
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."

Re: Stop and take a look around

Posted: Sat Apr 30, 2022 1:11 pm
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?

Re: Stop and take a look around

Posted: Fri May 06, 2022 1:12 pm
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-)

Re: Stop and take a look around

Posted: Fri May 13, 2022 2:24 pm
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.