0 votes
by (1.3k points)
edited by

So, I came up with the beginnings of an event system. My idea is to form a timestamp each and every passage and to use that to see if an event is still on-going or not, and if not, to cull it. I guess I'll call it something like TopicalListUpdate or so.

But, I haven't done that bit yet. I've just done the timestamp stuff. So code:

setup.makeTimestamp = function () {
	var sv = State.variables;
	return Number(""
			+ sv.time.years.toString()
			+ (sv.time.months < 10 ? "0" + sv.time.months.toString() : sv.time.months.toString())
			+ (sv.time.days < 10 ? "0" + sv.time.days.toString() : sv.time.days.toString())
			+ (sv.time.hours < 10 ? "0" + sv.time.hours.toString() : sv.time.hours.toString())
			+ (sv.time.minutes < 10 ? "0" + sv.time.minutes.toString() : sv.time.minutes.toString()));
};

Is this an OK function to have essentially running at the start of every passage?

The way I'm storing time would mean a lot more comparing between different elements to determine if an event has expired or not. With the timestamp, that comparison is just a simple number comparison. So, I think I win that way, since there's far less comparisons to do (each passage).

Another thing I was thinking of implementing is a sorted array with all these timstamps. The timestamp is just a number, 201709302100 for instance. And I'm pretty sure I can maintain an array of smallest to biggest numbers in it. For comparison, I have a while loop to compare the numbers until it encounters one that's bigger.

Time moves along discretely, with passage after passage. I just make the time up. A few minutes here, some hours there. So, between passages, a whole lot of time could have passed. And the topical stuff that had been present in the last passage aren't valid anymore. Hence the culling.

Or that's the idea I'm running with.

So, not only this timestamp function, but a while loop comparing a sorted event list. These two done at the start of each turn. A good way? Or too complex? Or is there a sneaky better way?

What stage would it be best to do these kinds of calculation in? predisplay? prerender? I guess it has a baring, since what's displayed in the main passage will be determined by what's still in the list. I think. So, opinion? Here's another timestamp, just so we're clear with what these look like: 201805070923. They're just numbers. I'm thinking... I add durations to them too, to elevate the timestamp to when an event can/should expire. But, that's something to do in a little bit.

===

With the event array, what if I had the events as objects? I figure at any given passage, more than just one event might trigger, so you'd have multiple events with the same "identifying" timestamp. To guard against this, maybe we put a tag in as well. Like this:

<<set $pcTopicalEventList.pushUnique({ timestamp: setup.makeTimestamp(), tag: "groundedTAG" })>>

So, I already know that pushUnique won't do its job with complex objects. Do I need to roll my own? Or is it better to just put these into an object container and use object methods to search, modify, delete? The main disadvantage with that is, that I'd need to run through the whole containing object to make sure I accounted for all the events, not just the first ones in an ordered array. So... what think?

===

So, I came up with some more code. And it seems to do what I was aiming for:

setup.setTimestamp = function () {
	var sv = State.variables;
	sv.time.timestamp = Number(""
			+ sv.time.years.toString()
			+ (sv.time.months < 10 ? "0"
			+ sv.time.months.toString() : sv.time.months.toString())
			+ (sv.time.days < 10 ? "0"
			+ sv.time.days.toString() : sv.time.days.toString())
			+ (sv.time.hours < 10 ? "0"
			+ sv.time.hours.toString() : sv.time.hours.toString())
			+ (sv.time.minutes < 10 ? "0"
			+ sv.time.minutes.toString() : sv.time.minutes.toString()));
};

setup.formatDuration = function () {
	var sv = State.variables;
	var	minutes	= sv.time.minutes,
		hours	= sv.time.hours,
		days	= sv.time.days,
		months	= sv.time.months,
		years	= sv.time.years;

	if (arguments[1] != null) {
		minutes = minutes + arguments[1];
		if (minutes > 59) {
			minutes = minutes - 59;
			hours++;
		}
	}
	if (arguments[0] != null) {
		hours = hours + arguments[0];
		if (hours > 23) {
			hours = hours - 23;
			days++;
		}
	}
	if (arguments[2] != null) {
		days = days + arguments[2];
		if (days > setup.calendar[years][months].days) {
			days = days - setup.calendar[years][months].days;
			months++;
		}
	}
	if (arguments[3] != null) {
		months = months + arguments[3];
		if (months > 12) {
			months = months - 12;
			years++;
		}
	}
	if (arguments[4] != null) {
		years = years + arguments[4];
	}

	return Number(""
			+ years.toString()
			+ (months < 10 ? "0" + months.toString() : months.toString())
			+ (days < 10 ? "0" + days.toString() : days.toString())
			+ (hours < 10 ? "0" + hours.toString() : hours.toString())
			+ (minutes < 10 ? "0" + minutes.toString() : minutes.toString()));
};

And this is what I'm using to test with:

<<timeSet 21 0 30>>
<<= $time.timeString + ", " + $time.dayString>>
<<run setup.setTimestamp()>>
<<= $time.timestamp>>
<<set $pcTopicalEventList.pushUnique({ duration: setup.formatDuration(0, 4, 2, 1, 1), tag: "anniversaryTAG" })>>
<<timeSet 9 23 7 5 2018>>
<<= $time.timeString + ", " + $time.dayString>>
<<run setup.setTimestamp()>>
<<= $time.timestamp>>
<<set $pcTopicalEventList.pushUnique( {timestamp: setup.formatDuration(), tag: "dunnoTAG" })>>

So, this produces strings like:

"pcTopicalEventList": [
      {
         "duration": 201811022104,
         "tag": "anniversaryTAG"
      },
      {
         "timestamp": 201805070923,
         "tag": "dunnoTAG"
      }
   ]

Now, assuming the time advances enough to make those future times less than the current timestamp, we can delete them or whatever. Am I on the right track with this? Or is it just some unnecessary busy work?

===

The actual format of the list object I was thinking of using for the events, is something like this:

	{
		dura:	0,
		tag:	"",
		level:	0,
		desc:	""
	}

Although, I'm not sure how to handle different levels. Like, say I use this to track damage. Then, a severe beating could be level 3 and have a duration for a couple of weeks. I'm still undecided if there should be residual levels after that level 3 damage has been recovered from. Or... and this is making things easy, if that three weeks includes the healing time that those injuries would require. I think I'm doing enough tracking work as it is. With new levels, I could change the desc and such. But, I'm not quite sure how this will go in practice. How big the periods will be. Anyway, I guess... I'll think about it a bit more.

2 Answers

0 votes
by (68.6k points)
selected by
 
Best answer

But, I haven't done that bit yet. I've just done the timestamp stuff. So code:

[…]

Is this an OK function to have essentially running at the start of every passage?

I'd probably have gone with a Date object for your time objects—because it's easy to get time systems wrong—but simply using an object with a bunch of date/time properties isn't completely bonkers or anything.

Your function could be a little more efficient though.  That said, since you're storing integers within the properties of your time objects, the best thing to do would simply be some arithmetic.  Since each field, save for the year, has a max width of two digits, just bump each successive field by two orders of magnitude (cumulative)—i.e. multiply each successive field by 100 (cumulative).

For example, here's what I'd suggest:

setup.makeTimestamp = function () {
	var svtime = State.variables.time;
	return svtime.years   * 100000000 /* ×100×100×100×100 */
		+  svtime.months  * 1000000   /* ×100×100×100 */
		+  svtime.days    * 10000     /* ×100×100 */
		+  svtime.hours   * 100       /* ×100 */
		+  svtime.minutes;
};

TIP: Since $time is an object, you can save yourself an extra property access each time you access it by caching it directly.  WARNING: You cannot do this with primitive types, if you need to alter their values, so it's safer to simply cache State.variables in most cases.

 

Another thing I was thinking of implementing is a sorted array with all these timstamps. The timestamp is just a number, 201709302100 for instance. And I'm pretty sure I can maintain an array of smallest to biggest numbers in it. For comparison, I have a while loop to compare the numbers until it encounters one that's bigger.

You very likely do not need to worry about sorting your event collection.  You would need to have a truly ridiculous numbers of events in the collection—I'm talking at least in the thousands—before you should need to worry about the per-turn processing being an issue.

I'd suggest not worrying about sorting for now.  You can always revisit it later if it does become an issue.

 

What stage would it be best to do these kinds of calculation in? predisplay? prerender?

You probably want to do this as soon as possible after the history is updated, so I'd probably go with a recurring predisplay task.

 

So, I already know that pushUnique won't do its job with complex objects.

You're confusing what you want, for it to somehow magically compare the contents of objects, with with it actually can do, which is compare the objects' references.  No completely generic method is going to do what you want.

You either need a custom solution or a generic one which allows you to specify your own predicate function.

Beyond that, it's unclear whether you should be handling conflicting events that way at all.  Even if you did want to silently block conflicting events, a search over the collection would probably be a better bet since you're likely to need to consult durations as well.

 

So, I came up with some more code. And it seems to do what I was aiming for:  […]

Your setup.formatDuration() method is a little naive and there's also the "just use arithmetic" issue I noted above.  I might suggest something like:

setup.toTimestamp = function (timeObj) {
	return timeObj.years   * 100000000 /* ×100×100×100×100 */
		+  timeObj.months  * 1000000   /* ×100×100×100 */
		+  timeObj.days    * 10000     /* ×100×100 */
		+  timeObj.hours   * 100       /* ×100 */
		+  timeObj.minutes;
};

setup.makeTimestamp = function () {
	return setup.toTimestamp(State.variables.time);
};

setup.setTimestamp = function () {
	var svtime = State.variables.time;
	svtime.timestamp = setup.toTimestamp(svtime);
};

setup.formatDuration = function (plusHours, plusMins, plusDays, plusMonths, plusYears) {
	var svtime  = State.variables.time;
	var minutes = svtime.minutes;
	var hours   = svtime.hours;
	var days    = svtime.days;
	var months  = svtime.months;
	var years   = svtime.years;

	/*
		Add time values.
	*/
	if (plusHours) {
		hours += plusHours;
	}
	if (plusMins) {
		minutes += plusMins;
	}
	if (plusDays) {
		days += plusDays;
	}
	if (plusMonths) {
		months += plusMonths;
	}
	if (plusYears) {
		years += plusYears;
	}

	/*
		Handle rollovers.
	*/
	if (minutes > 59) {
		hours += Math.trunc(minutes / 60);
		minutes %= 60;
	}
	if (hours > 23) {
		days += Math.trunc(hours / 24);
		hours %= 24;
	}
	// We have do an initial `months` rollover, since we're about to use
	// `months` and `years` to determine the monthly day count.
	if (months > 12) {
		years += Math.trunc(months / 12);
		months %= 12;
	}
	// We have to use a loop here, since the `days` rollover depends upon
	// `years` and `months`, which we're possibly altering as part of this
	// rollover.
	while (days > setup.calendar[years][months].days) {
		days -= setup.calendar[years][months].days;
		++months;

		// We have do the regular `months` rollover here, rather than after,
		// since we're using `months` and `years` to determine the monthly
		// day count.
		if (months > 12) {
			years += Math.trunc(months / 12);
			months %= 12;
		}
	}

	return setup.toTimestamp({
		minutes : minutes,
		hours   : hours,
		days    : days,
		months  : months,
		years   : years
	});
};

 

So... what think? Is this OK to be lugging around? I expect them to eventually be deleted. But there might be like five or maybe six for so similar objects that alert to different things.

It seems fine.  You'd need a lot more than five or six before you'd even need to begin to worry.

 

I haven't looked into how I search through the objects. But I did see an interesting discussion elsewhere making use of the jquery _.findWhere(list, properties) .

That's either Lodash or Underscore, not jQuery—you can tell because the object is _ (an underscore).  Neither are included with SugarCube, because you largely don't need them—the standard JavaScript library is much better than it used to be, and SugarCube's own additions fill in a lot of gaps.

Assuming an array of these event objects, I'd simply use the Array methods from the standard library and SugarCube's additions.  For example, <Array>.filter() would return all the events which matched the given predicate function.

by (68.6k points)
edited by

Your event handler is buggy.  You, generally, should never modify the array you're iterating over with any of the Array methods as it can cause abnormal behavior—e.g. you're changing the length, and possibly order, when deleting elements, thus some unprocessed elements may be skipped.

