From 32eb9f9d30bfdff4aa2ae5144152f8a61642ffed Mon Sep 17 00:00:00 2001
From: Svornost <11434-svornost@users.noreply.gitgud.io>
Date: Sat, 21 Mar 2020 17:12:16 -0700
Subject: [PATCH] Add a new pure JS random event framework and integrate it
 into the existing random event system.

---
 js/002-config/fc-js-init.js                   |   1 +
 src/events/jsRandomEvent.tw                   |   3 +
 src/events/randomEvent.js                     | 156 ++++++++++++++++++
 src/uncategorized/randomEventRoll.tw          |   4 +
 src/uncategorized/randomEventSelect.tw        |  16 +-
 src/uncategorized/randomIndividualEvent.tw    |   2 +-
 src/uncategorized/randomNonindividualEvent.tw |   2 +-
 7 files changed, 178 insertions(+), 6 deletions(-)
 create mode 100644 src/events/jsRandomEvent.tw
 create mode 100644 src/events/randomEvent.js

diff --git a/js/002-config/fc-js-init.js b/js/002-config/fc-js-init.js
index 2834aa7d3e7..3fa82b1595b 100644
--- a/js/002-config/fc-js-init.js
+++ b/js/002-config/fc-js-init.js
@@ -17,6 +17,7 @@ App.Encyclopedia = {};
 App.Encyclopedia.Entries = {};
 App.Entity = {};
 App.Entity.Utils = {};
+App.Events = {};
 App.MainView = {};
 App.UI = {};
 App.UI.DOM = {};
