0 votes
asked 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
answered by (62.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.

commented by (1.3k points)

That's cool! Yeah, nice, pretty code. And it's pretty easy to follow this time. Which is neat.

The only thing I'm not really sure about his what you said about caching?

This bit:

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.

Is it something different than what I'm doing?

I do have this in setup, as defined in in StoryInit:

<<set setup.calendar = {
	2017: {
		9:  { name: "September", days: 30, start: "Friday"    },
		10: { name: "October"  , days: 31, start: "Sunday"    },
		11: { name: "November" , days: 30, start: "Wednesday" },
		12: { name: "December" , days: 31, start: "Friday"    }
	},
	2018: {
		1:  { name: "January"  , days: 31, start: "Monday"    },
		2:  { name: "February" , days: 28, start: "Thursday"  },
		3:  { name: "March"    , days: 31, start: "Thursday"  },
		4:  { name: "April"    , days: 30, start: "Sunday"    },
		5:  { name: "May"      , days: 31, start: "Tuesday"   },
		6:  { name: "June"     , days: 30, start: "Friday"    },
		7:  { name: "July"     , days: 31, start: "Sunday"    },
		8:  { name: "August"   , days: 31, start: "Wednesday" }
	}
}>>

And I thought it'd go well there, since that data won't be changing at all during the story. But... how do I cache things? I'm totally lost there.

Anyway, thanks a lot, tme. I like it!

commented by (1.3k points)

I came up with this. I think I'm almost ready to play with the predisplay/comparison stuff.

setup.createEvent = function (timestampArray, tagString, descArray) {
	this.timestamp = timestampArray;
	this.tag = tagString;
	this.desc = descArray;
}

setup.makeDurations = function (plusHours, plusMins, plusDays, plusMonths, plusYears, level) {
	var array = [];
	var _minutes, _hours, _days, _months, _years;

	for (var i = 0, acc = [0, 0, 0, 0, 0]; i < level; i++) {
		if (plusMins > 0) {
			_minutes = Math.pow(plusMins, i + 1) + acc[0];
			acc[0] = _minutes;
		}
		else {
			_minutes = 0;
		}
		if (plusHours > 0) {
			_hours = Math.pow(plusHours, i + 1) + acc[1];
			acc[1] = _hours;
		}
		else {
			_hours = 0;
		}
		if (plusDays > 0) {
			_days = Math.pow(plusDays, i + 1) + acc[2];
			acc[2] = _days;
		}
		else {
			_days = 0;
		}
		if (plusMonths > 0) {
			_months = Math.pow(plusMonths, i + 1) + acc[3];
			acc[3] = _months;
		}
		else {
			_months = 0;
		}
		if (plusYears > 0) {
			_years = Math.pow(plusYears, i + 1) + acc[4];
			acc[4] = _years;
		}
		else {
			_years = 0;
		}
		array.pushUnique(setup.formatDuration(_hours, _minutes, _days, _months, _years));
	}

	return array;
}

It was a little tricky getting the exponential increases. From 2 to 6 to 14. Which is basically: 2, 2*2 + 2, and then 2*2*2 + 6. But, I did get it in the end, by using an accumulator.

And this is how I use it within passages:

<<set $pcTopicalEventList.pushUnique(new setup.createEvent(setup.makeDurations(0, 0, 2, 0, 0, 3), "groundedTAG", [ "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." ]))>>

And this is the result:

"pcTopicalEventList": [
      {
         "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."
         ]
      }
   ]

So, I think we've finished at least this part of things.

commented by (62.6k points)

Is it something different than what I'm doing?

I do have this in setup, as defined in in StoryInit: […]

I'm unsure how you took what I said that far out of context, but that's not what I meant.  Let me try again.

When you do something like the following:

var sv = State.variables;

You're caching the value of the State object's variables property, which happens to be a reference to the current moment's story variables store.  We do things like that for two reasons:

  1. To make it shorter to type, which hopefully makes our code easier to read.
  2. To bypass having to dereference the entire property chain.  Each step on the chain, while inexpensive in terms of resources, is not totally free.

When accessing story variables, I generally suggest that people only cache State.variables itself, because attempting to cache values of the actual variables doesn't always work as people might hope.  For example, you cannot do this with a variable whose value is a primitive type if you need to alter its value.  This is a limitation of the JavaScript which underpins SugarCube.

In the case of $time from your examples, it was clear that it was an object.  As such, you can get away with caching it directly, as I did in my examples, because an object is a reference type—the value you get is a reference to the object, not the object itself.