You can get around that by being a little creative, but I find that it's generally simpler and easier to reason about what you're doing by using a loop in these cases.  For example:

$(document).on(':passagestart', function (ev) {
	var svtime = State.variables.time;
	var svtel  = State.variables.pcTopicalEventList;

	// Iterate the topical events.
	for (var i = 0; i < svtel.length; ++i) {
		var ev = svtel[i];

		// Iterate the event's timestamps.
		for (var j = 0; j < ev.timestamp.length; ++j) {
			// Delete the timestamp, and associated description,
			// if it has expired.
			if (ev.timestamp[j] <= svtime.timestamp) {
				ev.timestamp.deleteAt(j);
				ev.desc.deleteAt(j);
				--j; // adjust `j` to account for the deletion
			}
		}

		// Delete the event, if all timestamps were deleted.
		if (ev.timestamp.length === 0) {
			svtel.deleteAt(i);
			--i; // adjust `i` to account for the deletion
		}
	}
});

 

[EDIT]  Something I've noticed you doing.

I'd suggest preferring the strict equality/identity operators (JS: ===, TS: is) over the lazy equality operators (JS: ==, TS: eq) in most cases, since the latter can introduce type coercion shenanigans.  The same recommendation goes for the strict inequality/nonidentity operators (JS: !==, TS: isnot) versus the lazy inequality operators (JS: !=, TS: neq).

As a personal anecdote, the only time I use the lazy operators is when I want to test for null or undefined at the same time, and I comment every such instance just to be safe.  For example, foo == null yields true when foo is either null or undefined.

by (1.3k points)

I see! Yeah, I'll keep that strict equality stuff in mind.

And thanks for the cleaned up code. I had wondered about the effects of deleting elements while iterating through it. Especially when deleting the entire object when still referencing it. That was pretty dodgy.

In other news, I've decided to make the event list something that can be used for more general stuff. Like dates dates to keep in mind. I haven't thought of all the uses. But, I can see that the system is general enough to do a lot of nice things.

So, I probably want to use the tag value more than I am doing. It's possible I'll make that a list too, and have multiple tags associated with each event. But, that's for later.

For right now, I wonder if there's some nice methods to separate string elements. Say I have a tag:

"pcBody_RedFace"

And I had a few of these kind of tagged events, each of them pertaining to some aspect to the PC's body. It'd be a lot more convenient to have this kind of conditional:

<<if $eventList[_i].tag === "pcBody">>
print it out
<</if>>

I suspect I could use regex expressions to cut it up. I haven't looked at those for a while. But, if I'm careful with my naming conventions, I guess I could have a whole range of tags that might pull certain events certain ways while going through the story.

I think... if I did utilize the event list like this, I'd probably have to do more checks as to what should happen when the object gets deleted. If it was an important date, then, perhaps I'd set a flag to suggest the time was up or something.

Anyway, thanks for this new version!

by (1.3k points)

I have another question.

Say I have this, for formatting:

blah, blah, blah

<<if $eventList.length > 0>>\
<<run $eventList.shuffle()>>\
<<for _i = 0; _i < $eventList.length; _i++>>
<<if $eventList[_i].tag === "pcBody_RedFace">>\
<<=$eventList[_i].desc[0]>>
<</if>>\
<</for>>\

<</if>>\
blah, blah, blah

This works perfectly, with or without an event. But, I'm curious. Why does the <<for>> loop get off with out needing a \ behind it? Why is it different than the others?

by (68.6k points)

For right now, I wonder if there's some nice methods to separate string elements. Say I have a tag:

"pcBody_RedFace"

If you're simply looking to see if a tag that starts with pcBody is present, then it depends on how you're storing the tag(s).

 

For a string with a single tag, the best thing would likely be the <String>.startsWith() method:

<<if $eventList[_i].tag.startsWith("pcBody")>>

Though a RegExp approach would also work.

If the string may contain multiple tags, separated by some delimiter, for example:

"pcBody_RedFace pcBody_BlueArms"

Then <RegExp>.test() is probably best, since you won't have to split the string:

<<if /\bpcBody/.test($eventList[_i].tag)>>

The <String>.includes() method could also be used here, but it would match the substring anywhere, not just at the start of one of the tags, so it isn't a perfect solution.

 

