//-----------------------------------------------------------------------------
// Event Handling SubSystem
//-----------------------------------------------------------------------------
/**
* @class RezEvent
* @category Utilities
* @description Represents a game event response in the Rez game engine. Events are used to communicate
* the results of user interactions and specify what actions should be taken (scene changes, card plays,
* flash messages, etc.). Supports method chaining for building complex event responses.
*
* @example
* // Create an event that plays a card and shows a message
* return RezEvent.playCard("next_card").flash("Moving forward!").render();
*/
class RezEvent {
#params;
#flashMessages;
#cardId;
#sceneId;
#sceneChangeEvent;
#sceneInterludeEvent;
#sceneResumeEvent;
#renderEvent;
#errorMessage;
#afterHandlers;
/**
* @function constructor
* @memberof RezEvent#
* @description Creates a new event response with default values
*/
constructor() {
this.#params = {};
this.#flashMessages = [];
this.#cardId = null;
this.#sceneId = null;
this.#sceneChangeEvent = false;
this.#sceneInterludeEvent = false;
this.#sceneResumeEvent = false;
this.#renderEvent = false;
this.#errorMessage = null;
this.#afterHandlers = [];
}
/**
* @function params
* @memberof RezEvent#
* @returns {object} the parameters object associated with this event
*/
get params() {
return this.#params;
}
/**
* @function flashMessages
* @memberof RezEvent#
* @returns {string[]} array of flash messages to display
*/
get flashMessages() {
return this.#flashMessages;
}
/**
* @function cardId
* @memberof RezEvent#
* @returns {string|null} ID of the card to play, or null if no card action
*/
get cardId() {
return this.#cardId;
}
/**
* @function sceneId
* @memberof RezEvent#
* @returns {string|null} ID of the scene for scene transition, or null if no scene action
*/
get sceneId() {
return this.#sceneId;
}
/**
* @function sceneChangeEvent
* @memberof RezEvent#
* @returns {boolean} true if this event should trigger a scene change
*/
get sceneChangeEvent() {
return this.#sceneChangeEvent;
}
/**
* @function sceneInterludeEvent
* @memberof RezEvent#
* @returns {boolean} true if this event should trigger a scene interlude
*/
get sceneInterludeEvent() {
return this.#sceneInterludeEvent;
}
/**
* @function sceneResumeEvent
* @memberof RezEvent#
* @returns {boolean} true if this event should resume the previous scene
*/
get sceneResumeEvent() {
return this.#sceneResumeEvent;
}
/**
* @function renderEvent
* @memberof RezEvent#
* @returns {boolean} true if this event should trigger a view render
*/
get renderEvent() {
return this.#renderEvent;
}
/**
* @function errorMessage
* @memberof RezEvent#
* @returns {string|null} error message if this is an error event, or null
*/
get errorMessage() {
return this.#errorMessage;
}
/**
* @function setParam
* @memberof RezEvent#
* @param {string} name - parameter name
* @param {*} value - parameter value
* @returns {RezEvent} this event for method chaining
* @description Sets a single parameter on this event
*/
setParam(name, value ) {
this.#params[name] = value;
return this;
}
/**
* @function setParams
* @memberof RezEvent#
* @param {object} params - parameters object to set
* @returns {RezEvent} this event for method chaining
* @description Replaces the entire parameters object for this event
*/
setParams(params) {
this.#params = params;
return this;
}
/**
* @function hasFlash
* @memberof RezEvent#
* @returns {boolean} true if this event has flash messages to display
*/
get hasFlash() {
return this.#flashMessages.length > 0;
}
/**
* @function flash
* @memberof RezEvent#
* @param {string} message - message to display as a flash
* @param {string} kind - optional notification kind (default: "")
* @returns {RezEvent} this event for method chaining
* @description Adds a flash message to be displayed to the user
*/
flash(message, kind = "") {
this.#flashMessages.push({message: message, kind: kind});
return this;
}
/**
* @function shouldPlayCard
* @memberof RezEvent#
* @returns {boolean} true if this event should play a card
*/
get shouldPlayCard() {
return this.#cardId != null;
}
/**
* @function playCard
* @memberof RezEvent#
* @param {string} cardId - ID of the card to play
* @returns {RezEvent} this event for method chaining
* @description Sets this event to play the specified card
*/
playCard(cardId) {
this.#cardId = cardId;
return this;
}
/**
* @function shouldRender
* @memberof RezEvent#
* @returns {boolean} true if this event should trigger a view render
*/
get shouldRender() {
return this.#renderEvent;
}
/**
* @function render
* @memberof RezEvent#
* @returns {RezEvent} this event for method chaining
* @description Sets this event to trigger a view render
*/
render() {
this.#renderEvent = true;
return this;
}
/**
* @function shouldChangeScene
* @memberof RezEvent#
* @returns {boolean} true if this event should change to a new scene
*/
get shouldChangeScene() {
return this.#sceneChangeEvent;
}
/**
* @function sceneChange
* @memberof RezEvent#
* @param {string} sceneId - ID of the scene to change to
* @returns {RezEvent} this event for method chaining
* @description Sets this event to change to the specified scene
*/
sceneChange(sceneId) {
if(this.#sceneInterludeEvent || this.#sceneResumeEvent) {
throw new Error(`Attempt to sceneChange after sceneInterlude or sceneResume!`);
}
this.#sceneChangeEvent = true;
this.#sceneId = sceneId;
return this;
}
/**
* @function shouldInterludeScene
* @memberof RezEvent#
* @returns {boolean} true if this event should start a scene interlude
*/
get shouldInterludeScene() {
return this.#sceneInterludeEvent;
}
/**
* @function sceneInterlude
* @memberof RezEvent#
* @param {string} sceneId - ID of the scene to interlude with
* @returns {RezEvent} this event for method chaining
* @description Sets this event to start an interlude with the specified scene
*/
sceneInterlude(sceneId) {
if(this.#sceneChangeEvent || this.#sceneResumeEvent) {
throw new Error(`Attempt to sceneInterlude after sceneChange or sceneResume!`);
}
this.#sceneInterludeEvent = true;
this.#sceneId = sceneId;
return this;
}
/**
* @function shouldResumeScene
* @memberof RezEvent#
* @returns {boolean} true if this event should resume the previous scene
*/
get shouldResumeScene() {
return this.#sceneResumeEvent;
}
/**
* @function sceneResume
* @memberof RezEvent#
* @returns {RezEvent} this event for method chaining
* @description Sets this event to resume the previous scene from the scene stack
*/
sceneResume() {
if(this.#sceneChangeEvent || this.#sceneInterludeEvent) {
throw new Error(`Attempt to sceneResume after sceneChange or sceneInterlude!`);
}
this.#sceneResumeEvent = true;
return this;
}
/**
* @function isError
* @memberof RezEvent#
* @returns {boolean} true if this is an error event
*/
get isError() {
return this.#errorMessage != null;
}
/**
* @function error
* @memberof RezEvent#
* @param {string} message - error message
* @returns {RezEvent} this event for method chaining
* @description Sets this event as an error with the specified message
*/
error(message) {
this.#errorMessage = message;
return this;
}
/**
* @function after
* @memberof RezEvent#
* @param {function} callback - a parameterless callback the runs after processing
* @return {RezEvent} this event for method chaining
* @description Adds a callback to be run after event processing is finished
*/
after(callback) {
this.#afterHandlers.push(callback);
return this;
}
/**
* @function runAfter
* @memberof RezEvent#
* @description Run any after callbacks
*/
runAfterHandlers() {
this.#afterHandlers.forEach((callback) => {
callback();
});
}
/**
* @function noop
* @memberof RezEvent#
* @returns {RezEvent} this event for method chaining
* @description No-operation method for method chaining when no action is needed
*/
noop() {
return this;
}
/**
* @function built_in
* @memberof RezEvent
* @static
* @returns {RezEvent} a new empty event
* @description Creates a new built-in event with default values
*/
static built_in() {
return new RezEvent();
}
/**
* @function flash
* @memberof RezEvent
* @static
* @param {string} message - flash message to display
* @returns {RezEvent} a new event with the flash message
* @description Creates a new event that displays a flash message
*/
static flash(message, kind = "") {
return new RezEvent().flash(message, kind);
}
/**
* @function playCard
* @memberof RezEvent
* @static
* @param {string} cardId - ID of the card to play
* @returns {RezEvent} a new event that plays the specified card
* @description Creates a new event that plays the specified card
*/
static playCard(cardId) {
return new RezEvent().playCard(cardId);
}
/**
* @function render
* @memberof RezEvent
* @static
* @returns {RezEvent} a new event that triggers a render
* @description Creates a new event that triggers a view render
*/
static render() {
return new RezEvent().render();
}
/**
* @function setParam
* @memberof RezEvent
* @static
* @param {string} param - parameter name
* @param {*} value - parameter value
* @returns {RezEvent} a new event with the specified parameter
* @description Creates a new event with a single parameter set
*/
static setParam(param, value) {
return new RezEvent().setParam(param, value);
}
/**
* @function sceneChange
* @memberof RezEvent
* @static
* @param {string} sceneId - ID of the scene to change to
* @returns {RezEvent} a new event that changes to the specified scene
* @description Creates a new event that changes to the specified scene
*/
static sceneChange(sceneId) {
return new RezEvent().sceneChange(sceneId);
}
/**
* @function sceneInterlude
* @memberof RezEvent
* @static
* @param {string} sceneId - ID of the scene to interlude with
* @returns {RezEvent} a new event that starts an interlude
* @description Creates a new event that starts an interlude with the specified scene
*/
static sceneInterlude(sceneId) {
return new RezEvent().sceneInterlude(sceneId);
}
/**
* @function sceneResume
* @memberof RezEvent
* @static
* @returns {RezEvent} a new event that resumes the previous scene
* @description Creates a new event that resumes the previous scene from the stack
*/
static sceneResume() {
return new RezEvent().sceneResume();
}
/**
* @function noop
* @memberof RezEvent
* @static
* @returns {RezEvent} a new empty event
* @description Creates a new event that performs no action
*/
static noop() {
return new RezEvent();
}
/**
* @function after
* @memberof RezEvent
* @static
* @param {function} callback - a parameterless callback the runs after processing
* @returns {RezEvent} a new event that will run the callback after processing
* @description Creates a new event that runs the specified callback after being processed
*/
static after(callback) {
return new RezEvent().after(callback);
}
/**
* @function error
* @memberof RezEvent
* @static
* @param {string} message - error message
* @returns {RezEvent} a new error event
* @description Creates a new event that represents an error
*/
static error(message) {
return new RezEvent().error(message);
}
}
window.Rez.RezEvent = RezEvent;
/**
* @class RezEventProcessor
* @category Internal
* @description Processes events in the Rez game engine. Handles browser events (clicks, inputs, submits),
* custom game events, timer events, and system events. Routes events to appropriate handlers and manages
* the event lifecycle including system pre/post processing and undo manager integration.
*/
class RezEventProcessor {
#game;
/**
* @function constructor
* @memberof RezEventProcessor#
* @param {RezGame} game - the game instance this processor belongs to
* @description Creates a new event processor for the specified game
*/
constructor(game) {
this.#game = game;
}
/**
* @function game
* @memberof RezEventProcessor#
* @returns {RezGame} the game instance
*/
get game() {
return this.#game;
}
/**
* @function scene
* @memberof RezEventProcessor#
* @returns {RezScene} the current scene
*/
get scene() {
return this.#game.current_scene;
}
/**
* @function card
* @memberof RezEventProcessor#
* @returns {RezCard} the current card
*/
get card() {
return this.#game.current_scene.current_card;
}
/**
* @function dispatchResponse
* @memberof RezEventProcessor#
* @param {RezEvent} response - the event response to process
* @description Processes a RezEvent response by executing the actions it specifies:
* flash messages, scene changes/interludes/resumes, card plays, view renders, and error handling.
* @throws {Error} if the response is not a RezEvent instance
*/
dispatchResponse(response) {
if(response instanceof RezEvent) {
if(response.hasFlash) {
for(const message of response.flashMessages) {
this.game.addFlashMessage(message);
}
}
if(response.shouldChangeScene) {
this.game.startSceneWithId(response.sceneId, response.params);
} else if(response.shouldInterludeScene) {
this.game.interludeSceneWithId(response.sceneId, response.params);
} else if(response.shouldResumeScene) {
this.game.resumePrevScene();
}
if(response.shouldPlayCard) {
this.scene.playCardWithId(response.cardId, response.params);
}
if(response.shouldRender) {
this.game.updateView();
}
if(response.isError) {
console.log(`Error: ${response.errorMessage}`);
}
response.runAfterHandlers();
} else {
throw new Error("Event handlers must return a RezEvent object!");
}
}
/**
* @function beforeEventProcessing
* @memberof RezEventProcessor#
* @param {Event} evt - the browser event to pre-process
* @returns {Event} the processed event
* @description Runs the event through all enabled systems' before_event handlers.
* Each system can modify the event before it gets processed.
* @throws {Error} if any system handler doesn't return a valid event object
*/
beforeEventProcessing(evt) {
const systems = this.game.getEnabledSystems();
return systems.reduce((eventInProgress, system) => {
const handler = system.before_event;
const handledEvent = handler ? handler(system, eventInProgress) : eventInProgress;
if(typeof(handledEvent) === "undefined") {
throw new Error(`before_event handler of system |${system.id}| has not returned a valid evt object!`);
}
return handledEvent;
}, evt);
}
/**
* @function afterEventProcessing
* @memberof RezEventProcessor#
* @param {Event} evt - the original browser event
* @param {*} result - the result from event processing
* @returns {*} the processed result
* @description Runs the event result through all enabled systems' after_event handlers.
* Each system can modify the result after the event has been processed.
* @throws {Error} if any system handler doesn't return a valid result object
*/
afterEventProcessing(evt, result) {
const systems = this.game.getEnabledSystems();
return systems.reduce((intermediateResult, system) => {
const handler = system.after_event;
const handledResult = handler ? handler(system, evt, intermediateResult) : intermediateResult;
if(typeof(handledResult) === "undefined") {
throw new Error(`after_event handler of system |${system.id}| has not returned a valid result object!`);
}
return handledResult;
}, result);
}
/**
* @function raiseTimerEvent
* @memberof RezEventProcessor#
* @param {RezTimer} timer - the timer that fired
* @returns {*} the result of processing the timer event
* @description Creates and processes a custom timer event
*/
raiseTimerEvent(timer) {
const evt = new CustomEvent('timer', {detail: {timer: timer}});
return this.handleBrowserEvent(evt);
}
/**
* @function raiseKeyBindingEvent
* @memberof RezEventProcessor#
* @param {string} event_name - the name of the key binding event
* @returns {*} the result of processing the key binding event
* @description Creates and processes a custom key binding event
*/
raiseKeyBindingEvent(event_name) {
const evt = new CustomEvent("key_binding", {detail: {event_name: event_name}});
return this.handleBrowserEvent(evt);
}
/**
* @function raiseWindowEvent
* @memberof RezEventProcessor#
* @param {string} eventName - the name of the window event (e.g. "wheel", "resize")
* @param {Event} browserEvt - the native browser event
* @returns {*} the result of processing the window event
* @description Creates and processes a custom window event that wraps a native browser event
*/
raiseWindowEvent(eventName, browserEvt) {
const evt = new CustomEvent("window_event", {detail: {event_name: eventName, event: browserEvt}});
return this.handleBrowserEvent(evt);
}
/**
* @function isAutoUndoEvent
* @memberof RezEventProcessor#
* @param {Event} evt - the event to check
* @returns {boolean} true if this event type should trigger automatic undo recording
* @description Determines if an event should automatically record undo state
*/
isAutoUndoEvent(evt) {
const evtTypes = ["click", "input", "submit", "key_binding"];
return evtTypes.includes(evt.type);
}
/**
* @function handleBrowserEvent
* @memberof RezEventProcessor#
* @param {Event} evt - the browser event to handle
* @returns {*} the result of processing the event
* @description Main event handler that processes browser events. Handles undo recording,
* system pre/post processing, and routes events to specific handlers based on type.
*/
handleBrowserEvent(evt) {
if(this.isAutoUndoEvent(evt)) {
this.game.undoManager.startChange();
this.game.undoManager.recordViewChange(this.game.view.copy());
}
evt = this.beforeEventProcessing(evt);
let result;
if(evt.type === "click") {
result = this.handleBrowserClickEvent(evt);
} else if(evt.type === "input") {
result = this.handleBrowserInputEvent(evt);
} else if(evt.type === "submit") {
result = this.handleBrowserSubmitEvent(evt);
} else if(evt.type === "timer") {
result = this.handleTimerEvent(evt);
} else if(evt.type === "key_binding") {
result = this.handleKeyBindingEvent(evt);
} else if(evt.type === "window_event") {
result = this.handleWindowEvent(evt);
} else {
result = RezEvent.error(`No handler for event of type '${evt.type}'!`);
}
return this.afterEventProcessing(evt, result);
}
/**
* @function decodeEvent
* @memberof RezEventProcessor#
* @param {Event} evt - the browser event to decode
* @returns {Array} [eventName, target, params] decoded from the event's dataset
* @description Extracts event name, target, and parameters from an element's dataset attributes
*/
decodeEvent(evt) {
const {event, target, ...params} = evt.currentTarget.dataset;
if(event === undefined) {
throw new Error(`Improperly encoded event!`);
}
return [event.toLowerCase(), target, params];
}
/**
* @function handleTimerEvent
* @memberof RezEventProcessor#
* @param {CustomEvent} evt - the timer event with timer details
* @returns {*} the result of handling the timer event
* @description Handles timer events by routing them to custom event handlers
*/
handleTimerEvent(evt) {
const timer = evt.detail.timer;
const result = this.handleCustomEvent(timer.event, {timer: timer.id});
if(typeof(result) !== "object") {
return RezEvent.noop();
} else {
return result;
}
}
/**
* @function handleKeyBindingEvent
* @memberof RezEventProcessor#
* @param {CustomEvent} evt - the key binding event with event name details
* @returns {*} the result of handling the key binding event
* @description Handles key binding events by routing them to custom event handlers
*/
handleKeyBindingEvent(evt) {
const result = this.handleCustomEvent(evt.detail.event_name, {});
if(typeof(result) !== "object") {
return RezEvent.noop();
} else {
return result;
}
}
/**
* @function handleWindowEvent
* @memberof RezEventProcessor#
* @param {CustomEvent} evt - the window event with event name and browser event details
* @returns {*} the result of handling the window event
* @description Handles window events by routing them to custom event handlers with
* the "window_" prefix (e.g. "wheel" becomes "window_wheel")
*/
handleWindowEvent(evt) {
const eventName = evt.detail.event_name;
const browserEvt = evt.detail.event;
const handlerName = `window_${eventName}`;
const [receiver, handler] = this.getEventHandler(handlerName);
if(!handler) {
return RezEvent.noop();
}
if(RezBasicObject.game.$debug_events) {
console.log(`Routing event |${handlerName}| to |${receiver.id}|`);
}
return handler(receiver, {event: browserEvt});
}
/**
* @function handleBrowserClickEvent
* @memberof RezEventProcessor#
* @param {Event} evt - the click event
* @returns {*} the result of handling the click event
* @description Handles browser click events by decoding the event data and routing to appropriate handlers.
* Supports built-in event types (card, switch, interlude, resume) and custom events.
*/
handleBrowserClickEvent(evt) {
const [eventName, target, params] = this.decodeEvent(evt);
if(typeof(eventName) === "undefined") {
if(RezBasicObject.game.$debug_events) {
console.log("Received click event without an event name!");
}
return RezEvent.error("Received event without an event name. Cannot process it!");
}
if(eventName === "card") {
return this.handleCardEvent(target, params);
} else if(eventName === "switch") {
return this.handleSwitchEvent(target, params);
} else if(eventName === "interlude") {
return this.handleInterludeEvent(target, params);
} else if(eventName === "resume") {
return this.handleResumeEvent(params);
} else {
return this.handleCustomEvent(eventName, params);
}
}
/**
* @function getReceiverEventHandler
* @memberof RezEventProcessor#
* @param {*} receiver - the object to check for event handlers
* @param {string} eventname - the name of the event to find a handler for
* @returns {Function|null} the event handler function or null if not found
* @description Gets an event handler function from a receiver object
*/
getReceiverEventHandler(receiver, eventname) {
const handler = receiver.eventHandler(eventname);
if(handler && typeof(handler) === "function") {
return handler;
} else {
return null;
}
}
/**
* @function getEventHandler
* @memberof RezEventProcessor#
* @param {string} eventName - the name of the event to find a handler for
* @returns {Array} [receiver, handler] pair where receiver is the object that handles the event
* @description Finds an event handler by checking card, scene, and game in that order.
* Returns the first receiver that has a handler for the event.
*/
getEventHandler(eventName) {
const receivers = [this.card, this.scene, this.game];
const handlers = receivers.map((receiver) => [receiver, this.getReceiverEventHandler(receiver, eventName)]);
return handlers.find(([_receiver, handler]) => handler) ?? [null, null];
}
/**
* @function handleCustomEvent
* @memberof RezEventProcessor#
* @param {string} eventName - the name of the custom event
* @param {object} params - parameters to pass to the event handler
* @returns {RezEvent} the result of the event handler or an error event
* @description Handles custom events by finding and calling the appropriate event handler
*/
handleCustomEvent(eventName, params) {
const [receiver, handler] = this.getEventHandler(eventName);
if(!handler) {
return RezEvent.error(`Unable to find an event handler for |${eventName}|`);
} else {
if(RezBasicObject.game.$debug_events) {
console.log(`Routing event |${eventName}| to |${receiver.id}|`);
}
return handler(receiver, params);
}
}
/**
* @function handleCardEvent
* @memberof RezEventProcessor#
* @param {string} target - ID of the card to play
* @param {object} params - parameters to pass to the card
* @returns {RezEvent} event that plays the specified card
* @description Handles built-in card events that play a specific card
*/
handleCardEvent(target, params) {
if(RezBasicObject.game.$debug_events) {
console.log(`Handle card event: |${target}|`);
}
return RezEvent.playCard(target).setParams(params);
}
/**
* @function handleSwitchEvent
* @memberof RezEventProcessor#
* @param {string} target - ID of the scene to switch to
* @param {object} params - parameters to pass to the scene
* @returns {RezEvent} event that changes to the specified scene
* @description Handles built-in switch events that change to a new scene
*/
handleSwitchEvent(target, params) {
if(RezBasicObject.game.$debug_events) {
console.log(`Handle switch event: |${target}|`);
}
return RezEvent.sceneChange(target).setParams(params);
}
/**
* @function handleInterludeEvent
* @memberof RezEventProcessor#
* @param {string} target - ID of the scene to interlude with
* @param {object} params - parameters to pass to the scene
* @returns {RezEvent} event that starts an interlude with the specified scene
* @description Handles built-in interlude events that interrupt the current scene
*/
handleInterludeEvent(target, params) {
if(RezBasicObject.game.$debug_events) {
console.log(`Handle interlude event: |${target}|`);
}
return RezEvent.sceneInterlude(target).setParams(params);
}
/**
* @function handleResumeEvent
* @memberof RezEventProcessor#
* @param {object} params - parameters to pass to the resumed scene
* @returns {RezEvent} event that resumes the previous scene
* @description Handles built-in resume events that return to the previous scene
*/
handleResumeEvent(params) {
if(RezBasicObject.game.$debug_events) {
console.log("Handle resume event");
}
return RezEvent.sceneResume().setParams(params);
}
/**
* @function handleBrowserInputEvent
* @memberof RezEventProcessor#
* @param {Event} evt - the input event
* @returns {RezEvent} the result of the card's input event handler
* @description Handles browser input events by finding the card that contains the input element
* and calling its input event handler.
* @throws {Error} if the card container or card ID cannot be found
*/
handleBrowserInputEvent(evt) {
console.log("Handle input event");
// Try to find the containing card from the DOM (handles blocks/nested cards)
const cardDiv = evt.target.closest("div[data-card]");
if(cardDiv) {
const cardId = cardDiv.dataset.card;
const card = $(cardId);
const handler = this.getReceiverEventHandler(card, "input");
if(handler) {
return handler(card, {evt: evt});
}
}
// Fall back to bubbling mechanism (scene → game)
const [receiver, handler] = this.getEventHandler("input");
if(!handler) {
return RezEvent.noop();
}
return handler(receiver, {evt: evt});
}
/**
* @function handleBrowserSubmitEvent
* @memberof RezEventProcessor#
* @param {Event} evt - the submit event
* @returns {RezEvent} the result of the card's form event handler
* @description Handles browser form submit events by finding the card that contains the form
* and calling its event handler named after the form.
* @throws {Error} if the form name or card container cannot be found
*/
handleBrowserSubmitEvent(evt) {
const formName = evt.target.getAttribute("name");
if(!formName) {
throw new Error("Cannot get form name!");
}
// Try to find the containing card from the DOM (handles blocks/nested cards)
const cardDiv = evt.target.closest("div[data-card]");
if(cardDiv) {
const cardId = cardDiv.dataset.card;
const card = $(cardId);
const handler = this.getReceiverEventHandler(card, formName);
if(handler) {
return handler(card, {form: evt.target}) || RezEvent.noop();
}
}
// Fall back to bubbling mechanism (scene → game)
const [receiver, handler] = this.getEventHandler(formName);
if(!handler) {
return RezEvent.noop();
}
return handler(receiver, {form: evt.target}) || RezEvent.noop();
}
}
window.Rez.RezEventProcessor = RezEventProcessor;
Source