1
1
forked from 0ad/0ad

# Support training units in buildings.

Includes basic batch training (see #298).

This was SVN commit r7469.
This commit is contained in:
Ykkrosh 2010-04-19 19:47:23 +00:00
parent 45368671c4
commit 08db7ebe13
19 changed files with 694 additions and 102 deletions

View File

@ -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

View File

@ -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});
}

View File

@ -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)
{

View File

@ -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>

View File

@ -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"

View File

@ -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;

View File

@ -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

View File

@ -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;

View 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);

View File

@ -0,0 +1 @@
Engine.RegisterInterface("TrainingQueue");

View File

@ -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,

View File

@ -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:

View File

@ -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()));
}

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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)
};

View File

@ -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;