On the other hand.  If you store the tags within an array, for example:

["pcBody_RedFace", "pcBody_BlueArms"]

Then you're probably better off with <Array>.some() combined with <String>.startsWith().  For example:

// Predicate function inline.
<<if $eventList[_i].tags.some(function (tag) {
	return tag.startsWith("pcBody");
})>>


// Predicate function declared elsewhere (e.g. on setup).
setup.hasPcBody = function (tag) {
	return tag.startsWith("pcBody");
};

<<if $eventList[_i].tags.some(setup.hasPcBody)>>

 

 But, I'm curious. Why does the <<for>> loop get off with out needing a \ behind it? Why is it different than the others?

The <<for>> macro's payload has special handling of its leading and trailing line breaks to make it work better for the average user—who in my experience is fairly sloppy with whitespace control.

by (1.3k points)
Ah right. I've copied all those suggestions, in case I need to find them later, at some point. I'm happy to stay with a single string for now, since I haven't really thought of what other events will be using the system. But, I can imagine I'll get to some point where I'll need to think about that stuff.

Thanks a lot. You'd be a very good teacher, you know that?
0 votes
by (1.3k points)
edited by

Eh, I can't add more to my opening post. But, I wanted to show the layout of the object I think will best serve for this:

	{
		timestamp	:	201710022100,
		tag			:	"groundedTAG",
		level		:	3,
		desc1		:	"Almost normal again. Except it's straight home after school's out.",
		desc2		:	"Well, at least you can go out with your friends now. That's something.",
		desc3		:	"It's not fair! You didn't deserve to be fully grounded for SO LONG!",
		time		:	{ hours: 21, minutes: 0, days: 30, months: 9, years: 2017 },
		duration	:	{ hours: 0, minutes: 0, days: 2, months: 0, years: 0 }
	}

I figure, I will degrade the desc as levels of it expire. So, level 3 lasts for 2 days. And the message for that is presented somewhere for the player to see. After this two days, this expires, this example. But, EventUpdate will downgrade the level (and maybe delete desc3, since it's no longer needed), and create a new timestamp.

A few notes: I have to include the original time this event occurred to be able to accurately model the degradation of the effects. I figure, I can just double whatever is in the "duration" key. So, level2 becomes 4 days. Level 1 becomes 8 days. I'll figure out how I do that when I come up with the EventUpdate stuff.

So... what think? Is this OK to be lugging around? I expect them to eventually be deleted. But there might be like five or maybe six for so similar objects that alert to different things. I like the inclusion of the tag with it, because I can then check for things like this in passages. Reactions or whatever. I haven't looked into how I search through the objects. But I did see an interesting discussion elsewhere making use of the jquery _.findWhere(list, properties) . Maybe I'll look into that when I finalize things.

But... what think? This approach? Good/bad?

===

I had other ideas with the structure of the object. I was thinking of how I'd need to redesign the functions to account for post-event reprocessing. And it was looking to be a lot of work. So, instead, if figure we do all the processing a the point, and save the results within the object. Like so:

	{
		timestamp	:	[ 201710022100, 201710062100, 201710142100 ],
		tag			:	"groundedTAG",
		level		:	3,
		desc		:	[ "It's not fair! You didn't deserve to be fully grounded for SO LONG!", "Well, at least you can go out with your friends now. That's something.", "Almost normal again. Except it's straight home after school's out." ]
	}

Then, we can just use timestamp.[0] and desc.[0] (I think?), to get the level 3 details. One the expiry date is reached, those are deleted and the level made level 2. I think that's a much better approach.

===

Or an improvement even:

	{
		timestamp	:	[ 201710022100, 201710062100, 201710142100 ],
		tag		:	"groundedTAG",
		desc		:	[ "It's not fair! You didn't deserve to be fully grounded for SO LONG!", "Well, at least you can go out with your friends now. That's something.", "Almost normal again. Except it's straight home after school's out." ]
	}

Since we can workout the current level by how many elements there are in the arrays. timestamp.length or something?

===

It's a bit odd for an grounding to end at 9 PM like that. I don't think it's a bit deal. Since they could just be real sticklers. But, I'm also not sure I'll even have this grounding stuff in the game. It's just an example I came up with.

...