For example, assume a setup like the following:

<<set $name to "Darth Helmet">>
<<set $char to {
	name     : "Darth Helmet",
	likes    : "Winning, Spaceballs, The Schwartz",
	location : "Spaceball One"
}>>

Attempting to access them via JavaScript by caching State.variables only:

var sv = State.variables;

/* Reading. */
… = sv.name;      // GOOD
… = sv.char.name; // GOOD

/* Writing. */
sv.name = …;      // GOOD
sv.char.name = …; // GOOD

Attempting to access them via JavaScript by caching the story variables too:

var svname = State.variables.name;
var svchar = State.variables.char;

/* READING */
… = svname;      // NOT GOOD, MAYBE NOT BAD [1]
… = svchar.name; // GOOD

/* WRITING */
svname = …;      // BAD [2]
svchar.name = …; // GOOD

/*
	[1] Has value of $name from time of caching,
	    but will not reflect changes made since.
	[2] Does not change value of $name.
 */

 

commented by (1.3k points)

I see! That's definitely the first I'm learning of that. But, good to know. I'll keep it in mind.

Hey, I had been doing this, when transitioning through passages:

[[Continue...|BladderTrouble][$time = "7:58 PM"]]

But, I've got a widget to do this (and other things). And I'd like to use it about the same way. Is there some kind of cheat to allow you to call/use widgets in that setter section?

Instead of that, this is what the widget looks like:

[[Continue...|BladderTrouble][<<setTime 19 58>>]]

But, this just throws up an error. I think I do want to call it at the time of passage transition, otherwise the time might...

Oh, I could use a postdisplay thing or something, right? But, I did like the clarity of including the time at the passage ends. So... what think?

commented by (1.3k points)

Oh right. I can just put it at the front of the destination passage, right? Like this:

:: BladderTrouble
<<setTime 19 58>>\

That works, and makes sense, I guess.

commented by (1.3k points)

The only problem I have with doing it this way, is this condition:

:: GoShiftDresser
<<setTime 22 24>>\

:: TryToResist
<<setTime 22 22>>\
<<set $statsWillfulness += 5>>\
@@.notify;[you feel a touch more emboldened]@@

[[Go shift the dresser|GoShiftDresser][$time = "10:26 PM"]]

See how the intermediate passage would have given a different time to the passage? How could I have the same effect? Some kind of conditional statement that looks at where you've come from? Give me a hint at what that would look like? Some kind of peek into history or something?

commented by (62.6k points)

No, you cannot use macros in a setter.  If you need to do that, use the <<link>> macro.  For example:

<<link [[Continue...|BladderTrouble]]>><<setTime 19 58>><</link>>

 

commented by (1.3k points)

Ah fair enough. That looks workable.

What I might do though is, just manually set the string, in situations where there's a conflict. I do like the look of [[]] markup.

[[Go shift the dresser|GoShiftDresser][$time.timeString = "10:24 PM"]]
[[Try... to resist!|TryToResist][$dresserDelay = true]]

:: GoShiftDresser

:: TryToResist
<<setTime 22 22>>\
<<set $statsWillfulness += 5>>\
@@.notify;[you feel a touch more emboldened]@@

[[Go shift the dresser|GoShiftDresser][$time.timeString = "10:26 PM"]]

 

commented by (1.3k points)
No, on second thought. I probably don't wanna be doing that. Since that doesn't actually set the time. So, I'll go with your suggestion.
commented by (62.6k points)

Well, you could create a function to set the time and call that in the setter.  For example:

[[Continue...|BladderTrouble][setup.setTime(19, 58)]]

The setter is evaluated as TwineScript, so any valid TwineScript is fair game.

commented by (1.3k points)
Actually, you're 100% right. I can't really use that idea of putting the time setting at the top of the passage. Since that will be too late for calculation stuff. So... thanks for suggesting the alternate. I'll go through and change them all.
commented by (1.3k points)
Oh, I see! Yeah, I'll go with that then, the function approach.
commented by (1.3k points)

I have a quite question. What happens if not all of these parameters is filled in javascript?

setup.setTime = function (hours, minutes, days, months, years) {

In my widget version, I had these kinds of checks:

		<<if $args[2] != null>>
			<<set $time.days = $args[2]>>

But, is that necessary in JS? It's no real trouble for me to write these kinds of checks again, but I'm not really sure how JS offers in that situation.

commented by (62.6k points)

TwineScript is simply a thin veneer of sugar on top of JavaScript, so if something is required in one, it's generally required in both.

In both cases, it's probably safer to check against null or undefined, which is what a lazy equality check against null actually does.

That said, you could probably use a simple truthy check for these.  For example, to check each for truthiness—which for numbers means not: NaN, 0, null, or undefined:

<<if $args[2]>>
if (hours) {

 

commented by (1.3k points)

Hey, I'm getting an odd error message when it attempts to pass through certain passages. It's just two, and they have a common thing with them.

Here's the error:

Error: SyntaxError: "0"-prefixed octal literals and octal escape sequences are deprecated; for octal literals use the "0o" prefix instead.

And theses are the passage links the error halts things at:

[[blah|blah][setup.setTime(20, 01)]]

and

[[Continue...|OnlinePlay][$Bottle = true;setup.setTime(22, 07)]]

So... it's getting caught up on the minute notation, right? Where does that error come from? Sugarcube? Or the browser I'm using? I'm using firefox to test, since it's the least overloaded with tabs browser I have. But, I just tried it with Yandex and it doesn't even come up with a message.

So... what to do? If I comment out that 01/07 stuff, the links click through as expected.

My code:

setup.setTime = function (hours, minutes, days, months, years) {
	var svtime = State.variables.time;
	svtime.hours = hours;
	svtime.minutes = minutes;
	if (days) {
		svtime.days = days;
	}
	if (months) {
		svtime.months = months;
	}
	if (years) {
		svtime.years = years;
	}
	if (hours - 12 >= 0 ) {
		svtime.timeString = "" + (hours - 12) + ":"
				+ (minutes < 10 ? "0" + minutes : minutes) + " PM";
	}
	else {
		svtime.timeString = "" + hours + ":"
				+ (minutes < 10 ? "0" + minutes : minutes) + " AM";
	}
	svtime.dayString = setup.getDayString();
	svtime.monthString = setup.calendar[svtime.years][svtime.months].name;
}

I tested all the other passages, but those two were the only ones that had minutes starting with a zero.

commented by (1.3k points)
Oh I see! I'm literally putting an octal in, when I use double digits. I should just use single. That was a pretty obvious mistake.
commented by (62.6k points)
Correct.  JavaScript number literals starting with a zero used to signify that the literal was in octal (base 8).

The format of literals was changed somewhat recently, but even current implementations still recognize the old octal form (and not all of them will complain about seeing it).

It's best to avoid leading zeros on number literals, unless you really want to use a non-decimal base.
commented by (1.3k points)
That setup.setTime() worked out great, actually. Not only do the passages look a lot cleaner now, I figured I'd put setup.setTimestamp() at the end of it. Since I'm setting time on every passage transition, I'm effectively getting a new timestamp set then as well. So, with this, I guess the original question really has been answered.

Now the only predisplay tasks I need to address are the ones comparing event timestamps with the current time. I'll work on that now, I guess.
commented by (1.3k points)

And... I think I've done it:

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

	svtel.forEach(function (obj) {
		for (var i = 0; i < obj.timestamp.length; i++) {
			if (obj.timestamp[i] <= svtime.timestamp) {
				obj.timestamp.splice(i, 1);
				obj.desc.splice(i, 1);
			}
		}
		if (obj.timestamp.length == 0) {
			idx = svtel.indexOf(obj);
			svtel.splice(idx, 1);
		}
	});
});

I think this is all I need to do, since it tidies up the list when timestamps expire. I've tested it very simply, and it behaves as expected.

I plug in these:

<<set $pcTopicalEventList.pushUnique(new setup.createEvent(setup.makeDurations(0, 1, 0, 0, 0, 3), "groundedTAG", [ "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." ]))>>
<<set $pcTopicalEventList.pushUnique(new setup.createEvent(setup.makeDurations(0, 0, 2, 0, 0, 3), "grounded2TAG", [ "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." ]))>>

And I test it with this:

<<if $pcTopicalEventList.length > 0>>\
<<for _i = 0; _i < $pcTopicalEventList.length; _i++>>\
<<=$pcTopicalEventList[_i].desc[0]>>
<</for>>\
<</if>>\

Now, I guess it's just a matter of making it all look nice.

Is there anything I'm overlooking? Or does it all seem pretty right?

commented by (62.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.

commented 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!

commented 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?

commented by (62.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.

commented 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
answered 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.

Welcome to Twine Q&A, where you can ask questions and receive answers from other members of the community.

You can also find hints and information on Twine on the official wiki and the old forums archive.

See a spam question? Flag it instead of downvoting. A question flagged enough times will automatically be hidden while moderators review it.
...