# 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"
|
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
|
- SETUP - COLORS
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
const SDL_BUTTON_LEFT = 1;
|
const SDL_BUTTON_LEFT = 1;
|
||||||
const SDL_BUTTON_MIDDLE = 2;
|
const SDL_BUTTON_MIDDLE = 2;
|
||||||
const SDL_BUTTON_RIGHT = 3;
|
const SDL_BUTTON_RIGHT = 3;
|
||||||
|
const SDLK_RSHIFT = 303;
|
||||||
|
const SDLK_LSHIFT = 304;
|
||||||
// TODO: these constants should be defined somewhere else instead, in
|
// TODO: these constants should be defined somewhere else instead, in
|
||||||
// case any other code wants to use them too
|
// case any other code wants to use them too
|
||||||
|
|
||||||
|
|
||||||
var INPUT_NORMAL = 0;
|
var INPUT_NORMAL = 0;
|
||||||
var INPUT_SELECTING = 1;
|
var INPUT_SELECTING = 1;
|
||||||
var INPUT_BANDBOXING = 2;
|
var INPUT_BANDBOXING = 2;
|
||||||
var INPUT_BUILDING_PLACEMENT = 3;
|
var INPUT_BUILDING_PLACEMENT = 3;
|
||||||
var INPUT_BUILDING_CLICK = 4;
|
var INPUT_BUILDING_CLICK = 4;
|
||||||
var INPUT_BUILDING_DRAG = 5;
|
var INPUT_BUILDING_DRAG = 5;
|
||||||
|
var INPUT_BATCHTRAINING = 6;
|
||||||
|
|
||||||
var inputState = INPUT_NORMAL;
|
var inputState = INPUT_NORMAL;
|
||||||
|
|
||||||
@ -21,6 +23,11 @@ var placementEntity;
|
|||||||
|
|
||||||
var mouseX = 0;
|
var mouseX = 0;
|
||||||
var mouseY = 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()
|
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
|
var dragStart; // used for remembering mouse coordinates at start of drag operations
|
||||||
|
|
||||||
function tryPlaceBuilding()
|
function tryPlaceBuilding()
|
||||||
@ -156,7 +161,8 @@ function tryPlaceBuilding()
|
|||||||
|
|
||||||
function handleInputBeforeGui(ev)
|
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)
|
switch (ev.type)
|
||||||
{
|
{
|
||||||
case "mousebuttonup":
|
case "mousebuttonup":
|
||||||
@ -165,6 +171,14 @@ function handleInputBeforeGui(ev)
|
|||||||
mouseX = ev.x;
|
mouseX = ev.x;
|
||||||
mouseY = ev.y;
|
mouseY = ev.y;
|
||||||
break;
|
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:
|
// State-machine processing:
|
||||||
@ -172,6 +186,9 @@ function handleInputBeforeGui(ev)
|
|||||||
// (This is for states which should override the normal GUI processing - events will
|
// (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
|
// be processed here before being passed on, and propagation will stop if this function
|
||||||
// returns true)
|
// 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)
|
switch (inputState)
|
||||||
{
|
{
|
||||||
@ -321,6 +338,18 @@ function handleInputBeforeGui(ev)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
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;
|
return false;
|
||||||
@ -459,9 +488,72 @@ function handleInputAfterGui(ev)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function testBuild(ent)
|
// Called by GUI when user clicks construction button
|
||||||
|
function startBuildingPlacement(buildEntType)
|
||||||
{
|
{
|
||||||
placementEntity = ent;
|
placementEntity = buildEntType;
|
||||||
placementAngle = defaultPlacementAngle;
|
placementAngle = defaultPlacementAngle;
|
||||||
inputState = INPUT_BUILDING_PLACEMENT;
|
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";
|
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,
|
// The unitSomethingPanel objects, which are displayed in a stack at the bottom of the screen,
|
||||||
// ordered with *lowest* first
|
// ordered with *lowest* first
|
||||||
var g_unitPanels = ["Stance", "Formation", "Construction", "Research", "Training", "Queue"];
|
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()
|
function updateUnitDisplay()
|
||||||
{
|
{
|
||||||
var detailsPanel = getGUIObjectByName("selectionDetails");
|
var detailsPanel = getGUIObjectByName("selectionDetails");
|
||||||
@ -147,83 +232,41 @@ function updateUnitDisplay()
|
|||||||
getGUIObjectByName("selectionDetailsGeneric").caption = template.name.generic;
|
getGUIObjectByName("selectionDetailsGeneric").caption = template.name.generic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getGUIObjectByName("selectionDetailsPlayer").caption = "Player " + entState.player; // TODO: get player name
|
||||||
|
|
||||||
getGUIObjectByName("selectionDetailsAttack").caption = damageTypesToText(entState.attack);
|
getGUIObjectByName("selectionDetailsAttack").caption = damageTypesToText(entState.attack);
|
||||||
getGUIObjectByName("selectionDetailsArmour").caption = damageTypesToText(entState.armour);
|
getGUIObjectByName("selectionDetailsArmour").caption = damageTypesToText(entState.armour);
|
||||||
|
|
||||||
var usedPanels = {};
|
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;
|
if (entState.attack) // TODO - this should be based on some AI properties
|
||||||
//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)
|
|
||||||
{
|
{
|
||||||
var button = getGUIObjectByName("unitConstructionButton["+i+"]");
|
//usedPanels["Stance"] = 1;
|
||||||
var icon = getGUIObjectByName("unitConstructionIcon["+i+"]");
|
//usedPanels["Formation"] = 1;
|
||||||
|
// (These are disabled since they're not implemented yet)
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
var numButtons = i;
|
else // TODO - this should be based on various other things
|
||||||
// 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("unitConstructionButton["+i+"]");
|
//usedPanels["Research"] = 1;
|
||||||
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
|
if (entState.buildEntities && entState.buildEntities.length)
|
||||||
for (i = numButtons; i < g_unitConstructionButtons; ++i)
|
setupUnitPanel("Construction", usedPanels, entState, entState.buildEntities, startBuildingPlacement);
|
||||||
getGUIObjectByName("unitConstructionButton["+i+"]").hidden = true;
|
|
||||||
g_unitConstructionButtons = numButtons;
|
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;
|
var offset = 0;
|
||||||
for each (var panelName in g_unitPanels)
|
for each (var panelName in g_unitPanels)
|
||||||
{
|
{
|
||||||
|
@ -137,7 +137,7 @@
|
|||||||
<object size="136 6 100% 100%">
|
<object size="136 6 100% 100%">
|
||||||
<object size="0 0 100% 20" name="selectionDetailsSpecific" type="text" font="prospero18b"/>
|
<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 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>
|
</object>
|
||||||
|
|
||||||
<!-- Attack stats -->
|
<!-- Attack stats -->
|
||||||
@ -212,12 +212,18 @@
|
|||||||
<object name="unitTrainingPanel"
|
<object name="unitTrainingPanel"
|
||||||
style="goldPanelFrilly"
|
style="goldPanelFrilly"
|
||||||
size="0 100%-56 100% 100%"
|
size="0 100%-56 100% 100%"
|
||||||
type="text"
|
type="image"
|
||||||
>
|
>
|
||||||
<object size="-5 -2 59 62" type="image" sprite="snIconSheetTab" tooltip_style="snToolTip"
|
<object size="-5 -2 59 62" type="image" sprite="snIconSheetTab" tooltip_style="snToolTip"
|
||||||
cell_id="2" tooltip="Training"/>
|
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>
|
||||||
|
|
||||||
<object name="unitQueuePanel"
|
<object name="unitQueuePanel"
|
||||||
@ -228,7 +234,15 @@
|
|||||||
<object size="-5 -2 59 62" type="image" sprite="snIconSheetTab" tooltip_style="snToolTip"
|
<object size="-5 -2 59 62" type="image" sprite="snIconSheetTab" tooltip_style="snToolTip"
|
||||||
cell_id="3" tooltip="Production queue"/>
|
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>
|
||||||
|
|
||||||
</object>
|
</object>
|
||||||
|
@ -38,10 +38,22 @@
|
|||||||
sprite="snIconPortrait"
|
sprite="snIconPortrait"
|
||||||
sprite_over="snIconPortraitOver"
|
sprite_over="snIconPortraitOver"
|
||||||
sprite_disabled="snIconPortraitDisabled"
|
sprite_disabled="snIconPortraitDisabled"
|
||||||
text_align="right"
|
tooltip_style="snToolTipBottom"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style name="iconButtonCount"
|
||||||
textcolor="255 255 255"
|
textcolor="255 255 255"
|
||||||
tooltip_style="snToolTip"
|
font="tahoma10"
|
||||||
tooltip="(TBA)"
|
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"
|
<style name="devCommandsText"
|
||||||
|
@ -39,7 +39,8 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
var ret = {
|
var ret = {
|
||||||
"template": template,
|
"id": ent,
|
||||||
|
"template": template
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
|
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
|
||||||
@ -73,6 +74,15 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
|
|||||||
ret.buildEntities = cmpBuilder.GetEntitiesList();
|
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);
|
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
|
||||||
if (cmpFoundation)
|
if (cmpFoundation)
|
||||||
{
|
{
|
||||||
@ -111,6 +121,9 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
|
|||||||
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
|
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
|
||||||
var template = cmpTempMan.GetTemplate(name);
|
var template = cmpTempMan.GetTemplate(name);
|
||||||
|
|
||||||
|
if (!template)
|
||||||
|
return null;
|
||||||
|
|
||||||
var ret = {};
|
var ret = {};
|
||||||
|
|
||||||
if (template.Identity)
|
if (template.Identity)
|
||||||
@ -125,13 +138,10 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
|
|||||||
if (template.Cost)
|
if (template.Cost)
|
||||||
{
|
{
|
||||||
ret.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.food) ret.cost.food = +template.Cost.Resources.food;
|
if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone;
|
||||||
if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood;
|
if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal;
|
||||||
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;
|
return ret;
|
||||||
|
@ -40,6 +40,12 @@ Player.prototype.AddResource = function(type, amount)
|
|||||||
this.resourceCount[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)
|
Player.prototype.TrySubtractResources = function(amounts)
|
||||||
{
|
{
|
||||||
// Check we can afford it all
|
// 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)
|
Timer.prototype.SetTimeout = function(ent, iid, funcname, time, data)
|
||||||
{
|
{
|
||||||
var id = ++this.id;
|
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/Health.js");
|
||||||
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
|
Engine.LoadComponentScript("interfaces/ResourceGatherer.js");
|
||||||
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
|
Engine.LoadComponentScript("interfaces/ResourceSupply.js");
|
||||||
|
Engine.LoadComponentScript("interfaces/TrainingQueue.js");
|
||||||
Engine.LoadComponentScript("GuiInterface.js");
|
Engine.LoadComponentScript("GuiInterface.js");
|
||||||
|
|
||||||
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
|
var cmp = ConstructComponent(SYSTEM_ENTITY, "GuiInterface");
|
||||||
@ -56,6 +57,7 @@ AddMock(10, IID_Builder, {
|
|||||||
|
|
||||||
var state = cmp.GetEntityState(-1, 10);
|
var state = cmp.GetEntityState(-1, 10);
|
||||||
TS_ASSERT_UNEVAL_EQUALS(state, {
|
TS_ASSERT_UNEVAL_EQUALS(state, {
|
||||||
|
id: 10,
|
||||||
template: "example",
|
template: "example",
|
||||||
position: {x:1, y:2, z:3},
|
position: {x:1, y:2, z:3},
|
||||||
hitpoints: 50,
|
hitpoints: 50,
|
||||||
|
@ -2,15 +2,17 @@ function ProcessCommand(player, cmd)
|
|||||||
{
|
{
|
||||||
// print("command: " + player + " " + uneval(cmd) + "\n");
|
// 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)
|
switch (cmd.type)
|
||||||
{
|
{
|
||||||
case "walk":
|
case "walk":
|
||||||
for each (var ent in cmd.entities)
|
for each (var ent in cmd.entities)
|
||||||
{
|
{
|
||||||
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
||||||
if (!ai)
|
if (ai)
|
||||||
continue;
|
ai.Walk(cmd.x, cmd.z);
|
||||||
ai.Walk(cmd.x, cmd.z);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -18,9 +20,8 @@ function ProcessCommand(player, cmd)
|
|||||||
for each (var ent in cmd.entities)
|
for each (var ent in cmd.entities)
|
||||||
{
|
{
|
||||||
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
||||||
if (!ai)
|
if (ai)
|
||||||
continue;
|
ai.Attack(cmd.target);
|
||||||
ai.Attack(cmd.target);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -29,9 +30,8 @@ function ProcessCommand(player, cmd)
|
|||||||
for each (var ent in cmd.entities)
|
for each (var ent in cmd.entities)
|
||||||
{
|
{
|
||||||
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
||||||
if (!ai)
|
if (ai)
|
||||||
continue;
|
ai.Repair(cmd.target);
|
||||||
ai.Repair(cmd.target);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -39,12 +39,23 @@ function ProcessCommand(player, cmd)
|
|||||||
for each (var ent in cmd.entities)
|
for each (var ent in cmd.entities)
|
||||||
{
|
{
|
||||||
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
var ai = Engine.QueryInterface(ent, IID_UnitAI);
|
||||||
if (!ai)
|
if (ai)
|
||||||
continue;
|
ai.Gather(cmd.target);
|
||||||
ai.Gather(cmd.target);
|
|
||||||
}
|
}
|
||||||
break;
|
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":
|
case "construct":
|
||||||
/*
|
/*
|
||||||
* Construction process:
|
* Construction process:
|
||||||
|
@ -53,3 +53,19 @@ CFixed_23_8 atan2_approx(CFixed_23_8 y, CFixed_23_8 x)
|
|||||||
else
|
else
|
||||||
return angle;
|
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) { }
|
CFixed() : value(0) { }
|
||||||
|
|
||||||
|
static CFixed Zero() { return CFixed(0); }
|
||||||
|
static CFixed Pi();
|
||||||
|
|
||||||
T GetInternalValue() const { return value; }
|
T GetInternalValue() const { return value; }
|
||||||
void SetInternalValue(T n) { value = n; }
|
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);
|
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
|
#endif // INCLUDED_FIXED
|
||||||
|
@ -71,6 +71,20 @@ public:
|
|||||||
return *this;
|
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.
|
* Returns the length of the vector.
|
||||||
|
@ -20,7 +20,10 @@
|
|||||||
#include "simulation2/system/Component.h"
|
#include "simulation2/system/Component.h"
|
||||||
#include "ICmpFootprint.h"
|
#include "ICmpFootprint.h"
|
||||||
|
|
||||||
|
#include "ICmpObstructionManager.h"
|
||||||
|
#include "ICmpPosition.h"
|
||||||
#include "simulation2/MessageTypes.h"
|
#include "simulation2/MessageTypes.h"
|
||||||
|
#include "maths/FixedVector2D.h"
|
||||||
|
|
||||||
class CCmpFootprint : public ICmpFootprint
|
class CCmpFootprint : public ICmpFootprint
|
||||||
{
|
{
|
||||||
@ -31,6 +34,8 @@ public:
|
|||||||
|
|
||||||
DEFAULT_COMPONENT_ALLOCATOR(Footprint)
|
DEFAULT_COMPONENT_ALLOCATOR(Footprint)
|
||||||
|
|
||||||
|
const CSimContext* m_Context;
|
||||||
|
|
||||||
EShape m_Shape;
|
EShape m_Shape;
|
||||||
CFixed_23_8 m_Size0; // width/radius
|
CFixed_23_8 m_Size0; // width/radius
|
||||||
CFixed_23_8 m_Size1; // height/radius
|
CFixed_23_8 m_Size1; // height/radius
|
||||||
@ -53,8 +58,10 @@ public:
|
|||||||
"</element>";
|
"</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())
|
if (paramNode.GetChild("Square").IsOk())
|
||||||
{
|
{
|
||||||
m_Shape = SQUARE;
|
m_Shape = SQUARE;
|
||||||
@ -96,6 +103,119 @@ public:
|
|||||||
size1 = m_Size1;
|
size1 = m_Size1;
|
||||||
height = m_Height;
|
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)
|
REGISTER_COMPONENT_TYPE(Footprint)
|
||||||
|
@ -22,4 +22,5 @@
|
|||||||
#include "simulation2/system/InterfaceScripted.h"
|
#include "simulation2/system/InterfaceScripted.h"
|
||||||
|
|
||||||
BEGIN_INTERFACE_WRAPPER(Footprint)
|
BEGIN_INTERFACE_WRAPPER(Footprint)
|
||||||
|
DEFINE_INTERFACE_METHOD_1("PickSpawnPoint", CFixedVector3D, ICmpFootprint, PickSpawnPoint, entity_id_t)
|
||||||
END_INTERFACE_WRAPPER(Footprint)
|
END_INTERFACE_WRAPPER(Footprint)
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
#include "simulation2/system/Interface.h"
|
#include "simulation2/system/Interface.h"
|
||||||
|
|
||||||
#include "simulation2/helpers/Position.h"
|
#include "simulation2/helpers/Position.h"
|
||||||
|
#include "maths/FixedVector3D.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Footprints - an approximation of the entity's shape, used for collision detection and for
|
* Footprints - an approximation of the entity's shape, used for collision detection and for
|
||||||
@ -37,8 +38,24 @@ public:
|
|||||||
SQUARE
|
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;
|
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)
|
DECLARE_INTERFACE_TYPE(Footprint)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ public:
|
|||||||
* @param x1 X coordinate of line's second point
|
* @param x1 X coordinate of line's second point
|
||||||
* @param z1 Z coordinate of line's second point
|
* @param z1 Z coordinate of line's second point
|
||||||
* @param r radius (half width) of line
|
* @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;
|
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 x X coordinate of center
|
||||||
* @param z Z coordinate of center
|
* @param z Z coordinate of center
|
||||||
* @param r radius of circle
|
* @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;
|
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 a angle of rotation (clockwise from +Z direction)
|
||||||
* @param w width (size along X axis)
|
* @param w width (size along X axis)
|
||||||
* @param h height (size along Z 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;
|
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