Skip to content

Instantly share code, notes, and snippets.

@roSievers
Last active August 29, 2015 14:08
Show Gist options
  • Save roSievers/32814166e3bbf440c192 to your computer and use it in GitHub Desktop.
Save roSievers/32814166e3bbf440c192 to your computer and use it in GitHub Desktop.
Bandboxing - Ripping apart the input code.
// This module takes care of the so called Bandbox. The component
// used to select several units at once by drawing a rectangle.
// In particular, it provides an event handling Object "bandboxing"
// used by handleInputBeforeGui in input.js.
// The Pyrogenesis engine can only import whole javascript modules.
// To prevent interference with other code this module is encapsulated
// in an object.
// First we create a new object, which can be used as an event processor
// via: .setHandleInputBeforeGui(bandboxing); on the eventProcessor.
var bandboxing = {
"channel" : "before",
"terminationCallback" : undefined,
"dragStart" : undefined, // This remembers, at which position the bandbox was started.
"bandbox" : undefined, // This is the Gui element, the visible part of the bandbox.
"x0" : undefined, // The top left corner of the bandbox, screen coordinates.
"y0" : undefined,
"x1" : undefined, // The bottom right corner.
"y1" : undefined,
};
// Filter by the supplied conditions, until one returns at least on element.
// A condition must accept an entity as parameter and return a boolean value.
bandboxing.iteratedEntityFilter = function (ents, conditions)
{
var preferredEnts = [];
for (var i = 0; i < conditions.length; i++)
{
preferredEnts = ents.filter(conditions[i]);
if (preferredEnts.length > 0) // filter was sucessfull
{
return preferredEnts;
}
}
return []; // no condition was matched at all.
}
// And so on ...
// This module takes care of the so called Bandbox. The component
// used to select several units at once by drawing a rectangle.
// In particular, it provides an event handling Object "bandboxing"
// used by handleInputBeforeGui in input.js.
// The Pyrogenesis engine can only import whole javascript modules.
// To prevent interference with other code this module is encapsulated
// in a closure. Thus "getBandboxHandler" is the only member visible to
// the outside.
var getBandboxHandler = function ()
{
var dragStart; // This remembers, at which position the bandbox was started.
var bandbox; // This is the Gui element, the visible part of the bandbox.
var x0, y0; // The top left corner of the bandbox, screen coordinates.
var x1, y1; // The bottom right corner.
// Filter by the supplied conditions, until one returns at least on element.
// A condition must accept an entity as parameter and return a boolean value.
function iteratedEntityFilter(ents, conditions)
{
var preferredEnts = [];
for (var i = 0; i < conditions.length; i++)
{
preferredEnts = ents.filter(conditions[i]);
if (preferredEnts.length > 0) // filter was sucessfull
{
return preferredEnts;
}
}
return []; // no condition was matched at all.
}
// Updates the bandbox object with new positions and visibility.
function updateBandboxPosition(event)
{
x0 = dragStart[0];
y0 = dragStart[1];
x1 = event.x;
y1 = event.y;
// normalize the orientation of the rectangle
if (x0 > x1) { var t = x0; x0 = x1; x1 = t; }
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; }
bandbox.size = [x0, y0, x1, y1].join(" ");
return false;
}
// This defines several conditions, by which units may be choosen.
function isUnit (entity)
{
var entState = GetEntityState(entity);
if (!entState) {return false;}
return hasClass(entState, "Unit");
}
function isDefensive (entity)
{
var entState = GetEntityState(entity);
if (!entState) {return false;}
return hasClass(entState, "Defensive");
}
function isNotSupport (entity)
{
var entState = GetEntityState(entity);
if (!entState) {return false;}
return hasClass(entState, "Unit") && !hasClass(entState, "Support");
}
function isIdle (entity)
{
var entState = GetEntityState(entity);
if (!entState) {return false;}
return hasClass(entState, "Unit") && entState.unitAI.isIdle;
}
function isAnything (entity)
{
return true;
}
// returns a list of filters to be used with the iterated entity filter
// the list is based of the hotkeys held down.
function unitFilterConfiguration()
{
var filters = unitFilterConfiguration.filters
if (Engine.HotkeyIsPressed("selection.milonly"))
return [isNotSupport];
if (Engine.HotkeyIsPressed("selection.idleonly"))
return [isIdle];
// By default, first look for units. If none are found, defensive
// buildings (e.g. towers) are considered. If there are no defensive
// structures either, anything goes.
return [isUnit, isDefensive, isAnything];
}
// When returnEntities is True, this won't behave as an event
// handling function. Instead, the entities selected will be returned.
function updateBandboxSelection(event, returnEntities)
{
var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID());
var preferredEntities = iteratedEntityFilter(ents, unitFilterConfiguration());
if (returnEntities)
{
return preferredEntities
}
g_Selection.setHighlightList(preferredEntities);
// TODO: If this funcion consumes any event which comes with a
// change of unitFilterConfiguration, then "." and other Hotkeys
// can be overloaded for this.
return false;
}
function updateBandbox(event)
{
var result = updateBandboxPosition(event);
return result || updateBandboxSelection(event);
}
function onMouseUp(event)
{
if (event.button == SDL_BUTTON_LEFT)
{
var ents = updateBandboxSelection(undefined, true);
// Update the list of selected units
if (Engine.HotkeyIsPressed("selection.add"))
{
g_Selection.addList(ents);
}
else if (Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.removeList(ents);
}
else
{
g_Selection.reset();
g_Selection.addList(ents);
}
bandboxing.terminate();
return true; // The event was consumed.
}
else if (event.button == SDL_BUTTON_RIGHT)
{
// Cancel selection
bandboxing.terminate();
return true;
}
return false;
}
// The following makes bandboxing into an eventHandler, as used by input.js
// First we create a new object, which can be used as an event processor
// preprocessing via: setHandleInputBeforeGui(bandboxing);
var bandboxing = {"channel":"before"};
var terminationCallback;
bandboxing.initialize = function (callback)
{
terminationCallback = callback;
bandbox = Engine.GetGUIObjectByName("bandbox");
if (!dragStart)
{
error("The bandbox wasn't properly initialized!");
error("The dragStart variable is not set.");
terminationCallback();
return;
}
// This updates the Bandbox. It will have a size of 0, which is wrong.
// However, at this stage the bandbox is tiny, thus the bug
// most likely won't have any impact.
updateBandboxPosition({"x":dragStart[0], "y":dragStart[1]});
bandbox.hidden = false;
};
bandboxing.handleEvent = bakeEventHandlingFunction({
"mousemotion" : updateBandbox,
"hotkeydown" : updateBandboxSelection,
"hotkeyup" : updateBandboxSelection,
"mousebuttonup" : onMouseUp
});
bandboxing.terminate = function ()
{
bandbox.hidden = true;
// Remove the bandbox hover highlighting
g_Selection.setHighlightList([]);
// delete the information about dragStart
dragStart = undefined;
terminationCallback();
}
// Any access to the bandboxing module must take it's route
// via this wrapper function.
function getBandbox(startingPoint)
{
if (dragStart)
{
warn("The bandbox wasn't properly terminated last time");
}
dragStart = startingPoint;
return bandboxing;
}
return getBandbox;
} (); // End of the bandboxingClosure.
// Rembember that we defined a function and now immediatly call it.
// This function creates a mutable event handler. It has a state
// which is used for the actuall event processing.
// A default state is implemented.
// This is not a complete state machine, as there are
// no transitions.
function stateBasedEventProcessor(defaultHandler)
{
// This is a closure.
var currentEventHandler;
var defaultEventHandler = defaultHandler;
// This function is passed to each eventHandler upon initialization.
// It must be called by the eventHandler when it terminates.
// Calling the terminationCallback does not terminate the event,
// it only removes it from the EventProcessor.
// As an optional argument, a new eventHandler can be supplied
// to replace the current one. This is useful, when several
// states for a system. (As is done with building placement)
function terminationCallback(newHandler)
{
currentEventHandler = undefined; // prevent calling terminate
if (!newHandler)
{
newHandler = defaultEventHandler
}
setEventHandler(newHandler);
}
function setEventHandler(handler)
{
if (currentEventHandler)
{
currentEventHandler.terminate();
}
currentEventHandler = handler;
handler.initialize(terminationCallback);
}
function setDefaultHandler(handler)
{
defaultEventHandler = handler;
}
// As an optional argument a channel may be supplied.
// This is necessary as a single state machine takes care of
// both preprocessing and afterGui processing.
function handleEvent(event, channel)
{
if (!currentEventHandler)
{
return false;
}
if (!channel)
{
return currentEventHandler.handleEvent(event);
}
if (channel == currentEventHandler.channel)
{
return currentEventHandler.handleEvent(event);
}
return false; // wrong channel
}
// Generates a new object and returns it.
return {
"setEventHandler" : setEventHandler,
"setDefaultHandler" : setDefaultHandler,
"handleEvent" : handleEvent
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment