# Support training units in buildings.
Includes basic batch training (see #298). This was SVN commit r7469.
This commit is contained in:
parent
45368671c4
commit
08db7ebe13
@ -140,6 +140,17 @@
|
||||
textcolor="0 0 0"
|
||||
/>
|
||||
|
||||
<tooltip name="snToolTipBottom"
|
||||
anchor="bottom"
|
||||
buffer_zone="4"
|
||||
delay="500"
|
||||
font="tahoma12"
|
||||
maxwidth="300"
|
||||
offset="-4 -4"
|
||||
sprite="bkWhiteBorderBlack"
|
||||
textcolor="0 0 0"
|
||||
/>
|
||||
|
||||
<!--
|
||||
==========================================
|
||||
- SETUP - COLORS
|
||||
|
@ -1,16 +1,18 @@
|
||||
const SDL_BUTTON_LEFT = 1;
|
||||
const SDL_BUTTON_MIDDLE = 2;
|
||||
const SDL_BUTTON_RIGHT = 3;
|
||||
const SDLK_RSHIFT = 303;
|
||||
const SDLK_LSHIFT = 304;
|
||||
// TODO: these constants should be defined somewhere else instead, in
|
||||
// case any other code wants to use them too
|
||||
|
||||
|
||||
var INPUT_NORMAL = 0;
|
||||
var INPUT_SELECTING = 1;
|
||||
var INPUT_BANDBOXING = 2;
|
||||
var INPUT_BUILDING_PLACEMENT = 3;
|
||||
var INPUT_BUILDING_CLICK = 4;
|
||||
var INPUT_BUILDING_DRAG = 5;
|
||||
var INPUT_BATCHTRAINING = 6;
|
||||
|
||||
var inputState = INPUT_NORMAL;
|
||||
|
||||
@ -21,6 +23,11 @@ var placementEntity;
|
||||
|
||||
var mouseX = 0;
|
||||
var mouseY = 0;
|
||||
var specialKeyStates = {};
|
||||
specialKeyStates[SDLK_RSHIFT] = 0;
|
||||
specialKeyStates[SDLK_LSHIFT] = 0;
|
||||
// (TODO: maybe we should fix the hotkey system to be usable in this situation,
|
||||
// rather than hardcoding Shift into this code?)
|
||||
|
||||
function updateCursor()
|
||||
{
|
||||
@ -117,8 +124,6 @@ Selection methods: (not all currently implemented)
|
||||
|
||||
*/
|
||||
|
||||
// TODO: it'd probably be nice to have a better state-machine system
|
||||
|
||||
var dragStart; // used for remembering mouse coordinates at start of drag operations
|
||||
|
||||
function tryPlaceBuilding()
|
||||
@ -156,7 +161,8 @@ function tryPlaceBuilding()
|
||||
|
||||
function handleInputBeforeGui(ev)
|
||||
{
|
||||
// Capture mouse position so we can use it for displaying cursors
|
||||
// Capture mouse position so we can use it for displaying cursors,
|
||||
// and key states
|
||||
switch (ev.type)
|
||||
{
|
||||
case "mousebuttonup":
|
||||
@ -165,6 +171,14 @@ function handleInputBeforeGui(ev)
|
||||
mouseX = ev.x;
|
||||
mouseY = ev.y;
|
||||
break;
|
||||
case "keydown":
|
||||
if (ev.keysym.sym in specialKeyStates)
|
||||
specialKeyStates[ev.keysym.sym] = 1;
|
||||
break;
|
||||
case "keyup":
|
||||
if (ev.keysym.sym in specialKeyStates)
|
||||
specialKeyStates[ev.keysym.sym] = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// State-machine processing:
|
||||
@ -172,6 +186,9 @@ function handleInputBeforeGui(ev)
|
||||
// (This is for states which should override the normal GUI processing - events will
|
||||
// be processed here before being passed on, and propagation will stop if this function
|
||||
// returns true)
|
||||
//
|
||||
// TODO: it'd probably be nice to have a better state-machine system, with guaranteed
|
||||
// entry/exit functions, since this is a bit broken now
|
||||
|
||||
switch (inputState)
|
||||
{
|
||||
@ -321,6 +338,18 @@ function handleInputBeforeGui(ev)
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case INPUT_BATCHTRAINING:
|
||||
switch (ev.type)
|
||||
{
|
||||
case "keyup":
|
||||
if (ev.keysym.sym == SDLK_RSHIFT || ev.keysym.sym == SDLK_LSHIFT)
|
||||
{
|
||||
flushTrainingQueueBatch();
|
||||
inputState = INPUT_NORMAL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -459,9 +488,72 @@ function handleInputAfterGui(ev)
|
||||
return false;
|
||||
}
|
||||
|
||||
function testBuild(ent)
|
||||
// Called by GUI when user clicks construction button
|
||||
function startBuildingPlacement(buildEntType)
|
||||
{
|
||||
placementEntity = ent;
|
||||
placementEntity = buildEntType;
|
||||
placementAngle = defaultPlacementAngle;
|
||||
inputState = INPUT_BUILDING_PLACEMENT;
|
||||
}
|
||||
|
||||
// Batch training:
|
||||
// When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING
|
||||
// When the user releases shift, or clicks on a different training button, we create the batched units
|
||||
var batchTrainingEntity;
|
||||
var batchTrainingType;
|
||||
var batchTrainingCount;
|
||||
const batchIncrementSize = 5;
|
||||
|
||||
function flushTrainingQueueBatch()
|
||||
{
|
||||
Engine.PostNetworkCommand({"type": "train", "entity": batchTrainingEntity, "template": batchTrainingType, "count": batchTrainingCount});
|
||||
}
|
||||
|
||||
// Called by GUI when user clicks training button
|
||||
function addToTrainingQueue(entity, trainEntType)
|
||||
{
|
||||
if (specialKeyStates[SDLK_RSHIFT] || specialKeyStates[SDLK_LSHIFT])
|
||||
{
|
||||
if (inputState == INPUT_BATCHTRAINING)
|
||||
{
|
||||
// If we're already creating a batch of this unit, then just extend it
|
||||
if (batchTrainingEntity == entity && batchTrainingType == trainEntType)
|
||||
{
|
||||
batchTrainingCount += batchIncrementSize;
|
||||
return;
|
||||
}
|
||||
// Otherwise start a new one
|
||||
else
|
||||
{
|
||||
flushTrainingQueueBatch();
|
||||
// fall through to create the new batch
|
||||
}
|
||||
}
|
||||
inputState = INPUT_BATCHTRAINING;
|
||||
batchTrainingEntity = entity;
|
||||
batchTrainingType = trainEntType;
|
||||
batchTrainingCount = batchIncrementSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-batched - just create a single entity
|
||||
Engine.PostNetworkCommand({"type": "train", "entity": entity, "template": trainEntType, "count": 1});
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the number of units that will be present in a batch if the user clicks
|
||||
// the training button with shift down
|
||||
function getTrainingQueueBatchStatus(entity, trainEntType)
|
||||
{
|
||||
if (inputState == INPUT_BATCHTRAINING && batchTrainingEntity == entity && batchTrainingType == trainEntType)
|
||||
return [batchTrainingCount, batchIncrementSize];
|
||||
else
|
||||
return [0, batchIncrementSize];
|
||||
}
|
||||
|
||||
// Called by GUI when user clicks production queue item
|
||||
function removeFromTrainingQueue(entity, id)
|
||||
{
|
||||
Engine.PostNetworkCommand({"type": "stop-train", "entity": entity, "id": id});
|
||||
}
|
||||
|
||||
|
@ -92,12 +92,97 @@ function damageTypesToText(dmg)
|
||||
return dmg.hack + " Hack\n" + dmg.pierce + " Pierce\n" + dmg.crush + " Crush";
|
||||
}
|
||||
|
||||
var g_unitConstructionButtons = 0; // the number currently visible
|
||||
// The number of currently visible buttons (used to optimise showing/hiding)
|
||||
var g_unitPanelButtons = { "Construction": 0, "Training": 0, "Queue": 0 };
|
||||
|
||||
// The unitSomethingPanel objects, which are displayed in a stack at the bottom of the screen,
|
||||
// ordered with *lowest* first
|
||||
var g_unitPanels = ["Stance", "Formation", "Construction", "Research", "Training", "Queue"];
|
||||
|
||||
// Helper function for updateUnitDisplay
|
||||
function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
|
||||
{
|
||||
usedPanels[guiName] = 1;
|
||||
var i = 0;
|
||||
for each (var item in items)
|
||||
{
|
||||
var entType;
|
||||
if (guiName == "Queue")
|
||||
entType = item.template;
|
||||
else
|
||||
entType = item;
|
||||
|
||||
var button = getGUIObjectByName("unit"+guiName+"Button["+i+"]");
|
||||
var icon = getGUIObjectByName("unit"+guiName+"Icon["+i+"]");
|
||||
|
||||
var template = Engine.GuiInterfaceCall("GetTemplateData", entType);
|
||||
|
||||
var name;
|
||||
if (template.name.specific && template.name.generic)
|
||||
name = template.name.specific + " (" + template.name.generic + ")";
|
||||
else
|
||||
name = template.name.specific || template.name.generic || "???";
|
||||
|
||||
var tooltip;
|
||||
if (guiName == "Queue")
|
||||
{
|
||||
var progress = Math.round(item.progress*100) + "%";
|
||||
tooltip = name + " - " + progress;
|
||||
|
||||
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (item.count > 1 ? item.count : "");
|
||||
getGUIObjectByName("unit"+guiName+"Progress["+i+"]").caption = (item.progress ? progress : "");
|
||||
}
|
||||
else
|
||||
{
|
||||
tooltip = "[font=trebuchet14b]" + name + "[/font]";
|
||||
|
||||
if (template.cost)
|
||||
{
|
||||
var costs = [];
|
||||
if (template.cost.food) costs.push("[font=tahoma10b]Food:[/font] " + template.cost.food);
|
||||
if (template.cost.wood) costs.push("[font=tahoma10b]Wood:[/font] " + template.cost.wood);
|
||||
if (template.cost.metal) costs.push("[font=tahoma10b]Metal:[/font] " + template.cost.metal);
|
||||
if (template.cost.stone) costs.push("[font=tahoma10b]Stone:[/font] " + template.cost.stone);
|
||||
if (costs.length)
|
||||
tooltip += "\n" + costs.join(", ");
|
||||
}
|
||||
|
||||
if (guiName == "Training")
|
||||
{
|
||||
var [batchSize, batchIncrement] = getTrainingQueueBatchStatus(unitEntState.id, entType);
|
||||
tooltip += "\n[font=tahoma11]";
|
||||
if (batchSize) tooltip += "Training [font=tahoma12]" + batchSize + "[font=tahoma11] units; ";
|
||||
tooltip += "Shift-click to train [font=tahoma12]" + (batchSize+batchIncrement) + "[font=tahoma11] units[/font]";
|
||||
}
|
||||
}
|
||||
|
||||
button.hidden = false;
|
||||
button.tooltip = tooltip;
|
||||
button.onpress = (function(e) { return function() { callback(e) } })(item);
|
||||
// (need nested functions to get the closure right)
|
||||
|
||||
icon.sprite = "snPortraitSheetHele"; // TODO
|
||||
icon.cell_id = template.icon_cell;
|
||||
++i;
|
||||
}
|
||||
var numButtons = i;
|
||||
// Position the visible buttons
|
||||
// (TODO: if there's lots, maybe they should be squeezed together to fit)
|
||||
for (i = 0; i < numButtons; ++i)
|
||||
{
|
||||
var button = getGUIObjectByName("unit"+guiName+"Button["+i+"]");
|
||||
var size = button.size;
|
||||
size.left = 40*i;
|
||||
size.right = 40*i + size.bottom;
|
||||
button.size = size;
|
||||
}
|
||||
|
||||
// Hide any buttons we're no longer using
|
||||
for (i = numButtons; i < g_unitPanelButtons[guiName]; ++i)
|
||||
getGUIObjectByName("unit"+guiName+"Button["+i+"]").hidden = true;
|
||||
g_unitPanelButtons[guiName] = numButtons;
|
||||
}
|
||||
|
||||
function updateUnitDisplay()
|
||||
{
|
||||
var detailsPanel = getGUIObjectByName("selectionDetails");
|
||||
@ -147,83 +232,41 @@ function updateUnitDisplay()
|
||||
getGUIObjectByName("selectionDetailsGeneric").caption = template.name.generic;
|
||||
}
|
||||
|
||||
getGUIObjectByName("selectionDetailsPlayer").caption = "Player " + entState.player; // TODO: get player name
|
||||
|
||||
getGUIObjectByName("selectionDetailsAttack").caption = damageTypesToText(entState.attack);
|
||||
getGUIObjectByName("selectionDetailsArmour").caption = damageTypesToText(entState.armour);
|
||||
|
||||
var usedPanels = {};
|
||||
|
||||
if (entState.attack) // TODO - this should be based on some AI properties
|
||||
// If the selection is friendly units, add the command panels
|
||||
var player = Engine.GetPlayerID();
|
||||
if (entState.player == player || g_DevSettings.controlAll)
|
||||
{
|
||||
//usedPanels["Stance"] = 1;
|
||||
//usedPanels["Formation"] = 1;
|
||||
// (These are disabled since they're not implemented yet)
|
||||
}
|
||||
else // TODO - this should be based on various other things
|
||||
{
|
||||
//usedPanels["Queue"] = 1;
|
||||
//usedPanels["Training"] = 1;
|
||||
//usedPanels["Research"] = 1;
|
||||
}
|
||||
|
||||
// Set up the unit construction buttons
|
||||
// (TODO: abstract this to apply to the other button panels)
|
||||
if (entState.buildEntities && entState.buildEntities.length)
|
||||
{
|
||||
usedPanels["Construction"] = 1;
|
||||
var i = 0;
|
||||
for each (var build in entState.buildEntities)
|
||||
if (entState.attack) // TODO - this should be based on some AI properties
|
||||
{
|
||||
var button = getGUIObjectByName("unitConstructionButton["+i+"]");
|
||||
var icon = getGUIObjectByName("unitConstructionIcon["+i+"]");
|
||||
|
||||
var template = Engine.GuiInterfaceCall("GetTemplateData", build);
|
||||
|
||||
var name;
|
||||
if (template.name.specific && template.name.generic)
|
||||
name = template.name.specific + " (" + template.name.generic + ")";
|
||||
else
|
||||
name = template.name.specific || template.name.generic || "???";
|
||||
|
||||
var tooltip = "[font=trebuchet14b]" + name + "[/font]";
|
||||
|
||||
if (template.cost)
|
||||
{
|
||||
var costs = [];
|
||||
if (template.cost.food) costs.push("[font=tahoma10b]Food:[/font] " + template.cost.food);
|
||||
if (template.cost.wood) costs.push("[font=tahoma10b]Wood:[/font] " + template.cost.wood);
|
||||
if (template.cost.metal) costs.push("[font=tahoma10b]Metal:[/font] " + template.cost.metal);
|
||||
if (template.cost.stone) costs.push("[font=tahoma10b]Stone:[/font] " + template.cost.stone);
|
||||
if (costs.length)
|
||||
tooltip += "\n" + costs.join(", ");
|
||||
}
|
||||
|
||||
button.hidden = false;
|
||||
button.tooltip = tooltip;
|
||||
button.onpress = (function(b) { return function() { testBuild(b) } })(build);
|
||||
// (need nested functions to get the closure right)
|
||||
|
||||
icon.sprite = "snPortraitSheetHele";
|
||||
icon.cell_id = template.icon_cell;
|
||||
++i;
|
||||
//usedPanels["Stance"] = 1;
|
||||
//usedPanels["Formation"] = 1;
|
||||
// (These are disabled since they're not implemented yet)
|
||||
}
|
||||
var numButtons = i;
|
||||
// Position the visible buttons
|
||||
// (TODO: if there's lots, maybe they should be squeezed together to fit)
|
||||
for (i = 0; i < numButtons; ++i)
|
||||
else // TODO - this should be based on various other things
|
||||
{
|
||||
var button = getGUIObjectByName("unitConstructionButton["+i+"]");
|
||||
var size = button.size;
|
||||
size.left = 40*i;
|
||||
size.right = 40*i + size.bottom;
|
||||
button.size = size;
|
||||
//usedPanels["Research"] = 1;
|
||||
}
|
||||
|
||||
// Hide any buttons we're no longer using
|
||||
for (i = numButtons; i < g_unitConstructionButtons; ++i)
|
||||
getGUIObjectByName("unitConstructionButton["+i+"]").hidden = true;
|
||||
g_unitConstructionButtons = numButtons;
|
||||
if (entState.buildEntities && entState.buildEntities.length)
|
||||
setupUnitPanel("Construction", usedPanels, entState, entState.buildEntities, startBuildingPlacement);
|
||||
|
||||
if (entState.training && entState.training.entities.length)
|
||||
setupUnitPanel("Training", usedPanels, entState, entState.training.entities,
|
||||
function (trainEntType) { addToTrainingQueue(entState.id, trainEntType); } );
|
||||
|
||||
if (entState.training && entState.training.queue.length)
|
||||
setupUnitPanel("Queue", usedPanels, entState, entState.training.queue,
|
||||
function (item) { removeFromTrainingQueue(entState.id, item.id); } );
|
||||
}
|
||||
|
||||
// Lay out all the used panels in a stack at the bottom of the screen
|
||||
var offset = 0;
|
||||
for each (var panelName in g_unitPanels)
|
||||
{
|
||||
|
@ -137,7 +137,7 @@
|
||||
<object size="136 6 100% 100%">
|
||||
<object size="0 0 100% 20" name="selectionDetailsSpecific" type="text" font="prospero18b"/>
|
||||
<object size="0 20 100% 40" name="selectionDetailsGeneric" type="text" font="prospero16"/>
|
||||
<object size="0 40 100% 60" name="selectionDetailsPlayer" type="text" font="prospero16" textcolor="blue">Wijitmaker</object>
|
||||
<object size="0 40 100% 60" name="selectionDetailsPlayer" type="text" font="prospero16" textcolor="blue"/>
|
||||
</object>
|
||||
|
||||
<!-- Attack stats -->
|
||||
@ -212,12 +212,18 @@
|
||||
<object name="unitTrainingPanel"
|
||||
style="goldPanelFrilly"
|
||||
size="0 100%-56 100% 100%"
|
||||
type="text"
|
||||
type="image"
|
||||
>
|
||||
<object size="-5 -2 59 62" type="image" sprite="snIconSheetTab" tooltip_style="snToolTip"
|
||||
cell_id="2" tooltip="Training"/>
|
||||
|
||||
[training commands]
|
||||
<object size="59 10 100% 47">
|
||||
<repeat count="16">
|
||||
<object name="unitTrainingButton[n]" hidden="true" style="iconButton" type="button" size="0 0 37 37">
|
||||
<object name="unitTrainingIcon[n]" type="image" ghost="true" size="3 3 35 35"/>
|
||||
</object>
|
||||
</repeat>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
<object name="unitQueuePanel"
|
||||
@ -228,7 +234,15 @@
|
||||
<object size="-5 -2 59 62" type="image" sprite="snIconSheetTab" tooltip_style="snToolTip"
|
||||
cell_id="3" tooltip="Production queue"/>
|
||||
|
||||
[training/research queue]
|
||||
<object size="59 10 100% 47">
|
||||
<repeat count="16">
|
||||
<object name="unitQueueButton[n]" hidden="true" style="iconButton" type="button" size="0 0 37 37">
|
||||
<object name="unitQueueIcon[n]" ghost="true" type="image" size="3 3 35 35"/>
|
||||
<object name="unitQueueCount[n]" ghost="true" style="iconButtonCount" type="text"/>
|
||||
<object name="unitQueueProgress[n]" ghost="true" style="iconButtonProgress" type="text"/>
|
||||
</object>
|
||||
</repeat>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
</object>
|
||||
|
@ -38,10 +38,22 @@
|
||||
sprite="snIconPortrait"
|
||||
sprite_over="snIconPortraitOver"
|
||||
sprite_disabled="snIconPortraitDisabled"
|
||||
text_align="right"
|
||||
tooltip_style="snToolTipBottom"
|
||||
/>
|
||||
|
||||
<style name="iconButtonCount"
|
||||
textcolor="255 255 255"
|
||||
tooltip_style="snToolTip"
|
||||
tooltip="(TBA)"
|
||||
font="tahoma10"
|
||||
text_align="right"
|
||||
text_valign="top"
|
||||
buffer_zone="4"
|
||||
/>
|
||||
|
||||
<style name="iconButtonProgress"
|
||||
textcolor="255 255 255"
|
||||
font="tahoma14"
|
||||
text_align="center"
|
||||
text_valign="center"
|
||||
/>
|
||||
|
||||
<style name="devCommandsText"
|
||||
|
@ -39,7 +39,8 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
|
||||
return null;
|
||||
|
||||
var ret = {
|
||||
"template": template,
|
||||
"id": ent,
|
||||
"template": template
|
||||
}
|
||||
|
||||
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
|
||||
@ -73,6 +74,15 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
|
||||
ret.buildEntities = cmpBuilder.GetEntitiesList();
|
||||
}
|
||||
|
||||
var cmpTrainingQueue = Engine.QueryInterface(ent, IID_TrainingQueue);
|
||||
if (cmpTrainingQueue)
|
||||
{
|
||||
ret.training = {
|
||||
"entities": cmpTrainingQueue.GetEntitiesList(),
|
||||
"queue": cmpTrainingQueue.GetQueue(),
|
||||
};
|
||||
}
|
||||
|
||||
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
|
||||
if (cmpFoundation)
|
||||
{
|
||||
@ -111,6 +121,9 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
|
||||
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
|
||||
var template = cmpTempMan.GetTemplate(name);
|
||||
|
||||
if (!template)
|
||||
return null;
|
||||
|
||||
var ret = {};
|
||||
|
||||
if (template.Identity)
|
||||
@ -125,13 +138,10 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
|
||||
if (template.Cost)
|
||||
{
|
||||
ret.cost = {};
|
||||
if (template.Cost.Resources)
|
||||
{
|
||||
if (template.Cost.Resources.food) ret.cost.food = +template.Cost.Resources.food;
|
||||
if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood;
|
||||
if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone;
|
||||
if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal;
|
||||
}
|
||||
if (template.Cost.Resources.food) ret.cost.food = +template.Cost.Resources.food;
|
||||
if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood;
|
||||
if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone;
|
||||
if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
@ -40,6 +40,12 @@ Player.prototype.AddResource = function(type, amount)
|
||||
this.resourceCount[type] += (+amount);
|
||||
};
|
||||
|
||||
Player.prototype.AddResources = function(amounts)
|
||||
{
|
||||
for (var type in amounts)
|
||||
this.resourceCount[type] += (+amounts[type]);
|
||||
};
|
||||
|
||||
Player.prototype.TrySubtractResources = function(amounts)
|
||||
{
|
||||
// Check we can afford it all
|
||||
|
@ -40,6 +40,11 @@ Timer.prototype.OnUpdate = function(msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new timer, which will call the 'funcname' method with argument 'data'
|
||||
* on the 'iid' component of the 'ent' entity, after at least 'time' milliseconds.
|
||||
* Returns a non-zero id that can be passed to CancelTimer.
|
||||
*/
|
||||
Timer.prototype.SetTimeout = function(ent, iid, funcname, time, data)
|
||||
{
|
||||
var id = ++this.id;
|
||||
|
212
binaries/data/mods/public/simulation/components/TrainingQueue.js
Normal file
212
binaries/data/mods/public/simulation/components/TrainingQueue.js
Normal file
@ -0,0 +1,212 @@
|
||||
var g_ProgressInterval = 1000;
|
||||
|
||||
function TrainingQueue() {}
|
||||
|
||||
TrainingQueue.prototype.Schema =
|
||||
"<element name='Entities'>" +
|
||||
"<attribute name='datatype'><value>tokens</value></attribute>" +
|
||||
"<text/>" +
|
||||
"</element>";
|
||||
|
||||
TrainingQueue.prototype.Init = function()
|
||||
{
|
||||
this.nextID = 1;
|
||||
|
||||
this.queue = [];
|
||||
// Queue items are:
|
||||
// {
|
||||
// "id": 1,
|
||||
// "template": "units/example",
|
||||
// "count": 10,
|
||||
// "resources": { "wood": 100, ... },
|
||||
// "timeTotal": 15000, // msecs
|
||||
// "timeRemaining": 10000, // msecs
|
||||
// }
|
||||
|
||||
this.timer = undefined; // g_ProgressInterval msec timer, active while the queue is non-empty
|
||||
};
|
||||
|
||||
TrainingQueue.prototype.GetEntitiesList = function()
|
||||
{
|
||||
var string = this.template.Entities._string;
|
||||
|
||||
// Replace the "{civ}" codes with this entity's civ ID
|
||||
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
|
||||
if (cmpIdentity)
|
||||
string = string.replace(/\{civ\}/g, cmpIdentity.GetCiv());
|
||||
|
||||
return string.split(/\s+/);
|
||||
};
|
||||
|
||||
TrainingQueue.prototype.AddBatch = function(player, templateName, count)
|
||||
{
|
||||
// TODO: there should probably be a limit on the number of queued batches
|
||||
// TODO: there should be a way for the GUI to determine whether it's going
|
||||
// to be possible to add a batch (based on resource costs and length limits)
|
||||
|
||||
// Find the template data so we can determine the build costs
|
||||
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
|
||||
var template = cmpTempMan.GetTemplate(templateName);
|
||||
if (!template)
|
||||
return;
|
||||
|
||||
var timeMult = count; // TODO: we want some kind of discount for larger batches
|
||||
var costMult = count;
|
||||
|
||||
var time = timeMult * (template.Cost.BuildTime || 1);
|
||||
var costs = {};
|
||||
for each (var r in ["food", "wood", "stone", "metal"])
|
||||
{
|
||||
if (template.Cost.Resources[r])
|
||||
costs[r] = Math.floor(costMult * template.Cost.Resources[r]);
|
||||
else
|
||||
costs[r] = 0;
|
||||
}
|
||||
|
||||
// Find the player
|
||||
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
||||
var playerEnt = cmpPlayerMan.GetPlayerByID(player);
|
||||
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
|
||||
|
||||
if (!cmpPlayer.TrySubtractResources(costs))
|
||||
{
|
||||
// TODO: report error to player (they ran out of resources)
|
||||
return;
|
||||
}
|
||||
|
||||
this.queue.push({
|
||||
"id": this.nextID++,
|
||||
"template": templateName,
|
||||
"count": count,
|
||||
"resources": costs,
|
||||
"timeTotal": time*1000,
|
||||
"timeRemaining": time*1000,
|
||||
});
|
||||
|
||||
// If this is the first item in the queue, start the timer
|
||||
if (!this.timer)
|
||||
{
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
this.timer = cmpTimer.SetTimeout(this.entity, IID_TrainingQueue, "ProgressTimeout", g_ProgressInterval, {});
|
||||
}
|
||||
};
|
||||
|
||||
TrainingQueue.prototype.RemoveBatch = function(player, id)
|
||||
{
|
||||
for (var i = 0; i < this.queue.length; ++i)
|
||||
{
|
||||
var item = this.queue[i];
|
||||
if (item.id != id)
|
||||
continue;
|
||||
|
||||
// Now we've found the item to remove
|
||||
|
||||
// Find the player
|
||||
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
||||
var playerEnt = cmpPlayerMan.GetPlayerByID(player);
|
||||
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
|
||||
|
||||
// Refund the resource cost for this batch
|
||||
cmpPlayer.AddResources(item.resources);
|
||||
|
||||
// Remove from the queue
|
||||
// (We don't need to remove the timer - it'll expire if it discovers the queue is empty)
|
||||
this.queue.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TrainingQueue.prototype.GetQueue = function()
|
||||
{
|
||||
var out = [];
|
||||
for each (var item in this.queue)
|
||||
{
|
||||
out.push({
|
||||
"id": item.id,
|
||||
"template": item.template,
|
||||
"count": item.count,
|
||||
"progress": 1-(item.timeRemaining/item.timeTotal),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
TrainingQueue.prototype.OnDestroy = function()
|
||||
{
|
||||
// If the building is destroyed while it's got a large training queue,
|
||||
// you lose all the resources invested in that queue. That'll teach you
|
||||
// to be so reckless with your buildings.
|
||||
|
||||
if (this.timer)
|
||||
{
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
cmpTimer.CancelTimer(this.timer);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
TrainingQueue.prototype.SpawnUnits = function(templateName, count)
|
||||
{
|
||||
var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
|
||||
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
|
||||
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
||||
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var ent = Engine.AddEntity(templateName);
|
||||
|
||||
var pos = cmpFootprint.PickSpawnPoint(ent);
|
||||
if (pos.y < 0)
|
||||
{
|
||||
// Whoops, something went wrong (maybe there wasn't any space to spawn the unit).
|
||||
// What should we do here?
|
||||
// For now, just move the unit into the middle of the building where it'll probably get stuck
|
||||
pos = cmpPosition.GetPosition();
|
||||
}
|
||||
|
||||
var cmpNewPosition = Engine.QueryInterface(ent, IID_Position);
|
||||
cmpNewPosition.JumpTo(pos.x, pos.z);
|
||||
// TODO: what direction should they face in?
|
||||
|
||||
var cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
|
||||
cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
|
||||
|
||||
// TODO: move to rally points
|
||||
}
|
||||
};
|
||||
|
||||
TrainingQueue.prototype.ProgressTimeout = function(data)
|
||||
{
|
||||
// Allocate the 1000msecs to as many queue items as it takes
|
||||
// until we've used up all the time (so that we work accurately
|
||||
// with items that take fractions of a second)
|
||||
var time = g_ProgressInterval;
|
||||
|
||||
while (time > 0 && this.queue.length)
|
||||
{
|
||||
var item = this.queue[0];
|
||||
if (item.timeRemaining > time)
|
||||
{
|
||||
item.timeRemaining -= time;
|
||||
break;
|
||||
}
|
||||
|
||||
// This item is finished now
|
||||
time -= item.timeRemaining;
|
||||
this.SpawnUnits(item.template, item.count);
|
||||
this.queue.shift();
|
||||
}
|
||||
|
||||
// If the queue's empty, delete the timer, else repeat it
|
||||
if (this.queue.length == 0)
|
||||
{
|
||||
this.timer = undefined;
|
||||
}
|
||||
else
|
||||
{
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
this.timer = cmpTimer.SetTimeout(this.entity, IID_TrainingQueue, "ProgressTimeout", g_ProgressInterval, data);
|
||||
}
|
||||
}
|
||||
|
||||
Engine.RegisterComponentType(IID_TrainingQueue, "TrainingQueue", TrainingQueue);
|
@ -0,0 +1 @@
|
||||
Engine.RegisterInterface("TrainingQueue");
|
@ -5,6 +5,7 @@ Engine.LoadComponentScript("interfaces/Foundation.js");
|
||||
Engine.LoadComponentScript("interfaces/Health.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
|
||||
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
|
||||
Engine.LoadComponentScript("interfaces/TrainingQueue.js");
|
||||
Engine.LoadComponentScript("GuiInterface.js");
|
||||
|
||||
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
|
||||
@ -56,6 +57,7 @@ AddMock(10, IID_Builder, {
|
||||
|
||||
var state = cmp.GetEntityState(-1, 10);
|
||||
TS_ASSERT_UNEVAL_EQUALS(state, {
|
||||
id: 10,
|
||||
template: "example",
|
||||
position: {x:1, y:2, z:3},
|
||||
hitpoints: 50,
|
||||
|
@ -2,15 +2,17 @@ function ProcessCommand(player, cmd)
|
||||
{
|
||||
// print("command: " + player + " " + uneval(cmd) + "\n");
|
||||
|
||||
// TODO: all of this stuff needs to do checks for valid arguments
|
||||
// (e.g. make sure players own the units they're trying to use)
|
||||
|
||||
switch (cmd.type)
|
||||
{
|
||||
case "walk":
|
||||
for each (var ent in cmd.entities)
|
||||
{
|
||||
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
||||
if (!ai)
|
||||
continue;
|
||||
ai.Walk(cmd.x, cmd.z);
|
||||
if (ai)
|
||||
ai.Walk(cmd.x, cmd.z);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -18,9 +20,8 @@ function ProcessCommand(player, cmd)
|
||||
for each (var ent in cmd.entities)
|
||||
{
|
||||
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
||||
if (!ai)
|
||||
continue;
|
||||
ai.Attack(cmd.target);
|
||||
if (ai)
|
||||
ai.Attack(cmd.target);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -29,9 +30,8 @@ function ProcessCommand(player, cmd)
|
||||
for each (var ent in cmd.entities)
|
||||
{
|
||||
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
||||
if (!ai)
|
||||
continue;
|
||||
ai.Repair(cmd.target);
|
||||
if (ai)
|
||||
ai.Repair(cmd.target);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -39,12 +39,23 @@ function ProcessCommand(player, cmd)
|
||||
for each (var ent in cmd.entities)
|
||||
{
|
||||
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
||||
if (!ai)
|
||||
continue;
|
||||
ai.Gather(cmd.target);
|
||||
if (ai)
|
||||
ai.Gather(cmd.target);
|
||||
}
|
||||
break;
|
||||
|
||||
case "train":
|
||||
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
|
||||
if (queue)
|
||||
queue.AddBatch(player, cmd.template, +cmd.count);
|
||||
break;
|
||||
|
||||
case "stop-train":
|
||||
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
|
||||
if (queue)
|
||||
queue.RemoveBatch(player, cmd.id);
|
||||
break;
|
||||
|
||||
case "construct":
|
||||
/*
|
||||
* Construction process:
|
||||
|
@ -53,3 +53,19 @@ CFixed_23_8 atan2_approx(CFixed_23_8 y, CFixed_23_8 x)
|
||||
else
|
||||
return angle;
|
||||
}
|
||||
|
||||
template<>
|
||||
CFixed_23_8 CFixed_23_8::Pi()
|
||||
{
|
||||
return CFixed_23_8(804); // = pi * 256
|
||||
}
|
||||
|
||||
void sincos_approx(CFixed_23_8 a, CFixed_23_8& sin_out, CFixed_23_8& cos_out)
|
||||
{
|
||||
// XXX: mustn't use floating-point here - need a fixed-point emulation
|
||||
|
||||
// TODO: it's stupid doing sin/cos with 8-bit precision - we ought to have a CFixed_16_16 instead
|
||||
|
||||
sin_out = CFixed_23_8::FromDouble(sin(a.ToDouble()));
|
||||
cos_out = CFixed_23_8::FromDouble(cos(a.ToDouble()));
|
||||
}
|
||||
|
@ -48,6 +48,9 @@ public:
|
||||
|
||||
CFixed() : value(0) { }
|
||||
|
||||
static CFixed Zero() { return CFixed(0); }
|
||||
static CFixed Pi();
|
||||
|
||||
T GetInternalValue() const { return value; }
|
||||
void SetInternalValue(T n) { value = n; }
|
||||
|
||||
@ -165,4 +168,6 @@ typedef CFixed<i32, (i32)0x7fffffff, 32, 23, 8, 256> CFixed_23_8;
|
||||
*/
|
||||
CFixed_23_8 atan2_approx(CFixed_23_8 y, CFixed_23_8 x);
|
||||
|
||||
void sincos_approx(CFixed_23_8 a, CFixed_23_8& sin_out, CFixed_23_8& cos_out);
|
||||
|
||||
#endif // INCLUDED_FIXED
|
||||
|
@ -71,6 +71,20 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Scalar multiplication by an integer
|
||||
CFixedVector2D operator*(int n) const
|
||||
{
|
||||
return CFixedVector2D(X*n, Y*n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiply by a CFixed. Likely to overflow if both numbers are large,
|
||||
* so we use an ugly name instead of operator* to make it obvious.
|
||||
*/
|
||||
CFixedVector2D Multiply(fixed n) const
|
||||
{
|
||||
return CFixedVector2D(X.Multiply(n), Y.Multiply(n));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the vector.
|
||||
|
@ -20,7 +20,10 @@
|
||||
#include "simulation2/system/Component.h"
|
||||
#include "ICmpFootprint.h"
|
||||
|
||||
#include "ICmpObstructionManager.h"
|
||||
#include "ICmpPosition.h"
|
||||
#include "simulation2/MessageTypes.h"
|
||||
#include "maths/FixedVector2D.h"
|
||||
|
||||
class CCmpFootprint : public ICmpFootprint
|
||||
{
|
||||
@ -31,6 +34,8 @@ public:
|
||||
|
||||
DEFAULT_COMPONENT_ALLOCATOR(Footprint)
|
||||
|
||||
const CSimContext* m_Context;
|
||||
|
||||
EShape m_Shape;
|
||||
CFixed_23_8 m_Size0; // width/radius
|
||||
CFixed_23_8 m_Size1; // height/radius
|
||||
@ -53,8 +58,10 @@ public:
|
||||
"</element>";
|
||||
}
|
||||
|
||||
virtual void Init(const CSimContext& UNUSED(context), const CParamNode& paramNode)
|
||||
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
|
||||
{
|
||||
m_Context = &context;
|
||||
|
||||
if (paramNode.GetChild("Square").IsOk())
|
||||
{
|
||||
m_Shape = SQUARE;
|
||||
@ -96,6 +103,119 @@ public:
|
||||
size1 = m_Size1;
|
||||
height = m_Height;
|
||||
}
|
||||
|
||||
virtual CFixedVector3D PickSpawnPoint(entity_id_t spawned)
|
||||
{
|
||||
CFixedVector3D error(CFixed_23_8::FromInt(-1), CFixed_23_8::FromInt(-1), CFixed_23_8::FromInt(-1));
|
||||
|
||||
CmpPtr<ICmpPosition> cmpPosition(*m_Context, GetEntityId());
|
||||
if (cmpPosition.null() || !cmpPosition->IsInWorld())
|
||||
return error;
|
||||
|
||||
CmpPtr<ICmpObstructionManager> cmpObstructionManager(*m_Context, SYSTEM_ENTITY);
|
||||
if (cmpObstructionManager.null())
|
||||
return error;
|
||||
|
||||
// Always approximate the spawned entity as a circle, so we're orientation-independent
|
||||
CFixed_23_8 spawnedRadius;
|
||||
|
||||
CmpPtr<ICmpFootprint> cmpSpawnedFootprint(*m_Context, spawned);
|
||||
if (!cmpSpawnedFootprint.null())
|
||||
{
|
||||
EShape shape;
|
||||
CFixed_23_8 size0, size1, height;
|
||||
cmpSpawnedFootprint->GetShape(shape, size0, size1, height);
|
||||
if (shape == CIRCLE)
|
||||
spawnedRadius = size0;
|
||||
else
|
||||
spawnedRadius = std::max(size0, size1); // safe overapproximation of the correct sqrt((size0/2)^2 + (size1/2)^2)
|
||||
}
|
||||
else
|
||||
{
|
||||
// No footprint - weird but let's just pretend it's a point
|
||||
spawnedRadius = CFixed_23_8::FromInt(0);
|
||||
}
|
||||
|
||||
// The spawn point should be far enough from this footprint to fit the unit, plus a little gap
|
||||
CFixed_23_8 clearance = spawnedRadius + CFixed_23_8::FromInt(2);
|
||||
|
||||
CFixedVector3D initialPos = cmpPosition->GetPosition();
|
||||
entity_angle_t initialAngle = cmpPosition->GetRotation().Y;
|
||||
|
||||
if (m_Shape == CIRCLE)
|
||||
{
|
||||
CFixed_23_8 radius = m_Size0 + clearance;
|
||||
|
||||
// Try equally-spaced points around the circle, starting from the front and expanding outwards in alternating directions
|
||||
const ssize_t numPoints = 31;
|
||||
for (ssize_t i = 0; i < (numPoints+1)/2; i = (i > 0 ? -i : 1-i)) // [0, +1, -1, +2, -2, ... (np-1)/2, -(np-1)/2]
|
||||
{
|
||||
entity_angle_t angle = initialAngle + (entity_angle_t::Pi()*2).Multiply(entity_angle_t::FromInt(i)/numPoints);
|
||||
|
||||
CFixed_23_8 s, c;
|
||||
sincos_approx(angle, s, c);
|
||||
|
||||
CFixedVector3D pos (initialPos.X + s.Multiply(radius), CFixed_23_8::Zero(), initialPos.Z + c.Multiply(radius));
|
||||
|
||||
SkipTagObstructionFilter filter(spawned); // ignore collisions with the spawned entity
|
||||
if (cmpObstructionManager->TestCircle(filter, pos.X, pos.Z, spawnedRadius))
|
||||
return pos; // this position is okay, so return it
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CFixed_23_8 s, c;
|
||||
sincos_approx(initialAngle, s, c);
|
||||
|
||||
for (size_t edge = 0; edge < 4; ++edge)
|
||||
{
|
||||
// Try equally-spaced points along the edge, starting from the middle and expanding outwards in alternating directions
|
||||
const ssize_t numPoints = 9;
|
||||
|
||||
// Compute the direction and length of the current edge
|
||||
CFixedVector2D dir;
|
||||
CFixed_23_8 sx, sy;
|
||||
switch (edge)
|
||||
{
|
||||
case 0:
|
||||
dir = CFixedVector2D(c, -s);
|
||||
sx = m_Size0;
|
||||
sy = m_Size1;
|
||||
break;
|
||||
case 1:
|
||||
dir = CFixedVector2D(-s, -c);
|
||||
sx = m_Size1;
|
||||
sy = m_Size0;
|
||||
break;
|
||||
case 2:
|
||||
dir = CFixedVector2D(s, c);
|
||||
sx = m_Size1;
|
||||
sy = m_Size0;
|
||||
break;
|
||||
case 3:
|
||||
dir = CFixedVector2D(-c, s);
|
||||
sx = m_Size0;
|
||||
sy = m_Size1;
|
||||
break;
|
||||
}
|
||||
CFixedVector2D center;
|
||||
center.X = initialPos.X + (-dir.Y).Multiply(sy/2 + clearance);
|
||||
center.Y = initialPos.Z + dir.X.Multiply(sy/2 + clearance);
|
||||
dir = dir.Multiply((sx + clearance*2) / (numPoints-1));
|
||||
|
||||
for (ssize_t i = 0; i < (numPoints+1)/2; i = (i > 0 ? -i : 1-i)) // [0, +1, -1, +2, -2, ... (np-1)/2, -(np-1)/2]
|
||||
{
|
||||
CFixedVector2D pos (center + dir*i);
|
||||
|
||||
SkipTagObstructionFilter filter(spawned); // ignore collisions with the spawned entity
|
||||
if (cmpObstructionManager->TestCircle(filter, pos.X, pos.Y, spawnedRadius))
|
||||
return CFixedVector3D(pos.X, CFixed_23_8::Zero(), pos.Y); // this position is okay, so return it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
REGISTER_COMPONENT_TYPE(Footprint)
|
||||
|
@ -22,4 +22,5 @@
|
||||
#include "simulation2/system/InterfaceScripted.h"
|
||||
|
||||
BEGIN_INTERFACE_WRAPPER(Footprint)
|
||||
DEFINE_INTERFACE_METHOD_1("PickSpawnPoint", CFixedVector3D, ICmpFootprint, PickSpawnPoint, entity_id_t)
|
||||
END_INTERFACE_WRAPPER(Footprint)
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include "simulation2/system/Interface.h"
|
||||
|
||||
#include "simulation2/helpers/Position.h"
|
||||
#include "maths/FixedVector3D.h"
|
||||
|
||||
/**
|
||||
* Footprints - an approximation of the entity's shape, used for collision detection and for
|
||||
@ -37,8 +38,24 @@ public:
|
||||
SQUARE
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the shape of this footprint.
|
||||
* Shapes are horizontal circles or squares, extended vertically upwards to make cylinders or boxes.
|
||||
* @param[out] shape either CIRCLE or SQUARE
|
||||
* @param[out] size0 if CIRCLE then radius, else width (size in X axis)
|
||||
* @param[out] size1 if CIRCLE then radius, else depth (size in Z axis)
|
||||
* @param[out] height size in Y axis
|
||||
*/
|
||||
virtual void GetShape(EShape& shape, entity_pos_t& size0, entity_pos_t& size1, entity_pos_t& height) = 0;
|
||||
|
||||
/**
|
||||
* Pick a sensible position to place a newly-spawned entity near this footprint,
|
||||
* such that it won't be in an invalid (obstructed) location regardless of the spawned unit's
|
||||
* orientation.
|
||||
* @return the X and Z coordinates of the spawn point, with Y = 0; or the special value (-1, -1, -1) if there's no space
|
||||
*/
|
||||
virtual CFixedVector3D PickSpawnPoint(entity_id_t spawned) = 0;
|
||||
|
||||
DECLARE_INTERFACE_TYPE(Footprint)
|
||||
};
|
||||
|
||||
|
@ -92,7 +92,7 @@ public:
|
||||
* @param x1 X coordinate of line's second point
|
||||
* @param z1 Z coordinate of line's second point
|
||||
* @param r radius (half width) of line
|
||||
* @return true if there is a collision
|
||||
* @return false if there is a collision
|
||||
*/
|
||||
virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r) = 0;
|
||||
|
||||
@ -102,7 +102,7 @@ public:
|
||||
* @param x X coordinate of center
|
||||
* @param z Z coordinate of center
|
||||
* @param r radius of circle
|
||||
* @return true if there is a collision
|
||||
* @return false if there is a collision
|
||||
*/
|
||||
virtual bool TestCircle(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r) = 0;
|
||||
|
||||
@ -114,7 +114,7 @@ public:
|
||||
* @param a angle of rotation (clockwise from +Z direction)
|
||||
* @param w width (size along X axis)
|
||||
* @param h height (size along Z axis)
|
||||
* @return true if there is a collision
|
||||
* @return false if there is a collision
|
||||
*/
|
||||
virtual bool TestSquare(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h) = 0;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user