diff --git a/src/events/jsRandomEvent.tw b/src/events/jsRandomEvent.tw
new file mode 100644
index 00000000000..ea038bac347
--- /dev/null
+++ b/src/events/jsRandomEvent.tw
@@ -0,0 +1,3 @@
+:: JS Random Event [nobr]
+
+<<run html5passage(n => $event.execute(n))>>
diff --git a/src/events/randomEvent.js b/src/events/randomEvent.js
new file mode 100644
index 00000000000..b4a73c60b39
--- /dev/null
+++ b/src/events/randomEvent.js
@@ -0,0 +1,156 @@
+/** base class for class-based events */
+App.Events.BaseEvent = class BaseEvent {
+	/** build a new event
+	 *  parameters are necessary for serialization (so that saving with the event active will work correctly) and should not normally be used directly
+	 *  child classes should forward to this implementation */
+	constructor(actors, params) {
+		/** @member {Array<number>} actors - a list of IDs for the actors participating in this event. */
+		this.actors = actors || [];
+		/** @member {object} params - a set of parameters to pass to the event. */
+		this.params = params || {};
+	}
+
+	/** generate an array of zero or more predicates which must all be true in order for the event to be valid.
+	 *  lambda predicates may add properties to {@link App.Events.BaseEvent#params the params member} in order to pass information on to the event.
+	 *  child classes should implement this.
+	 * @returns {Array<Function>}
+	 */
+	eventPrerequisites() {
+		return [];
+	}
+
+	/** generate an array of zero or more arrays, each corresponding to an actor in the event, which contain zero or more predicates which must be satisfied by the actor.
+	 *  child classes should implement this.
+	 * @returns {Array<Array<Function>>}
+	 */
+	actorPrerequisites() {
+		return [];
+	}
+
+	/** run the event and attach DOM output to the event passage.
+	 *  child classes must implement this.
+	 * @param {DocumentFragment} node - Document fragment which event output should be attached to
+	 */
+	execute(node) {
+	}
+
+	/** clone the event (needed for serialization).
+	 *  default implementation should suffice for child classes */
+	clone() {
+		let c = {};
+		Reflect.setPrototypeOf(c, Reflect.getPrototypeOf(this));
+		deepAssign(c, this);
+		return c;
+	}
+
+	/** serialize the event instance so it persists through saves.
+	 *  default implementation should suffice for child classes assigned to App.Events */
+	toJSON() {
+		return JSON.reviveWrapper(`new App.Events.${this.constructor.name}(${JSON.stringify(this.actors)},${JSON.stringify(this.params)})`);
+	}
+
+	/** build the actual list of actors that will be involved in this event.
+	 *  default implementation should suffice for child classes with a fixed number of actors; may be overridden for events with variable actor count.
+	 * @param {App.Entity.SlaveState} firstActor - if non-null, the first actor should be this slave (fail if she is not qualified)
+	 * @returns {boolean} - return false if sufficient qualified actors could not be found (cancel the event)
+	 */
+	castActors(firstActor) {
+		const prereqs = this.actorPrerequisites();
+
+		let i = 0;
+		if (firstActor && prereqs.length > 0) {
+			if (prereqs[0].every(p => p(firstActor))) {
+				this.actors.push(firstActor.ID);
+			} else {
+				return false; // preselected first actor was not qualified
+			}
+			i = 1; // first actor is cast
+		}
+
+		for (; i < prereqs.length; ++i) {
+			const qualified = V.slaves.filter(s => !this.actors.includes(s.ID) && prereqs[i].every(p => p(s)));
+			if (qualified.empty) {
+				return false; // a required actor was not found
+			}
+			this.actors.push(qualified.pluck().ID);
+		}
+
+		return true; // all actors cast
+	}
+};
+
+/* This is a trivial event for use as an example. */
+App.Events.TestEvent = class TestEvent extends App.Events.BaseEvent {
+	constructor(actors, params) {
+		super(actors, params);
+	}
+
+	eventPrerequisites() {
+		return [];
+	}
+
+	actorPrerequisites() {
+		return [
+			[] // one actor, no requirements
+		];
+	}
+
+	execute(node) {
+		let [eventSlave] = this.actors.map(a => getSlave(a)); // mapped deconstruction of actors into local slave variables
+		node.appendChild(document.createTextNode(`This test event for ${eventSlave.slaveName} was successful.`));
+	}
+};
+
+/* Note that we use a much more strict delineation between individual and nonindividual events here than in the old event system.
+ * Individual events always trigger for the chosen event slave, and the first actor is always the event slave.
+ * Nonindividual events are not provided any event slave and should cast one themselves.
+ */
+
+/** get a list of possible individual event based on a given available main actor
+ * @param {App.Entity.SlaveState} slave
+ * @returns {Array<App.Events.BaseEvent>}
+ */
+App.Events.getIndividualEvents = function(slave) {
+	return [
+		// instantiate all possible random individual events here
+		new App.Events.TestEvent()
+	]
+	.filter(e => (e.eventPrerequisites().every(p => p()) && e.castActors(slave)));
+};
+
+/** get a list of possible nonindividual events
+ * @returns {Array<App.Events.BaseEvent>}
+ */
+App.Events.getNonindividualEvents = function() {
+	return [
+		// instantiate all possible random nonindividual events here
+	]
+	.filter(e => (e.eventPrerequisites().every(p => p()) && e.castActors(null)));
+};
+
+/* --- below here is a bunch of workaround crap because we have to somehow persist event selection through multiple twine passages. ---
+ * eventually all this should go away, and we should use just one simple passage for both selection and execution, so everything can be kept in object form instead of being continually serialized and deserialized.
+ * we need to be able to serialize/deserialize the active event anyway so that saves work right, so this mechanism just piggybacks on that capability so the event passages don't need to be reworked all at once
+ */
+
+/** get a stringified list of possible individual events as fake passage names - TODO: kill me */
+App.Events.getIndividualEventsPassageList = function(slave) {
+	const events = App.Events.getIndividualEvents(slave);
+	return events.map(e => `JSRE ${e.constructor.name}:${JSON.stringify(e.toJSON())}`);
+};
+
+/** get a stringified list of possible individual events as fake passage names - TODO: kill me */
+App.Events.getNonindividualEventsPassageList = function() {
+	const events = App.Events.getNonindividualEvents();
+	return events.map(e => `JSRE ${e.constructor.name}:${JSON.stringify(e.toJSON())}`);
+};
+
+/** execute a fake event passage from the embedded JSON - TODO: kill me */
+App.Events.setGlobalEventForPassageTransition = function(psg) {
+	V.event = JSON.parse(psg.slice(psg.indexOf(":") + 1));
+};
+
+/** strip the embedded JSON from the fake event passage so it can be read by a human being - TODO: kill me */
+App.Events.printEventPassage = function(psg) {
+	return psg.slice(0, psg.indexOf(":"));
+};
diff --git a/src/uncategorized/randomEventRoll.tw b/src/uncategorized/randomEventRoll.tw
index fd1ee7d70c5..43d7412525e 100644
--- a/src/uncategorized/randomEventRoll.tw
+++ b/src/uncategorized/randomEventRoll.tw
@@ -1,4 +1,8 @@
 :: random event roll
 
 <<set $goto = $events.random()>>
+<<if $goto.startsWith("JSRE")>>
+	<<run App.Events.setGlobalEventForPassageTransition($goto)>>
+	<<goto "JS Random Event">>
+<</if>>
 <<goto $goto>>
diff --git a/src/uncategorized/randomEventSelect.tw b/src/uncategorized/randomEventSelect.tw
index e935ed03b68..e1cb5f31aef 100644
--- a/src/uncategorized/randomEventSelect.tw
+++ b/src/uncategorized/randomEventSelect.tw
@@ -14,11 +14,19 @@
 ''A random event would have been selected from the following:''
 <<set $RESSeventIndex = 0>>
 <<for $i = 0; $i < $events.length; $i++>>
-	<br>[[$events[$i]]]
-	<<if $events[$i] == "RESS">>
-		&ndash; $RESSevent[$RESSeventIndex]
-		<<set $RESSeventIndex += 1>>
+<<capture $i>>
+	<br>
+	<<if $events[$i].startsWith("JSRE")>>
+		<<set _linkText = App.Events.printEventPassage($events[$i])>>
+		<<link _linkText "JS Random Event">><<run App.Events.setGlobalEventForPassageTransition($events[$i])>><</link>>
+	<<else>>
+		[[$events[$i]]]
+		<<if $events[$i] == "RESS">>
+			&ndash; $RESSevent[$RESSeventIndex]
+			<<set $RESSeventIndex += 1>>
+		<</if>>
 	<</if>>
+<</capture>>
 <</for>>
 <br><br>
 //RESS is an amalgamated Random Event, Single Slave that combines existing single slave random events//
diff --git a/src/uncategorized/randomIndividualEvent.tw b/src/uncategorized/randomIndividualEvent.tw
index 3982c48566f..5de395425e6 100644
--- a/src/uncategorized/randomIndividualEvent.tw
+++ b/src/uncategorized/randomIndividualEvent.tw
@@ -119,7 +119,7 @@
 	<</if>>
 
 	/* EVENT RANDOMIZATION */
-	<<set $events = populateEventArray()>>
+	<<set $events = populateEventArray().concat(App.Events.getIndividualEventsPassageList($eventSlave))>>
 
 	<<if $cheatMode == 1>>
 		<<goto "random event select">>
diff --git a/src/uncategorized/randomNonindividualEvent.tw b/src/uncategorized/randomNonindividualEvent.tw
index aa46228cf53..6a509930cf4 100644
--- a/src/uncategorized/randomNonindividualEvent.tw
+++ b/src/uncategorized/randomNonindividualEvent.tw
@@ -1468,7 +1468,7 @@
 		<<set _recruitEvents.shuffle()>>
 		<<set _recruitEvents.length = _maxRecruitNumber>>
 	<</if>>
-	<<set $events = $events.concat(_recruitEvents)>>
+	<<set $events = $events.concat(_recruitEvents).concat(App.Events.getNonindividualEventsPassageList())>>
 	<<if $cheatMode == 1>>
 		<<goto "random event select">>
 	<<else>>
-- 
GitLab