Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 12343)
@@ -1,3559 +1,3561 @@
function UnitAI() {}
UnitAI.prototype.Schema =
"Controls the unit's movement, attacks, etc, in response to commands from the player." +
"" +
"" +
"" +
"violent" +
"aggressive" +
"defensive" +
"passive" +
"standground" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"violent" +
"aggressive" +
"defensive" +
"passive" +
"skittish" +
"domestic" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
""+
"" +
"";
// Unit stances.
// There some targeting options:
// targetVisibleEnemies: anything in vision range is a viable target
// targetAttackersAlways: anything that hurts us is a viable target,
// possibly overriding user orders!
// targetAttackersPassive: anything that hurts us is a viable target,
// if we're on a passive/unforced order (e.g. gathering/building)
// There are some response options, triggered when targets are detected:
// respondFlee: run away
// respondChase: start chasing after the enemy
// respondChaseBeyondVision: start chasing, and don't stop even if it's out
// of this unit's vision range (though still visible to the player)
// respondStandGround: attack enemy but don't move at all
// respondHoldGround: attack enemy but don't move far from current position
// TODO: maybe add targetAggressiveEnemies (don't worry about lone scouts,
// do worry around armies slaughtering the guy standing next to you), etc.
var g_Stances = {
"violent": {
targetVisibleEnemies: true,
targetAttackersAlways: true,
targetAttackersPassive: true,
respondFlee: false,
respondChase: true,
respondChaseBeyondVision: true,
respondStandGround: false,
respondHoldGround: false,
},
"aggressive": {
targetVisibleEnemies: true,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: false,
respondChase: true,
respondChaseBeyondVision: false,
respondStandGround: false,
respondHoldGround: false,
},
"defensive": {
targetVisibleEnemies: true,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: false,
respondChase: false,
respondChaseBeyondVision: false,
respondStandGround: false,
respondHoldGround: true,
},
"passive": {
targetVisibleEnemies: false,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: true,
respondChase: false,
respondChaseBeyondVision: false,
respondStandGround: false,
respondHoldGround: false,
},
"standground": {
targetVisibleEnemies: true,
targetAttackersAlways: false,
targetAttackersPassive: true,
respondFlee: false,
respondChase: false,
respondChaseBeyondVision: false,
respondStandGround: true,
respondHoldGround: false,
},
};
// See ../helpers/FSM.js for some documentation of this FSM specification syntax
var UnitFsmSpec = {
// Default event handlers:
"MoveCompleted": function() {
// ignore spurious movement messages
// (these can happen when stopping moving at the same time
// as switching states)
},
"MoveStarted": function() {
// ignore spurious movement messages
},
"ConstructionFinished": function(msg) {
// ignore uninteresting construction messages
},
"LosRangeUpdate": function(msg) {
// ignore newly-seen units by default
},
"LosGaiaRangeUpdate": function(msg) {
// ignore newly-seen Gaia units by default
},
"LosHealRangeUpdate": function(msg) {
// ignore newly-seen injured units by default
},
"Attacked": function(msg) {
// ignore attacker
},
"HealthChanged": function(msg) {
// ignore
},
"EntityRenamed": function(msg) {
// ignore
},
// Formation handlers:
"FormationLeave": function(msg) {
// ignore when we're not in FORMATIONMEMBER
},
// Called when being told to walk as part of a formation
"Order.FormationWalk": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.MoveToFormationOffset(msg.data.target, msg.data.x, msg.data.z);
this.SetNextState("FORMATIONMEMBER.WALKING");
},
// Special orders:
// (these will be overridden by various states)
"Order.LeaveFoundation": function(msg) {
// Default behaviour is to ignore the order since we're busy
return { "discardOrder": true };
},
// Individual orders:
// (these will switch the unit out of formation mode)
"Order.Stop": function(msg) {
// We have no control over non-domestic animals.
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
// Stop moving immediately.
this.StopMoving();
this.FinishOrder();
// No orders left, we're an individual now
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
},
"Order.Walk": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
this.SetHeldPosition(this.order.data.x, this.order.data.z);
this.MoveToPoint(this.order.data.x, this.order.data.z);
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
this.SetNextState("INDIVIDUAL.WALKING");
},
"Order.WalkToTarget": function(msg) {
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
var ok = this.MoveToTarget(this.order.data.target);
if (ok)
{
// We've started walking to the given point
if (this.IsAnimal())
this.SetNextState("ANIMAL.WALKING");
else
this.SetNextState("INDIVIDUAL.WALKING");
}
else
{
// We are already at the target, or can't move at all
this.StopMoving();
this.FinishOrder();
}
},
"Order.Flee": function(msg) {
// TODO: if we were attacked by a ranged unit, we need to flee much further away
var ok = this.MoveToTargetRangeExplicit(this.order.data.target, +this.template.FleeDistance, -1);
if (ok)
{
// We've started fleeing from the given target
if (this.IsAnimal())
this.SetNextState("ANIMAL.FLEEING");
else
this.SetNextState("INDIVIDUAL.FLEEING");
}
else
{
// We are already at the target, or can't move at all
this.StopMoving();
this.FinishOrder();
}
},
"Order.Attack": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Work out how to attack the given target
var type = this.GetBestAttackAgainst(this.order.data.target);
if (!type)
{
// Oops, we can't attack at all
this.FinishOrder();
return;
}
this.attackType = type;
// If we are already at the target, try attacking it from here
if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.attackType))
{
this.StopMoving();
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.ATTACKING");
else
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
return;
}
// If we can't reach the target, but are standing ground,
// then abandon this attack order
if (this.GetStance().respondStandGround && !this.order.data.force)
{
this.FinishOrder();
return;
}
// Try to move within attack range
if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType))
{
// We've started walking to the given point
if (this.IsAnimal())
this.SetNextState("ANIMAL.COMBAT.APPROACHING");
else
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
return;
}
// We can't reach the target, and can't move towards it,
// so abandon this attack order
this.FinishOrder();
},
"Order.Heal": function(msg) {
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Check if the target is in range
if (this.CheckTargetRange(this.order.data.target, IID_Heal))
{
this.StopMoving();
this.SetNextState("INDIVIDUAL.HEAL.HEALING");
return;
}
// If we can't reach the target, but are standing ground,
// then abandon this heal order
if (this.GetStance().respondStandGround && !this.order.data.force)
{
this.FinishOrder();
return;
}
// Try to move within heal range
if (this.MoveToTargetRange(this.order.data.target, IID_Heal))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
return;
}
// We can't reach the target, and can't move towards it,
// so abandon this heal order
this.FinishOrder();
},
"Order.Gather": function(msg) {
// If the target is still alive, we need to kill it first
if (this.MustKillGatherTarget(this.order.data.target) && this.CheckTargetVisible(this.order.data.target))
{
// Make sure we can attack the target, else we'll get very stuck
if (!this.GetBestAttackAgainst(this.order.data.target))
{
// Oops, we can't attack at all - give up
// TODO: should do something so the player knows why this failed
this.FinishOrder();
return;
}
this.PushOrderFront("Attack", { "target": this.order.data.target, "force": false });
return;
}
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try gathering it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
this.SetNextStateAlwaysEntering("INDIVIDUAL.GATHER.GATHERING");
}
},
"Order.GatherNearPosition": function(msg) {
// Move the unit to the position to gather from.
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("INDIVIDUAL.GATHER.WALKING");
},
"Order.ReturnResource": function(msg) {
// Try to move to the dropsite
if (this.MoveToTarget(this.order.data.target))
{
// We've started walking to the target
this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
}
else
{
// Oops, we can't reach the dropsite.
// Maybe we should try to pick another dropsite, to find an
// accessible one?
// For now, just give up.
this.StopMoving();
this.FinishOrder();
return;
}
},
"Order.Trade": function(msg) {
if (this.MoveToMarket(this.order.data.firstMarket))
{
// We've started walking to the first market
this.SetNextState("INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET");
}
},
"Order.Repair": function(msg) {
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, IID_Builder))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try repairing it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING");
}
},
"Order.Garrison": function(msg) {
if (this.MoveToTarget(this.order.data.target))
{
this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING");
}
else
{
// We do a range check before actually garrisoning
this.StopMoving();
this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED");
}
},
"Order.Cheering": function(msg) {
this.SetNextState("INDIVIDUAL.CHEERING");
},
// States for the special entity representing a group of units moving in formation:
"FORMATIONCONTROLLER": {
"Order.Walk": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("WALKING");
},
// Only used by other orders to walk there in formation
"Order.WalkToTargetRange": function(msg) {
if(this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max))
this.SetNextState("WALKING");
else
this.FinishOrder();
},
"Order.Stop": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.CallMemberFunction("Stop", [false]);
cmpFormation.Disband();
},
"Order.Attack": function(msg) {
// TODO: we should move in formation towards the target,
// then break up into individuals when close enough to it
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.CallMemberFunction("Attack", [msg.data.target, false]);
// TODO: we should wait until the target is killed, then
// move on to the next queued order.
// Don't bother now, just disband the formation immediately.
cmpFormation.Disband();
},
"Order.Heal": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.CallMemberFunction("Heal", [msg.data.target, false]);
cmpFormation.Disband();
},
"Order.Repair": function(msg) {
// TODO on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target))
// The building was finished or destroyed
this.FinishOrder();
else
// Out of range move there in formation
this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
// We don't want to rearrange the formation if the individual units are carrying
// out a task and one of the members dies/leaves the formation.
cmpFormation.SetRearrange(false);
cmpFormation.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]);
this.SetNextState("REPAIR");
},
"Order.Gather": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.CallMemberFunction("Gather", [msg.data.target, false]);
cmpFormation.Disband();
},
"Order.GatherNearPosition": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
cmpFormation.Disband();
},
"Order.ReturnResource": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.CallMemberFunction("ReturnResource", [msg.data.target, false]);
cmpFormation.Disband();
},
"Order.Garrison": function(msg) {
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.CallMemberFunction("Garrison", [msg.data.target, false]);
cmpFormation.Disband();
},
"IDLE": {
},
"WALKING": {
"MoveStarted": function(msg) {
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true);
},
"MoveCompleted": function(msg) {
if (this.FinishOrder())
return;
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.Disband();
},
},
"REPAIR": {
"ConstructionFinished": function(msg) {
if (msg.data.entity != this.order.data.target)
return;
if (this.FinishOrder())
return;
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
cmpFormation.Disband();
},
},
},
// States for entities moving as part of a formation:
"FORMATIONMEMBER": {
"HealthChanged": function(msg) {
if (this.IsAnimal() && msg.to == 0)
this.SetNextState("ANIMAL.CORPSE");
},
"FormationLeave": function(msg) {
// Stop moving as soon as the formation disbands
this.StopMoving();
// We're leaving the formation, so stop our FormationWalk order
if (this.FinishOrder())
return;
// No orders left, we're an individual now
if (this.IsAnimal())
this.SetNextState("ANIMAL.IDLE");
else
this.SetNextState("INDIVIDUAL.IDLE");
},
// Override the LeaveFoundation order since we're not doing
// anything more important (and we might be stuck in the WALKING
// state forever and need to get out of foundations in that case)
"Order.LeaveFoundation": function(msg) {
if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target))
{
this.FinishOrder();
return;
}
// Move a tile outside the building
var range = 4;
var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range);
if (ok)
{
// We've started walking to the given point
this.SetNextState("WALKING");
}
else
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
},
"IDLE": {
"enter": function() {
this.SelectAnimation("idle");
},
},
"WALKING": {
"enter": function () {
this.SelectAnimation("move");
},
// (We stay in this state even if we're already in position
// and no longer moving, because the formation controller might
// move and we'll automatically start chasing after it again)
},
},
// States for entities not part of a formation:
"INDIVIDUAL": {
"enter": function() {
// Sanity-checking
if (this.IsAnimal())
error("Animal got moved into INDIVIDUAL.* state");
},
"Attacked": function(msg) {
// Respond to attack if we always target attackers, or if we target attackers
// during passive orders (e.g. gathering/repairing are never forced)
if (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && (!this.order || !this.order.data || !this.order.data.force)))
{
this.RespondToTargetedEntities([msg.data.attacker]);
}
},
"IDLE": {
"enter": function() {
// Switch back to idle animation to guarantee we won't
// get stuck with an incorrect animation
this.SelectAnimation("idle");
// The GUI and AI want to know when a unit is idle, but we don't
// want to send frequent spurious messages if the unit's only
// idle for an instant and will quickly go off and do something else.
// So we'll set a timer here and only report the idle event if we
// remain idle
this.StartTimer(1000);
// If a unit can heal and attack we first want to heal wounded units,
// so check if we are a healer and find whether there's anybody nearby to heal.
// (If anyone approaches later it'll be handled via LosHealRangeUpdate.)
// If anyone in sight gets hurt that will be handled via LosHealRangeUpdate.
if (this.IsHealer() && this.FindNewHealTargets())
return true; // (abort the FSM transition since we may have already switched state)
// If we entered the idle state we must have nothing better to do,
// so immediately check whether there's anybody nearby to attack.
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
if (this.FindNewTargets())
return true; // (abort the FSM transition since we may have already switched state)
// Nobody to attack - stay in idle
return false;
},
"leave": function() {
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
rangeMan.DisableActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
rangeMan.DisableActiveQuery(this.losHealRangeQuery);
this.StopTimer();
if (this.isIdle)
{
this.isIdle = false;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
"LosRangeUpdate": function(msg) {
if (this.GetStance().targetVisibleEnemies)
{
// Start attacking one of the newly-seen enemy (if any)
this.AttackEntitiesByPreference(msg.data.added);
}
},
"LosGaiaRangeUpdate": function(msg) {
if (this.GetStance().targetVisibleEnemies)
{
// Start attacking one of the newly-seen enemy (if any)
this.AttackGaiaEntitiesByPreference(msg.data.added);
}
},
"LosHealRangeUpdate": function(msg) {
this.RespondToHealableEntities(msg.data.added);
},
"Timer": function(msg) {
if (!this.isIdle)
{
this.isIdle = true;
Engine.PostMessage(this.entity, MT_UnitIdleChanged, { "idle": this.isIdle });
}
},
// Override the LeaveFoundation order since we're not doing
// anything more important
"Order.LeaveFoundation": function(msg) {
if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target))
{
this.FinishOrder();
return;
}
// Move a tile outside the building
var range = 4;
var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range);
if (ok)
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.WALKING");
}
else
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
},
},
"WALKING": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.FinishOrder();
},
},
"FLEEING": {
"enter": function() {
this.PlaySound("panic");
// Run quickly
var speed = this.GetRunSpeed();
this.SelectAnimation("move");
this.SetMoveSpeed(speed);
},
"leave": function() {
// Reset normal speed
this.SetMoveSpeed(this.GetWalkSpeed());
},
"MoveCompleted": function() {
// When we've run far enough, stop fleeing
this.FinishOrder();
},
// TODO: what if we run into more enemies while fleeing?
},
"COMBAT": {
"EntityRenamed": function(msg) {
if (this.order.data.target == msg.entity)
this.order.data.target = msg.newentity;
},
"Attacked": function(msg) {
// If we're already in combat mode, ignore anyone else
// who's attacking us
},
"APPROACHING": {
"enter": function () {
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function() {
this.SetNextState("ATTACKING");
},
"Attacked": function(msg) {
// If we're attacked by a close enemy, we should try to defend ourself
// but only if we're not forced to target something else
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force)))
{
this.RespondToTargetedEntities([msg.data.attacker]);
}
},
},
"ATTACKING": {
"enter": function() {
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
this.attackTimers = cmpAttack.GetTimers(this.attackType);
this.SelectAnimation("melee", false, 1.0, "attack");
this.SetAnimationSync(this.attackTimers.prepare, this.attackTimers.repeat);
this.StartTimer(this.attackTimers.prepare, this.attackTimers.repeat);
// TODO: we should probably only bother syncing projectile attacks, not melee
// TODO: if .prepare is short, players can cheat by cycling attack/stop/attack
// to beat the .repeat time; should enforce a minimum time
this.FaceTowardsTarget(this.order.data.target);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
var target = this.order.data.target;
// Check the target is still alive and attackable
if (this.TargetIsAlive(target) && this.CanAttack(target))
{
// Check we can still reach the target
if (this.CheckTargetRange(target, IID_Attack, this.attackType))
{
this.FaceTowardsTarget(target);
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
cmpAttack.PerformAttack(this.attackType, target);
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.MoveToTargetRange(target, IID_Attack, this.attackType))
{
this.SetNextState("COMBAT.CHASING");
return;
}
}
}
// Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up
if (this.FinishOrder())
return;
// See if we can switch to a new nearby enemy
if (this.FindNewTargets())
return;
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
},
// TODO: respond to target deaths immediately, rather than waiting
// until the next Timer event
"Attacked": function(msg) {
if (this.order.data.target != msg.data.attacker)
{
// If we're attacked by a close enemy, stronger than our current target,
// we choose to attack it, but only if we're not forced to target something else
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force)))
{
var ents = [this.order.data.target, msg.data.attacker];
SortEntitiesByPriority(ents);
if (ents[0] != this.order.data.target)
{
this.RespondToTargetedEntities(ents);
}
}
}
},
},
"CHASING": {
"enter": function () {
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Attack))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function() {
this.SetNextState("ATTACKING");
},
},
},
"GATHER": {
"APPROACHING": {
"enter": function() {
this.SelectAnimation("move");
},
"MoveCompleted": function(msg) {
if (msg.data.error)
{
// We failed to reach the target
// Save the current order's data in case we need it later
var oldType = this.order.data.type;
var oldTarget = this.order.data.target;
var oldTemplate = this.order.data.template;
// Try the next queued order if there is any
if (this.FinishOrder())
return;
// Try to find another nearby target of the same specific type
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (
ent != oldTarget
&& ((type.generic == "treasure" && oldType.generic == "treasure")
|| (type.specific == oldType.specific
&& (type.specific != "meat" || oldTemplate == template)))
);
});
if (nearby)
{
this.PerformGather(nearby, false, false);
return;
}
// Couldn't find anything else. Just try this one again,
// maybe we'll succeed next time
this.PerformGather(oldTarget, false, false);
return;
}
// We reached the target - start gathering from it now
this.SetNextState("GATHERING");
},
},
// Walking to a good place to gather resources near, used by GatherNearPosition
"WALKING": {
"enter": function() {
this.SelectAnimation("move");
},
"MoveCompleted": function(msg) {
var resourceType = this.order.data.type;
var resourceTemplate = this.order.data.template;
// Try to find another nearby target of the same specific type
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (
(type.generic == "treasure" && resourceType.generic == "treasure")
|| (type.specific == resourceType.specific
&& (type.specific != "meat" || resourceTemplate == template))
);
});
// If there is a nearby resource start gathering
if (nearby)
{
this.PerformGather(nearby, false, false);
return;
}
// Couldn't find nearby resources, so give up
this.FinishOrder();
},
},
"GATHERING": {
"enter": function() {
var target = this.order.data.target;
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
this.order.data.force = false;
// Calculate timing based on gather rates
// This allows the gather rate to control how often we gather, instead of how much.
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var rate = cmpResourceGatherer.GetTargetGatherRate(target);
if (!rate)
{
// Try to find another target if the current one stopped existing
if (!Engine.QueryInterface(target, IID_Identity))
{
// Let the Timer logic handle this
this.StartTimer(0);
return;
}
// No rate, give up on gathering
this.FinishOrder();
return;
}
// Scale timing interval based on rate, and start timer
// The offset should be at least as long as the repeat time so we use the same value for both.
var offset = 1000/rate;
var repeat = offset;
this.StartTimer(offset, repeat);
// We want to start the gather animation as soon as possible,
// but only if we're actually at the target and it's still alive
// (else it'll look like we're chopping empty air).
// (If it's not alive, the Timer handler will deal with sending us
// off to a different target.)
if (this.CheckTargetRange(target, IID_ResourceGatherer))
{
var typename = "gather_" + this.order.data.type.specific;
this.SelectAnimation(typename, false, 1.0, typename);
}
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
var target = this.order.data.target;
var resourceTemplate = this.order.data.template;
var resourceType = this.order.data.type;
// Check we can still reach and gather from the target
if (this.CheckTargetRange(target, IID_ResourceGatherer) && this.CanGather(target))
{
// Gather the resources:
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
// Try to gather treasure
if (cmpResourceGatherer.TryInstantGather(target))
return;
// If we've already got some resources but they're the wrong type,
// drop them first to ensure we're only ever carrying one type
if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic))
cmpResourceGatherer.DropResources();
// Collect from the target
var status = cmpResourceGatherer.PerformGather(target);
// If we've collected as many resources as possible,
// return to the nearest dropsite
if (status.filled)
{
var nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
// (Keep this Gather order on the stack so we'll
// continue gathering after returning)
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on gathering.
this.FinishOrder();
return;
}
// We can gather more from this target, do so in the next timer
if (!status.exhausted)
return;
}
else
{
// Try to follow the target
if (this.MoveToTargetRange(target, IID_ResourceGatherer))
{
this.SetNextState("APPROACHING");
return;
}
// Can't reach the target, or it doesn't exist any more
// We want to carry on gathering resources in the same area as
// the old one. So try to get close to the old resource's
// last known position
var maxRange = 8; // get close but not too close
if (this.order.data.lastPos &&
this.MoveToPointRange(this.order.data.lastPos.x, this.order.data.lastPos.z,
0, maxRange))
{
this.SetNextState("APPROACHING");
return;
}
}
// We're already in range, can't get anywhere near it or the target is exhausted.
// Give up on this order and try our next queued order
if (this.FinishOrder())
return;
// No remaining orders - pick a useful default behaviour
// Try to find a new resource of the same specific type near our current position:
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (
(type.generic == "treasure" && resourceType.generic == "treasure")
|| (type.specific == resourceType.specific
&& (type.specific != "meat" || resourceTemplate == template))
);
});
if (nearby)
{
this.PerformGather(nearby, false, false);
return;
}
// Nothing else to gather - if we're carrying anything then we should
// drop it off, and if not then we might as well head to the dropsite
// anyway because that's a nice enough place to congregate and idle
var nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby)
{
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// No dropsites - just give up
},
},
},
"HEAL": {
"EntityRenamed": function(msg) {
if (this.order.data.target == msg.entity)
this.order.data.target = msg.newentity;
},
"Attacked": function(msg) {
// If we stand ground we will rather die than flee
if (!this.GetStance().respondStandGround && !this.order.data.force)
this.Flee(msg.data.attacker, false);
},
"APPROACHING": {
"enter": function () {
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function() {
this.SetNextState("HEALING");
},
},
"HEALING": {
"enter": function() {
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
this.healTimers = cmpHeal.GetTimers();
this.SelectAnimation("heal", false, 1.0, "heal");
this.SetAnimationSync(this.healTimers.prepare, this.healTimers.repeat);
this.StartTimer(this.healTimers.prepare, this.healTimers.repeat);
// TODO if .prepare is short, players can cheat by cycling heal/stop/heal
// to beat the .repeat time; should enforce a minimum time
// see comment in ATTACKING.enter
this.FaceTowardsTarget(this.order.data.target);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
var target = this.order.data.target;
// Check the target is still alive and healable
if (this.TargetIsAlive(target) && this.CanHeal(target))
{
// Check if we can still reach the target
if (this.CheckTargetRange(target, IID_Heal))
{
this.FaceTowardsTarget(target);
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
cmpHeal.PerformHeal(target);
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.MoveToTargetRange(target, IID_Heal))
{
this.SetNextState("HEAL.CHASING");
return;
}
}
}
// Can't reach it, healed to max hp or doesn't exist any more - give up
if (this.FinishOrder())
return;
// Heal another one
if (this.FindNewHealTargets())
return;
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
},
},
"CHASING": {
"enter": function () {
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
},
"leave": function () {
this.StopTimer();
},
"Timer": function(msg) {
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, IID_Heal))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
this.WalkToHeldPosition();
}
},
"MoveCompleted": function () {
this.SetNextState("HEALING");
},
},
},
// Returning to dropsite
"RETURNRESOURCE": {
"APPROACHING": {
"enter": function () {
// Work out what we're carrying, in order to select an appropriate animation
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var type = cmpResourceGatherer.GetLastCarriedType();
if (type)
{
var typename = "carry_" + type.generic;
// Special case for meat
if (type.specific == "meat")
typename = "carry_" + type.specific;
this.SelectAnimation(typename, false, this.GetWalkSpeed());
}
else
{
// We're returning empty-handed
this.SelectAnimation("move");
}
},
"MoveCompleted": function() {
// Switch back to idle animation to guarantee we won't
// get stuck with the carry animation after stopping moving
this.SelectAnimation("idle");
// Check the dropsite is in range and we can return our resource there
// (we didn't get stopped before reaching it)
if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer) && this.CanReturnResource(this.order.data.target, true))
{
var cmpResourceDropsite = Engine.QueryInterface(this.order.data.target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
cmpResourceGatherer.CommitResources(dropsiteTypes);
// Our next order should always be a Gather,
// so just switch back to that order
this.FinishOrder();
return;
}
}
// The dropsite was destroyed, or we couldn't reach it, or ownership changed
// Look for a new one.
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
var genericType = cmpResourceGatherer.GetMainCarryingType();
var nearby = this.FindNearestDropsite(genericType);
if (nearby)
{
this.FinishOrder();
this.PushOrderFront("ReturnResource", { "target": nearby, "force": false });
return;
}
// Oh no, couldn't find any drop sites. Give up on returning.
this.FinishOrder();
},
},
},
"TRADE": {
"Attacked": function(msg) {
// Ignore attack
// TODO: Inform player
},
"APPROACHINGFIRSTMARKET": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.PerformTradeAndMoveToNextMarket(this.order.data.firstMarket, this.order.data.secondMarket, "INDIVIDUAL.TRADE.APPROACHINGSECONDMARKET");
},
},
"APPROACHINGSECONDMARKET": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.order.data.firstPass = false;
this.PerformTradeAndMoveToNextMarket(this.order.data.secondMarket, this.order.data.firstMarket, "INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET");
},
},
},
"REPAIR": {
"APPROACHING": {
"enter": function () {
this.SelectAnimation("move");
},
"MoveCompleted": function() {
this.SetNextState("REPAIRING");
},
},
"REPAIRING": {
"enter": function() {
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
this.order.data.force = false;
this.SelectAnimation("build", false, 1.0, "build");
this.StartTimer(1000, 1000);
},
"leave": function() {
this.StopTimer();
},
"Timer": function(msg) {
var target = this.order.data.target;
// Check we can still reach and repair the target
if (!this.CheckTargetRange(target, IID_Builder) || !this.CanRepair(target))
{
// Can't reach it, no longer owned by ally, or it doesn't exist any more
this.FinishOrder();
return;
}
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
cmpBuilder.PerformBuilding(target);
},
},
"ConstructionFinished": function(msg) {
if (msg.data.entity != this.order.data.target)
return; // ignore other buildings
// Save the current order's data in case we need it later
var oldAutocontinue = this.order.data.autocontinue;
// Save the current state so we can continue walking if necessary
// FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation.
// Idle animation while moving towards finished construction looks weird (ghosty).
var oldState = this.GetCurrentState();
// We finished building it.
// Switch to the next order (if any)
if (this.FinishOrder())
return;
// No remaining orders - pick a useful default behaviour
// If autocontinue explicitly disabled (e.g. by AI) then
// do nothing automatically
if (!oldAutocontinue)
return;
// If this building was e.g. a farm, we should start gathering from it
// if we own the building
if (this.CanGather(msg.data.newentity))
{
this.PerformGather(msg.data.newentity, true, false);
return;
}
// If this building was e.g. a farmstead, we should look for nearby
// resources we can gather, if we own the building
if (this.CanReturnResource(msg.data.newentity, false))
{
var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite);
var types = cmpResourceDropsite.GetTypes();
// TODO: Slightly undefined behavior here, we don't know what type of resource will be collected,
// may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that!
var nearby = this.FindNearbyResource(function (ent, type, template) {
return (types.indexOf(type.generic) != -1);
});
if (nearby)
{
this.PerformGather(nearby, true, false);
return;
}
}
// Look for a nearby foundation to help with
var nearbyFoundation = this.FindNearbyFoundation();
if (nearbyFoundation)
{
this.AddOrder("Repair", { "target": nearbyFoundation, "autocontinue": oldAutocontinue, "force": false }, true);
return;
}
// Unit was approaching and there's nothing to do now, so switch to walking
if (oldState === "INDIVIDUAL.REPAIR.APPROACHING")
{
// We're already walking to the given point, so add this as a order.
this.WalkToTarget(msg.data.newentity, true);
}
},
// Override the LeaveFoundation order since we don't want to be
// accidentally blocking our own building
"Order.LeaveFoundation": function(msg) {
if (!IsOwnedByAllyOfEntity(this.entity, msg.data.target))
{
this.FinishOrder();
return;
}
// Move a tile outside the building
var range = 4;
var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range);
if (ok)
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.WALKING");
}
else
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
},
},
"GARRISON": {
"APPROACHING": {
"enter": function() {
this.SelectAnimation("walk", false, this.GetWalkSpeed());
},
"MoveCompleted": function() {
this.SetNextState("GARRISONED");
},
"leave": function() {
this.StopTimer();
}
},
"GARRISONED": {
"enter": function() {
var target = this.order.data.target;
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
// Check that we can garrison here
if (this.CanGarrison(target))
{
// Check that we're in range of the garrison target
if (this.CheckGarrisonRange(target))
{
// Check that garrisoning succeeds
if (cmpGarrisonHolder.Garrison(this.entity))
{
this.isGarrisoned = true;
// Check if we are garrisoned in a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer)
cmpResourceGatherer.CommitResources(dropsiteTypes);
}
return;
}
}
else
{
// Unable to reach the target, try again
// (or follow if it's a moving target)
if (this.MoveToTarget(target))
{
this.SetNextState("APPROACHING");
return;
}
}
}
// Garrisoning failed for some reason, so finish the order
this.FinishOrder();
return;
},
"Order.Ungarrison": function() {
if (this.FinishOrder())
return;
},
"leave": function() {
this.isGarrisoned = false;
}
},
},
"CHEERING": {
"enter": function() {
// Unit is invulnerable while cheering
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(true);
this.SelectAnimation("promotion");
this.StartTimer(4000, 4000);
return false;
},
"leave": function() {
this.StopTimer();
var cmpDamageReceiver = Engine.QueryInterface(this.entity, IID_DamageReceiver);
cmpDamageReceiver.SetInvulnerability(false);
},
"Timer": function(msg) {
this.FinishOrder();
},
},
},
"ANIMAL": {
"HealthChanged": function(msg) {
// If we died (got reduced to 0 hitpoints), stop the AI and act like a corpse
if (msg.to == 0)
this.SetNextState("CORPSE");
},
"Attacked": function(msg) {
if (this.template.NaturalBehaviour == "skittish" ||
this.template.NaturalBehaviour == "passive")
{
this.Flee(msg.data.attacker, false);
}
else if (this.IsDangerousAnimal() || this.template.NaturalBehaviour == "defensive")
{
if (this.CanAttack(msg.data.attacker))
this.Attack(msg.data.attacker, false);
}
else if (this.template.NaturalBehaviour == "domestic")
{
// Never flee, stop what we were doing
this.SetNextState("IDLE");
}
},
"Order.LeaveFoundation": function(msg) {
// Run away from the foundation
this.MoveToTargetRangeExplicit(msg.data.target, +this.template.FleeDistance, +this.template.FleeDistance);
this.SetNextState("FLEEING");
},
"IDLE": {
// (We need an IDLE state so that FinishOrder works)
"enter": function() {
// Start feeding immediately
this.SetNextState("FEEDING");
return true;
},
},
"CORPSE": {
"enter": function() {
this.StopMoving();
},
// Ignore all orders that animals might otherwise respond to
"Order.FormationWalk": function() { },
"Order.Walk": function() { },
"Order.WalkToTarget": function() { },
"Order.Attack": function() { },
"Attacked": function(msg) {
// Do nothing, because we're dead already
},
"Order.LeaveFoundation": function(msg) {
// We can't walk away from the foundation (since we're dead),
// but we mustn't block its construction (since the builders would get stuck),
// and we don't want to trick gatherers into trying to reach us when
// we're stuck in the middle of a building, so just delete our corpse.
Engine.DestroyEntity(this.entity);
},
},
"ROAMING": {
"enter": function() {
// Walk in a random direction
this.SelectAnimation("walk", false, this.GetWalkSpeed());
this.MoveRandomly(+this.template.RoamDistance);
// Set a random timer to switch to feeding state
this.StartTimer(RandomInt(+this.template.RoamTimeMin, +this.template.RoamTimeMax));
},
"leave": function() {
this.StopTimer();
},
"LosRangeUpdate": function(msg) {
if (this.template.NaturalBehaviour == "skittish")
{
if (msg.data.added.length > 0)
{
this.Flee(msg.data.added[0], false);
return;
}
}
// Start attacking one of the newly-seen enemy (if any)
else if (this.IsDangerousAnimal())
{
this.AttackVisibleEntity(msg.data.added);
}
// TODO: if two units enter our range together, we'll attack the
// first and then the second won't trigger another LosRangeUpdate
// so we won't notice it. Probably we should do something with
// ResetActiveQuery in ROAMING.enter/FEEDING.enter in order to
// find any units that are already in range.
},
"Timer": function(msg) {
this.SetNextState("FEEDING");
},
"MoveCompleted": function() {
this.MoveRandomly(+this.template.RoamDistance);
},
},
"FEEDING": {
"enter": function() {
// Stop and eat for a while
this.SelectAnimation("feeding");
this.StopMoving();
this.StartTimer(RandomInt(+this.template.FeedTimeMin, +this.template.FeedTimeMax));
},
"leave": function() {
this.StopTimer();
},
"LosRangeUpdate": function(msg) {
if (this.template.NaturalBehaviour == "skittish")
{
if (msg.data.added.length > 0)
{
this.Flee(msg.data.added[0], false);
return;
}
}
// Start attacking one of the newly-seen enemy (if any)
else if (this.template.NaturalBehaviour == "violent")
{
this.AttackVisibleEntity(msg.data.added);
}
},
"MoveCompleted": function() { },
"Timer": function(msg) {
this.SetNextState("ROAMING");
},
},
"FLEEING": "INDIVIDUAL.FLEEING", // reuse the same fleeing behaviour for animals
"COMBAT": "INDIVIDUAL.COMBAT", // reuse the same combat behaviour for animals
"WALKING": "INDIVIDUAL.WALKING", // reuse the same walking behaviour for animals
// only used for domestic animals
},
};
var UnitFsm = new FSM(UnitFsmSpec);
UnitAI.prototype.Init = function()
{
this.orderQueue = []; // current order is at the front of the list
this.order = undefined; // always == this.orderQueue[0]
this.formationController = INVALID_ENTITY; // entity with IID_Formation that we belong to
this.isGarrisoned = false;
this.isIdle = false;
this.lastFormationName = "";
this.SetStance(this.template.DefaultStance);
};
UnitAI.prototype.IsFormationController = function()
{
return (this.template.FormationController == "true");
};
UnitAI.prototype.IsAnimal = function()
{
return (this.template.NaturalBehaviour ? true : false);
};
UnitAI.prototype.IsDangerousAnimal = function()
{
return (this.IsAnimal() && (this.template.NaturalBehaviour == "violent" ||
this.template.NaturalBehaviour == "aggressive"));
};
UnitAI.prototype.IsDomestic = function()
{
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
if (!cmpIdentity)
return false;
return cmpIdentity.HasClass("Domestic");
};
UnitAI.prototype.IsHealer = function()
{
return Engine.QueryInterface(this.entity, IID_Heal);
};
UnitAI.prototype.IsIdle = function()
{
return this.isIdle;
};
UnitAI.prototype.IsGarrisoned = function()
{
return this.isGarrisoned;
};
UnitAI.prototype.CanAttackGaia = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
// Rejects Gaia (0) and INVALID_PLAYER (-1)
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() <= 0)
return false;
return true;
};
UnitAI.prototype.OnCreate = function()
{
if (this.IsAnimal())
UnitFsm.Init(this, "ANIMAL.FEEDING");
else if (this.IsFormationController())
UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
else
UnitFsm.Init(this, "INDIVIDUAL.IDLE");
};
UnitAI.prototype.OnOwnershipChanged = function(msg)
{
this.SetupRangeQueries();
// If the unit isn't being created or dying, clear orders and reset stance.
if (msg.to != -1 && msg.from != -1)
{
this.SetStance(this.template.DefaultStance);
this.Stop(false);
}
};
UnitAI.prototype.OnDestroy = function()
{
// Clean up any timers that are now obsolete
this.StopTimer();
// Clean up range queries
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.losRangeQuery)
rangeMan.DestroyActiveQuery(this.losRangeQuery);
if (this.losHealRangeQuery)
rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
if (this.losGaiaRangeQuery)
rangeMan.DestroyActiveQuery(this.losGaiaRangeQuery);
};
// Wrapper function that sets up the normal, healer, and Gaia range queries.
UnitAI.prototype.SetupRangeQueries = function()
{
this.SetupRangeQuery();
if (this.IsHealer())
this.SetupHealRangeQuery();
if (this.CanAttackGaia() || this.losGaiaRangeQuery)
this.SetupGaiaRangeQuery();
}
// Set up a range query for all enemy units within LOS range
// which can be attacked.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupRangeQuery = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var owner = cmpOwnership.GetOwner();
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (this.losRangeQuery)
{
rangeMan.DestroyActiveQuery(this.losRangeQuery);
this.losRangeQuery = undefined;
}
var players = [];
if (owner != -1)
{
// If unit not just killed, get enemy players via diplomacy
var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
var numPlayers = playerMan.GetNumPlayers();
for (var i = 1; i < numPlayers; ++i)
{
// Exclude gaia, allies, and self
// TODO: How to handle neutral players - Special query to attack military only?
if (cmpPlayer.IsEnemy(i))
players.push(i);
}
}
var range = this.GetQueryRange(IID_Attack);
this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_DamageReceiver, rangeMan.GetEntityFlagMask("normal"));
rangeMan.EnableActiveQuery(this.losRangeQuery);
};
// Set up a range query for all own or ally units within LOS range
// which can be healed.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupHealRangeQuery = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var owner = cmpOwnership.GetOwner();
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (this.losHealRangeQuery)
rangeMan.DestroyActiveQuery(this.losHealRangeQuery);
var players = [owner];
if (owner != -1)
{
// If unit not just killed, get ally players via diplomacy
var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), IID_Player);
var numPlayers = playerMan.GetNumPlayers();
for (var i = 1; i < numPlayers; ++i)
{
// Exclude gaia and enemies
if (cmpPlayer.IsAlly(i))
players.push(i);
}
}
var range = this.GetQueryRange(IID_Heal);
this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, players, IID_Health, rangeMan.GetEntityFlagMask("injured"));
rangeMan.EnableActiveQuery(this.losHealRangeQuery);
};
// Set up a range query for Gaia units within LOS range which can be attacked.
// This should be called whenever our ownership changes.
UnitAI.prototype.SetupGaiaRangeQuery = function()
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var owner = cmpOwnership.GetOwner();
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (this.losGaiaRangeQuery)
{
rangeMan.DestroyActiveQuery(this.losGaiaRangeQuery);
this.losGaiaRangeQuery = undefined;
}
// Only create the query if Gaia is our enemy and we can attack.
if (this.CanAttackGaia())
{
var range = this.GetQueryRange(IID_Attack);
// This query is only interested in Gaia entities that can attack.
this.losGaiaRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.min, range.max, [0], IID_Attack, rangeMan.GetEntityFlagMask("normal"));
rangeMan.EnableActiveQuery(this.losGaiaRangeQuery);
}
};
//// FSM linkage functions ////
UnitAI.prototype.SetNextState = function(state)
{
UnitFsm.SetNextState(this, state);
};
// This will make sure that the state is always entered even if this means leaving it and reentering it
// This is so that a state can be reinitialized with new order data without having to switch to an intermediate state
UnitAI.prototype.SetNextStateAlwaysEntering = function(state)
{
UnitFsm.SetNextStateAlwaysEntering(this, state);
};
UnitAI.prototype.DeferMessage = function(msg)
{
UnitFsm.DeferMessage(this, msg);
};
UnitAI.prototype.GetCurrentState = function()
{
return UnitFsm.GetCurrentState(this);
};
UnitAI.prototype.FsmStateNameChanged = function(state)
{
Engine.PostMessage(this.entity, MT_UnitAIStateChanged, { "to": state });
};
/**
* Call when the current order has been completed (or failed).
* Removes the current order from the queue, and processes the
* next one (if any). Returns false and defaults to IDLE
* if there are no remaining orders.
*/
UnitAI.prototype.FinishOrder = function()
{
if (!this.orderQueue.length)
error("FinishOrder called when order queue is empty");
this.orderQueue.shift();
this.order = this.orderQueue[0];
if (this.orderQueue.length)
{
var ret = UnitFsm.ProcessMessage(this,
{"type": "Order."+this.order.type, "data": this.order.data}
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret && ret.discardOrder)
{
return this.FinishOrder();
}
// Otherwise we've successfully processed a new order
return true;
}
else
{
this.SetNextState("IDLE");
return false;
}
};
/**
* Add an order onto the back of the queue,
* and execute it if we didn't already have an order.
*/
UnitAI.prototype.PushOrder = function(type, data)
{
var order = { "type": type, "data": data };
this.orderQueue.push(order);
// If we didn't already have an order, then process this new one
if (this.orderQueue.length == 1)
{
this.order = order;
var ret = UnitFsm.ProcessMessage(this,
{"type": "Order."+this.order.type, "data": this.order.data}
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret && ret.discardOrder)
{
this.FinishOrder();
}
}
};
/**
* Add an order onto the front of the queue,
* and execute it immediately.
*/
UnitAI.prototype.PushOrderFront = function(type, data)
{
var order = { "type": type, "data": data };
// If current order is cheering then add new order after it
if (this.order && this.order.type == "Cheering")
{
var cheeringOrder = this.orderQueue.shift();
this.orderQueue.unshift(cheeringOrder, order);
}
else
{
this.orderQueue.unshift(order);
this.order = order;
var ret = UnitFsm.ProcessMessage(this,
{"type": "Order."+this.order.type, "data": this.order.data}
);
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
// If the order was rejected then immediately take it off again;
// assume the previous active order is still valid (the short-lived
// new order hasn't changed state or anything) so we can carry on
// as if nothing had happened
if (ret && ret.discardOrder)
{
this.orderQueue.shift();
this.order = this.orderQueue[0];
}
}
};
UnitAI.prototype.ReplaceOrder = function(type, data)
{
// If current order is cheering then add new order after it
if (this.order && this.order.type == "Cheering")
{
var order = { "type": type, "data": data };
var cheeringOrder = this.orderQueue.shift();
this.orderQueue = [ cheeringOrder, order ];
}
else
{
this.orderQueue = [];
this.PushOrder(type, data);
}
};
UnitAI.prototype.GetOrders = function()
{
return this.orderQueue.slice();
};
UnitAI.prototype.AddOrders = function(orders)
{
for each (var order in orders)
{
this.PushOrder(order.type, order.data);
}
};
UnitAI.prototype.GetOrderData = function()
{
- if (this.order && this.order.data)
- return deepcopy(this.order.data);
- else
- return undefined;
+ var orders = [];
+ for (i in this.orderQueue) {
+ if (this.orderQueue[i].data)
+ orders.push(deepcopy(this.orderQueue[i].data));
+ }
+ return orders;
};
UnitAI.prototype.TimerHandler = function(data, lateness)
{
// Reset the timer
if (data.timerRepeat === undefined)
{
this.timer = undefined;
}
UnitFsm.ProcessMessage(this, {"type": "Timer", "data": data, "lateness": lateness});
};
/**
* Set up the UnitAI timer to run after 'offset' msecs, and then
* every 'repeat' msecs until StopTimer is called. A "Timer" message
* will be sent each time the timer runs.
*/
UnitAI.prototype.StartTimer = function(offset, repeat)
{
if (this.timer)
error("Called StartTimer when there's already an active timer");
var data = { "timerRepeat": repeat };
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
if (repeat === undefined)
this.timer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "TimerHandler", offset, data);
else
this.timer = cmpTimer.SetInterval(this.entity, IID_UnitAI, "TimerHandler", offset, repeat, data);
};
/**
* Stop the current UnitAI timer.
*/
UnitAI.prototype.StopTimer = function()
{
if (!this.timer)
return;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
};
//// Message handlers /////
UnitAI.prototype.OnMotionChanged = function(msg)
{
if (msg.starting && !msg.error)
{
UnitFsm.ProcessMessage(this, {"type": "MoveStarted", "data": msg});
}
else if (!msg.starting || msg.error)
{
UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg});
}
};
UnitAI.prototype.OnGlobalConstructionFinished = function(msg)
{
// TODO: This is a bit inefficient since every unit listens to every
// construction message - ideally we could scope it to only the one we're building
UnitFsm.ProcessMessage(this, {"type": "ConstructionFinished", "data": msg});
};
UnitAI.prototype.OnGlobalEntityRenamed = function(msg)
{
UnitFsm.ProcessMessage(this, {"type": "EntityRenamed", "entity": msg.entity, "newentity": msg.newentity});
};
UnitAI.prototype.OnAttacked = function(msg)
{
UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
};
UnitAI.prototype.OnHealthChanged = function(msg)
{
UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
};
UnitAI.prototype.OnRangeUpdate = function(msg)
{
if (msg.tag == this.losRangeQuery)
UnitFsm.ProcessMessage(this, {"type": "LosRangeUpdate", "data": msg});
else if (msg.tag == this.losGaiaRangeQuery)
UnitFsm.ProcessMessage(this, {"type": "LosGaiaRangeUpdate", "data": msg});
else if (msg.tag == this.losHealRangeQuery)
UnitFsm.ProcessMessage(this, {"type": "LosHealRangeUpdate", "data": msg});
};
//// Helper functions to be called by the FSM ////
UnitAI.prototype.GetWalkSpeed = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.GetWalkSpeed();
};
UnitAI.prototype.GetRunSpeed = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.GetRunSpeed();
};
/**
* Returns true if the target exists and has non-zero hitpoints.
*/
UnitAI.prototype.TargetIsAlive = function(ent)
{
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (!cmpHealth)
return false;
return (cmpHealth.GetHitpoints() != 0);
};
/**
* Returns true if the target exists and needs to be killed before
* beginning to gather resources from it.
*/
UnitAI.prototype.MustKillGatherTarget = function(ent)
{
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (!cmpResourceSupply)
return false;
if (!cmpResourceSupply.GetKillBeforeGather())
return false;
return this.TargetIsAlive(ent);
};
/**
* Returns the entity ID of the nearest resource supply where the given
* filter returns true, or undefined if none can be found.
* TODO: extend this to exclude resources that already have lots of
* gatherers.
*/
UnitAI.prototype.FindNearbyResource = function(filter)
{
var range = 64; // TODO: what's a sensible number?
var playerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
// We accept resources owned by Gaia or any player
var players = [0];
for (var i = 1; i < playerMan.GetNumPlayers(); ++i)
players.push(i);
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, IID_ResourceSupply);
for each (var ent in nearby)
{
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
var type = cmpResourceSupply.GetType();
var amount = cmpResourceSupply.GetCurrentAmount();
var template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (amount > 0 && filter(ent, type, template))
return ent;
}
return undefined;
};
/**
* Returns the entity ID of the nearest resource dropsite that accepts
* the given type, or undefined if none can be found.
*/
UnitAI.prototype.FindNearestDropsite = function(genericType)
{
// Find dropsites owned by this unit's player
var players = [];
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership)
players.push(cmpOwnership.GetOwner());
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var nearby = rangeMan.ExecuteQuery(this.entity, 0, -1, players, IID_ResourceDropsite);
for each (var ent in nearby)
{
var cmpDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (!cmpDropsite.AcceptsType(genericType))
continue;
return ent;
}
return undefined;
};
/**
* Returns the entity ID of the nearest building that needs to be constructed,
* or undefined if none can be found close enough.
*/
UnitAI.prototype.FindNearbyFoundation = function()
{
var range = 64; // TODO: what's a sensible number?
// Find buildings owned by this unit's player
var players = [];
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership)
players.push(cmpOwnership.GetOwner());
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, IID_Foundation);
for each (var ent in nearby)
{
// Skip foundations that are already complete. (This matters since
// we process the ConstructionFinished message before the foundation
// we're working on has been deleted.)
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation.IsFinished())
continue;
return ent;
}
return undefined;
};
/**
* Play a sound appropriate to the current entity.
*/
UnitAI.prototype.PlaySound = function(name)
{
// If we're a formation controller, use the sounds from our first member
if (this.IsFormationController())
{
var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
var member = cmpFormation.GetPrimaryMember();
if (member)
PlaySound(name, member);
}
else
{
// Otherwise use our own sounds
PlaySound(name, this.entity);
}
};
UnitAI.prototype.SelectAnimation = function(name, once, speed, sound)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
// Special case: the "move" animation gets turned into a special
// movement mode that deals with speeds and walk/run automatically
if (name == "move")
{
// Speed to switch from walking to running animations
var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2;
cmpVisual.SelectMovementAnimation(runThreshold);
return;
}
var soundgroup;
if (sound)
{
var cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
if (cmpSound)
soundgroup = cmpSound.GetSoundGroup(sound);
}
// Set default values if unspecified
if (once === undefined)
once = false;
if (speed === undefined)
speed = 1.0;
if (soundgroup === undefined)
soundgroup = "";
cmpVisual.SelectAnimation(name, once, speed, soundgroup);
};
UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetAnimationSyncRepeat(repeattime);
cmpVisual.SetAnimationSyncOffset(actiontime);
};
UnitAI.prototype.StopMoving = function()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpUnitMotion.StopMoving();
};
UnitAI.prototype.MoveToPoint = function(x, z)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToPointRange(x, z, 0, 0);
};
UnitAI.prototype.MoveToPointRange = function(x, z, rangeMin, rangeMax)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax);
};
UnitAI.prototype.MoveToTarget = function(target)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, 0, 0);
};
UnitAI.prototype.MoveToTargetRange = function(target, iid, type)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpRanged = Engine.QueryInterface(this.entity, iid);
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, range.min, range.max);
};
UnitAI.prototype.MoveToTargetRangeExplicit = function(target, min, max)
{
if (!this.CheckTargetVisible(target))
return false;
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.MoveToTargetRange(target, min, max);
};
UnitAI.prototype.CheckTargetRange = function(target, iid, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
};
UnitAI.prototype.CheckTargetRangeExplicit = function(target, min, max)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInTargetRange(target, min, max);
};
UnitAI.prototype.CheckGarrisonRange = function(target)
{
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpUnitMotion.IsInTargetRange(target, range.min, range.max);
};
/**
* Returns true if the target entity is visible through the FoW/SoD.
*/
UnitAI.prototype.CheckTargetVisible = function(target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
return false;
if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner(), false) == "hidden")
return false;
// Either visible directly, or visible in fog
return true;
};
UnitAI.prototype.FaceTowardsTarget = function(target)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var cmpTargetPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
var targetpos = cmpTargetPosition.GetPosition();
var angle = Math.atan2(targetpos.x - pos.x, targetpos.z - pos.z);
var rot = cmpPosition.GetRotation();
var delta = (rot.y - angle + Math.PI) % (2 * Math.PI) - Math.PI;
if (Math.abs(delta) > 0.2)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.FaceTowardsPoint(targetpos.x, targetpos.z);
}
};
UnitAI.prototype.CheckTargetDistanceFromHeldPosition = function(target, iid, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(type);
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return false;
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
var halfvision = cmpVision.GetRange() / 2;
var pos = cmpPosition.GetPosition();
var dx = this.heldPosition.x - pos.x;
var dz = this.heldPosition.z - pos.z;
var dist = Math.sqrt(dx*dx + dz*dz);
return dist < halfvision + range.max;
};
UnitAI.prototype.CheckTargetIsInVisionRange = function(target)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return false;
var range = cmpVision.GetRange();
var distance = DistanceBetweenEntities(this.entity,target);
return distance < range;
};
UnitAI.prototype.GetBestAttack = function()
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
return cmpAttack.GetBestAttack();
};
UnitAI.prototype.GetBestAttackAgainst = function(target)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return undefined;
return cmpAttack.GetBestAttackAgainst(target);
};
/**
* Try to find one of the given entities which can be attacked,
* and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackVisibleEntity = function(ents)
{
for each (var target in ents)
{
if (this.CanAttack(target))
{
this.PushOrderFront("Attack", { "target": target, "force": false });
return true;
}
}
return false;
};
/**
* Try to find one of the given entities which can be attacked
* and which is close to the hold position, and start attacking it.
* Returns true if it found something to attack.
*/
UnitAI.prototype.AttackEntityInZone = function(ents)
{
for each (var target in ents)
{
var type = this.GetBestAttackAgainst(target);
if (this.CanAttack(target) && this.CheckTargetDistanceFromHeldPosition(target, IID_Attack, type))
{
this.PushOrderFront("Attack", { "target": target, "force": false });
return true;
}
}
return false;
};
/**
* Try to respond appropriately given our current stance,
* given a list of entities that match our stance's target criteria.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToTargetedEntities = function(ents)
{
if (!ents.length)
return false;
if (this.GetStance().respondChase)
return this.AttackVisibleEntity(ents);
if (this.GetStance().respondStandGround)
return this.AttackVisibleEntity(ents);
if (this.GetStance().respondHoldGround)
return this.AttackEntityInZone(ents);
if (this.GetStance().respondFlee)
{
this.PushOrderFront("Flee", { "target": ents[0], "force": false });
return true;
}
return false;
};
/**
* Try to respond to healable entities.
* Returns true if it responded.
*/
UnitAI.prototype.RespondToHealableEntities = function(ents)
{
if (!ents.length)
return false;
for each (var ent in ents)
{
if (this.CanHeal(ent))
{
this.PushOrderFront("Heal", { "target": ent, "force": false });
return true;
}
}
return false;
};
/**
* Returns true if we should stop following the target entity.
*/
UnitAI.prototype.ShouldAbandonChase = function(target, force, iid)
{
// Stop if we're in hold-ground mode and it's too far from the holding point
if (this.GetStance().respondHoldGround)
{
if (!this.CheckTargetDistanceFromHeldPosition(target, iid, this.attackType))
return true;
}
// Stop if it's left our vision range, unless we're especially persistent
if (!force && !this.GetStance().respondChaseBeyondVision)
{
if (!this.CheckTargetIsInVisionRange(target))
return true;
}
// (Note that CCmpUnitMotion will detect if the target is lost in FoW,
// and will continue moving to its last seen position and then stop)
return false;
};
/*
* Returns whether we should chase the targeted entity,
* given our current stance.
*/
UnitAI.prototype.ShouldChaseTargetedEntity = function(target, force)
{
if (this.GetStance().respondChase)
return true;
if (force)
return true;
return false;
};
//// External interface functions ////
UnitAI.prototype.SetFormationController = function(ent)
{
this.formationController = ent;
// Set obstruction group, so we can walk through members
// of our own formation (or ourself if not in formation)
var cmpObstruction = Engine.QueryInterface(this.entity, IID_Obstruction);
if (cmpObstruction)
{
if (ent == INVALID_ENTITY)
cmpObstruction.SetControlGroup(this.entity);
else
cmpObstruction.SetControlGroup(ent);
}
// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
if (ent == INVALID_ENTITY)
UnitFsm.ProcessMessage(this, { "type": "FormationLeave" });
};
UnitAI.prototype.GetFormationController = function()
{
return this.formationController;
};
UnitAI.prototype.SetLastFormationName = function(name)
{
this.lastFormationName = name;
};
UnitAI.prototype.GetLastFormationName = function()
{
return this.lastFormationName;
};
/**
* Returns the estimated distance that this unit will travel before either
* finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
* Intended for Formation to switch to column layout on long walks.
*/
UnitAI.prototype.ComputeWalkingDistance = function()
{
var distance = 0;
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return 0;
// Keep track of the position at the start of each order
var pos = cmpPosition.GetPosition();
for (var i = 0; i < this.orderQueue.length; ++i)
{
var order = this.orderQueue[i];
switch (order.type)
{
case "Walk":
case "GatherNearPosition":
// Add the distance to the target point
var dx = order.data.x - pos.x;
var dz = order.data.z - pos.z;
var d = Math.sqrt(dx*dx + dz*dz);
distance += d;
// Remember this as the start position for the next order
pos = order.data;
break; // and continue the loop
case "WalkToTarget":
case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
case "Flee":
case "LeaveFoundation":
case "Attack":
case "Heal":
case "Gather":
case "ReturnResource":
case "Repair":
case "Garrison":
// Find the target unit's position
var cmpTargetPosition = Engine.QueryInterface(order.data.target, IID_Position);
if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld())
return distance;
var targetPos = cmpTargetPosition.GetPosition();
// Add the distance to the target unit
var dx = targetPos.x - pos.x;
var dz = targetPos.z - pos.z;
var d = Math.sqrt(dx*dx + dz*dz);
distance += d;
// Return the total distance to the target
return distance;
case "Stop":
return 0;
default:
error("ComputeWalkingDistance: Unrecognised order type '"+order.type+"'");
return distance;
}
}
// Return the total distance to the end of the order queue
return distance;
};
UnitAI.prototype.AddOrder = function(type, data, queued)
{
if (queued)
this.PushOrder(type, data);
else
this.ReplaceOrder(type, data);
};
/**
* Adds walk order to queue, forced by the player.
*/
UnitAI.prototype.Walk = function(x, z, queued)
{
this.AddOrder("Walk", { "x": x, "z": z, "force": true }, queued);
};
/**
* Adds stop order to queue, forced by the player.
*/
UnitAI.prototype.Stop = function(queued)
{
this.AddOrder("Stop", undefined, queued);
};
/**
* Adds walk-to-target order to queue, this only occurs in response
* to a player order, and so is forced.
*/
UnitAI.prototype.WalkToTarget = function(target, queued)
{
this.AddOrder("WalkToTarget", { "target": target, "force": true }, queued);
};
/**
* Adds leave foundation order to queue, treated as forced.
*/
UnitAI.prototype.LeaveFoundation = function(target)
{
// If we're already being told to leave a foundation, then
// ignore this new request so we don't end up being too indecisive
// to ever actually move anywhere
if (this.order && this.order.type == "LeaveFoundation")
return;
this.PushOrderFront("LeaveFoundation", { "target": target, "force": true });
};
/**
* Adds attack order to the queue, forced by the player.
*/
UnitAI.prototype.Attack = function(target, queued)
{
if (!this.CanAttack(target))
{
// We don't want to let healers walk to the target unit so they can be easily killed.
// Instead we just let them get into healing range.
if (this.IsHealer())
this.MoveToTargetRange(target, IID_Heal);
else
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Attack", { "target": target, "force": true }, queued);
};
/**
* Adds garrison order to the queue, forced by the player.
*/
UnitAI.prototype.Garrison = function(target, queued)
{
if (!this.CanGarrison(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Garrison", { "target": target, "force": true }, queued);
};
/**
* Adds ungarrison order to the queue.
*/
UnitAI.prototype.Ungarrison = function()
{
if (this.IsGarrisoned())
{
this.AddOrder("Ungarrison", null, false);
}
};
/**
* Adds gather order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Gather = function(target, queued)
{
this.PerformGather(target, queued, true);
};
/**
* Internal function to abstract the force parameter.
*/
UnitAI.prototype.PerformGather = function(target, queued, force)
{
if (!this.CanGather(target))
{
this.WalkToTarget(target, queued);
return;
}
// Save the resource type now, so if the resource gets destroyed
// before we process the order then we still know what resource
// type to look for more of
var cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply);
var type = cmpResourceSupply.GetType();
// Also save the target entity's template, so that if it's an animal,
// we won't go from hunting slow safe animals to dangerous fast ones
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(target);
// Remember the position of our target, if any, in case it disappears
// later and we want to head to its last known position
// (TODO: if the target moves a lot (e.g. it's an animal), maybe we
// need to update this lastPos regularly rather than just here?)
var lastPos = undefined;
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
lastPos = cmpPosition.GetPosition();
this.AddOrder("Gather", { "target": target, "type": type, "template": template, "lastPos": lastPos, "force": force }, queued);
};
/**
* Adds gather-near-position order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.GatherNearPosition = function(x, z, type, template, queued)
{
this.AddOrder("GatherNearPosition", { "type": type, "template": template, "x": x, "z": z, "force": false }, queued);
};
/**
* Adds heal order to the queue, forced by the player.
*/
UnitAI.prototype.Heal = function(target, queued)
{
if (!this.CanHeal(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Heal", { "target": target, "force": true }, queued);
};
/**
* Adds return resource order to the queue, forced by the player.
*/
UnitAI.prototype.ReturnResource = function(target, queued)
{
if (!this.CanReturnResource(target, true))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("ReturnResource", { "target": target, "force": true }, queued);
};
/**
* Adds trade order to the queue. Either walk to the first market, or
* start a new route. Not forced, so it can be interrupted by attacks.
*/
UnitAI.prototype.SetupTradeRoute = function(target, source, queued)
{
if (!this.CanTrade(target))
{
this.WalkToTarget(target, queued);
return;
}
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
var marketsChanged = cmpTrader.SetTargetMarket(target, source);
if (marketsChanged)
{
if (cmpTrader.HasBothMarkets())
this.AddOrder("Trade", { "firstMarket": cmpTrader.GetFirstMarket(), "secondMarket": cmpTrader.GetSecondMarket(), "force": false }, queued);
else
this.WalkToTarget(cmpTrader.GetFirstMarket(), queued);
}
};
UnitAI.prototype.MoveToMarket = function(targetMarket)
{
if (this.MoveToTarget(targetMarket))
{
// We've started walking to the market
return true;
}
else
{
// We can't reach the market.
// Give up.
this.StopMoving();
this.StopTrading();
return false;
}
};
UnitAI.prototype.PerformTradeAndMoveToNextMarket = function(currentMarket, nextMarket, nextFsmStateName)
{
if (!this.CanTrade(currentMarket))
{
this.StopTrading();
return;
}
if (this.CheckTargetRange(currentMarket, IID_Trader))
{
this.PerformTrade();
if (this.MoveToMarket(nextMarket))
{
// We've started walking to the next market
this.SetNextState(nextFsmStateName);
}
}
else
{
// If the current market is not reached try again
this.MoveToMarket(currentMarket);
}
};
UnitAI.prototype.PerformTrade = function()
{
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
cmpTrader.PerformTrade();
};
UnitAI.prototype.StopTrading = function()
{
this.FinishOrder();
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
cmpTrader.StopTrading();
};
/**
* Adds repair/build order to the queue, forced by the player
* until the target is reached
*/
UnitAI.prototype.Repair = function(target, autocontinue, queued)
{
if (!this.CanRepair(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Repair", { "target": target, "autocontinue": autocontinue, "force": true }, queued);
};
/**
* Adds flee order to the queue, not forced, so it can be
* interrupted by attacks.
*/
UnitAI.prototype.Flee = function(target, queued)
{
this.AddOrder("Flee", { "target": target, "force": false }, queued);
};
/**
* Adds cheer order to the queue. Forced so it won't be interrupted by attacks.
*/
UnitAI.prototype.Cheer = function()
{
this.AddOrder("Cheering", { "force": true }, false);
};
UnitAI.prototype.SetStance = function(stance)
{
if (g_Stances[stance])
this.stance = stance;
else
error("UnitAI: Setting to invalid stance '"+stance+"'");
};
UnitAI.prototype.SwitchToStance = function(stance)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.x, pos.z);
this.SetStance(stance);
// Stop moving if switching to stand ground
// TODO: Also stop existing orders in a sensible way
if (stance == "standground")
this.StopMoving();
// Reset the range queries, since the range depends on stance.
this.SetupRangeQueries();
};
/**
* Resets losRangeQuery, and if there are some targets in range that we can
* attack then we start attacking and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewTargets = function()
{
if (!this.losRangeQuery)
return false;
if (!this.GetStance().targetVisibleEnemies)
return false;
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (this.AttackEntitiesByPreference( rangeMan.ResetActiveQuery(this.losRangeQuery) ))
return true;
// If no regular enemies were found, attempt to attack a hostile Gaia entity.
else if (this.losGaiaRangeQuery)
return this.AttackGaiaEntitiesByPreference( rangeMan.ResetActiveQuery(this.losGaiaRangeQuery) );
return false;
};
/**
* Resets losHealRangeQuery, and if there are some targets in range that we can heal
* then we start healing and this returns true; otherwise, returns false.
*/
UnitAI.prototype.FindNewHealTargets = function()
{
if (!this.losHealRangeQuery)
return false;
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery);
for each (var ent in ents)
{
if (this.CanHeal(ent))
{
this.PushOrderFront("Heal", { "target": ent, "force": false });
return true;
}
}
// We haven't found any target to heal
return false;
};
UnitAI.prototype.GetQueryRange = function(iid)
{
var ret = { "min": 0, "max": 0 };
if (this.GetStance().respondStandGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack());
ret.min = range.min;
ret.max = range.max;
}
else if (this.GetStance().respondChase)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var range = cmpVision.GetRange();
ret.max = range;
}
else if (this.GetStance().respondHoldGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
if (!cmpRanged)
return ret;
var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack());
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var halfvision = cmpVision.GetRange() / 2;
ret.max = range.max + halfvision;
}
// We probably have stance 'passive' and we wouldn't have a range,
// but as it is the default for healers we need to set it to something sane.
else if (iid === IID_Heal)
{
var cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
if (!cmpVision)
return ret;
var range = cmpVision.GetRange();
ret.max = range;
}
return ret;
};
UnitAI.prototype.GetStance = function()
{
return g_Stances[this.stance];
};
UnitAI.prototype.GetStanceName = function()
{
return this.stance;
};
UnitAI.prototype.SetMoveSpeed = function(speed)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpMotion.SetSpeed(speed);
};
UnitAI.prototype.SetHeldPosition = function(x, z)
{
this.heldPosition = {"x": x, "z": z};
};
UnitAI.prototype.GetHeldPosition = function(pos)
{
return this.heldPosition;
};
UnitAI.prototype.WalkToHeldPosition = function()
{
if (this.heldPosition)
{
this.AddOrder("Walk", { "x": this.heldPosition.x, "z": this.heldPosition.z, "force": false }, false);
return true;
}
return false;
};
//// Helper functions ////
UnitAI.prototype.CanAttack = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Attack commands
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
if (!cmpAttack.CanAttack(target))
return false;
// Verify that the target is alive
if (!this.TargetIsAlive(target))
return false;
// Verify that the target is owned by an enemy of this entity's player,
// or that it's an attackable resource supply like a domestic animal
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || (!this.MustKillGatherTarget(target) && !IsOwnedByEnemyOfPlayer(cmpOwnership.GetOwner(), target)))
return false;
return true;
};
UnitAI.prototype.CanGarrison = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder);
if (!cmpGarrisonHolder)
return false;
// Verify that the target is owned by this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
return false;
// Don't let animals garrison for now
// (If we want to support that, we'll need to change Order.Garrison so it
// doesn't move the animal into an INVIDIDUAL.* state)
if (this.IsAnimal())
return false;
return true;
};
UnitAI.prototype.CanGather = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Gather commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
// Verify that we can gather from this target
if (!cmpResourceGatherer.GetTargetGatherRate(target))
return false;
// No need to verify ownership as we should be able to gather from
// a target regardless of ownership.
return true;
};
UnitAI.prototype.CanHeal = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Heal commands
var cmpHeal = Engine.QueryInterface(this.entity, IID_Heal);
if (!cmpHeal)
return false;
// Verify that the target is alive
if (!this.TargetIsAlive(target))
return false;
// Verify that the target is owned by the same player as the entity or of an ally
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !(IsOwnedByPlayer(cmpOwnership.GetOwner(), target) || IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)))
return false;
// Verify that the target is not unhealable (or at max health)
var cmpHealth = Engine.QueryInterface(target, IID_Health);
if (!cmpHealth || cmpHealth.IsUnhealable())
return false;
// Verify that the target has no unhealable class
var cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return false;
for each (var unhealableClass in cmpHeal.GetUnhealableClasses())
{
if (cmpIdentity.HasClass(unhealableClass) != -1)
{
return false;
}
}
// Verify that the target is a healable class
var healable = false;
for each (var healableClass in cmpHeal.GetHealableClasses())
{
if (cmpIdentity.HasClass(healableClass) != -1)
{
healable = true;
}
}
if (!healable)
return false;
return true;
};
UnitAI.prototype.CanReturnResource = function(target, checkCarriedResource)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to ReturnResource commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
return false;
// Verify that the target is a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite);
if (!cmpResourceDropsite)
return false;
if (checkCarriedResource)
{
// Verify that we are carrying some resources,
// and can return our current resource to this target
var type = cmpResourceGatherer.GetMainCarryingType();
if (!type || !cmpResourceDropsite.AcceptsType(type))
return false;
}
// Verify that the dropsite is owned by this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !IsOwnedByPlayer(cmpOwnership.GetOwner(), target))
return false;
return true;
};
UnitAI.prototype.CanTrade = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Trade commands
var cmpTrader = Engine.QueryInterface(this.entity, IID_Trader);
if (!cmpTrader || !cmpTrader.CanTrade(target))
return false;
return true;
};
UnitAI.prototype.CanRepair = function(target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Repair (Builder) commands
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpBuilder)
return false;
// Verify that the target is owned by an ally of this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target))
return false;
return true;
};
//// Animal specific functions ////
UnitAI.prototype.MoveRandomly = function(distance)
{
// We want to walk in a random direction, but avoid getting stuck
// in obstacles or narrow spaces.
// So pick a circular range from approximately our current position,
// and move outwards to the nearest point on that circle, which will
// lead to us avoiding obstacles and moving towards free space.
// TODO: we probably ought to have a 'home' point, and drift towards
// that, so we don't spread out all across the whole map
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
if (!cmpPosition)
return;
if (!cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
var jitter = 0.5;
// Randomly adjust the range's center a bit, so we tend to prefer
// moving in random directions (if there's nothing in the way)
var tx = pos.x + (2*Math.random()-1)*jitter;
var tz = pos.z + (2*Math.random()-1)*jitter;
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
cmpMotion.MoveToPointRange(tx, tz, distance, distance);
};
UnitAI.prototype.AttackEntitiesByPreference = function(ents)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
return this.RespondToTargetedEntities(
ents.filter(function (v, i, a) { return cmpAttack.CanAttack(v); })
.sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); })
);
};
UnitAI.prototype.AttackGaiaEntitiesByPreference = function(ents)
{
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
return false;
const filter = function(e) {
var cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
return (cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()));
};
return this.RespondToTargetedEntities(
ents.filter(function (v, i, a) { return cmpAttack.CanAttack(v) && filter(v); })
.sort(function (a, b) { return cmpAttack.CompareEntitiesByPreference(a, b); })
);
};
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot/worker.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot/worker.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot/worker.js (revision 12343)
@@ -1,247 +1,247 @@
/**
* This class makes a worker do as instructed by the economy manager
*/
var Worker = function(ent) {
this.ent = ent;
this.approachCount = 0;
};
Worker.prototype.update = function(gameState) {
var subrole = this.ent.getMetadata("subrole");
if (!this.ent.position()){
// If the worker has no position then no work can be done
return;
}
if (subrole === "gatherer"){
- if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData().type
- && this.getResourceType(this.ent.unitAIOrderData().type) === this.ent.getMetadata("gather-type"))
+ if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0].type
+ && this.getResourceType(this.ent.unitAIOrderData()[0].type) === this.ent.getMetadata("gather-type"))
&& !(this.ent.unitAIState().split(".")[1] === "RETURNRESOURCE")){
// TODO: handle combat for hunting animals
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 ||
this.ent.resourceCarrying()[0].type === this.ent.getMetadata("gather-type")){
Engine.ProfileStart("Start Gathering");
this.startGathering(gameState);
Engine.ProfileStop();
} else if (this.ent.unitAIState().split(".")[1] !== "RETURNRESOURCE") {
// Should deposit resources
Engine.ProfileStart("Return Resources");
this.returnResources(gameState);
Engine.ProfileStop();
}
this.startApproachingResourceTime = gameState.getTimeElapsed();
//Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [10,0,0]});
}else{
// If we haven't reached the resource in 2 minutes twice in a row and none of the resource has been
// gathered then mark it as inaccessible.
if (gameState.getTimeElapsed() - this.startApproachingResourceTime > 120000){
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
if (ent && ent.resourceSupplyAmount() == ent.resourceSupplyMax()){
if (this.approachCount > 0){
ent.setMetadata("inaccessible", true);
this.ent.setMetadata("subrole", "idle");
}
this.approachCount++;
}else{
this.approachCount = 0;
}
this.startApproachingResourceTime = gameState.getTimeElapsed();
}
}
}
}else if(subrole === "builder"){
if (this.ent.unitAIState().split(".")[1] !== "REPAIR"){
var target = this.ent.getMetadata("target-foundation");
this.ent.repair(target);
}
//Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [0,10,0]});
}
Engine.ProfileStart("Update Gatherer Counts");
this.updateGathererCounts(gameState);
Engine.ProfileStop();
};
Worker.prototype.updateGathererCounts = function(gameState, dead){
// update gatherer counts for the resources
if (this.ent.unitAIState().split(".")[2] === "GATHERING" && !dead){
- if (this.gatheringFrom !== this.ent.unitAIOrderData().target){
+ if (this.gatheringFrom !== this.ent.unitAIOrderData()[0].target){
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
if (ent){
ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
this.markFull(ent);
}
}
- this.gatheringFrom = this.ent.unitAIOrderData().target;
+ this.gatheringFrom = this.ent.unitAIOrderData()[0].target;
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
if (ent){
ent.setMetadata("gatherer-count", (ent.getMetadata("gatherer-count") || 0) + 1);
this.markFull(ent);
}
}
}
}else{
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
if (ent){
ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
this.markFull(ent);
}
this.gatheringFrom = undefined;
}
}
};
Worker.prototype.markFull = function(ent){
var maxCounts = {"food": 20, "wood": 5, "metal": 20, "stone": 20, "treasure": 1};
if (ent.resourceSupplyType() && ent.getMetadata("gatherer-count") >= maxCounts[ent.resourceSupplyType().generic]){
if (!ent.getMetadata("full")){
ent.setMetadata("full", true);
}
}else{
if (ent.getMetadata("full")){
ent.setMetadata("full", false);
}
}
};
Worker.prototype.startGathering = function(gameState){
var resource = this.ent.getMetadata("gather-type");
var ent = this.ent;
if (!ent.position()){
// TODO: work out what to do when entity has no position
return;
}
// find closest dropsite which has nearby resources of the correct type
var minDropsiteDist = Math.min(); // set to infinity initially
var nearestResources = undefined;
var nearestDropsite = undefined;
gameState.updatingCollection("active-dropsite-" + resource, Filters.byMetadata("active-dropsite-" + resource, true),
gameState.getOwnDropsites(resource)).forEach(function (dropsite){
if (dropsite.position()){
var dist = VectorDistance(ent.position(), dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
nearestResources = dropsite.getMetadata("nearby-resources-" + resource);
nearestDropsite = dropsite;
}
}
});
if (!nearestResources || nearestResources.length === 0){
nearestResources = gameState.getResourceSupplies(resource);
gameState.getOwnDropsites(resource).forEach(function (dropsite){
if (dropsite.position()){
var dist = VectorDistance(ent.position(), dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
nearestDropsite = dropsite;
}
}
});
}
if (nearestResources.length === 0){
debug("No " + resource + " found! (1)");
return;
}
var supplies = [];
var nearestSupplyDist = Math.min();
var nearestSupply = undefined;
nearestResources.forEach(function(supply) {
// TODO: handle enemy territories
if (!supply.position()){
return;
}
// measure the distance to the resource
var dist = VectorDistance(supply.position(), ent.position());
// Add on a factor for the nearest dropsite if one exists
if (nearestDropsite){
dist += 5 * VectorDistance(supply.position(), nearestDropsite.position());
}
// Go for treasure as a priority
if (dist < 1000 && supply.resourceSupplyType().generic == "treasure"){
dist /= 1000;
}
if (dist < nearestSupplyDist){
nearestSupplyDist = dist;
nearestSupply = supply;
}
});
if (nearestSupply) {
var pos = nearestSupply.position();
var territoryOwner = gameState.getTerritoryMap().getOwner(pos);
if (!gameState.ai.accessibility.isAccessible(pos) ||
(territoryOwner != gameState.getPlayerID() && territoryOwner != 0)){
nearestSupply.setMetadata("inaccessible", true);
}else{
ent.gather(nearestSupply);
}
}else{
debug("No " + resource + " found! (2)");
}
};
// Makes the worker deposit the currently carried resources at the closest dropsite
Worker.prototype.returnResources = function(gameState){
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0){
return;
}
var resource = this.ent.resourceCarrying()[0].type;
var self = this;
if (!this.ent.position()){
// TODO: work out what to do when entity has no position
return;
}
var closestDropsite = undefined;
var dist = Math.min();
gameState.getOwnDropsites(resource).forEach(function(dropsite){
if (dropsite.position()){
var d = VectorDistance(self.ent.position(), dropsite.position());
if (d < dist){
dist = d;
closestDropsite = dropsite;
}
}
});
if (!closestDropsite){
debug("No dropsite found for " + resource);
return;
}
this.ent.returnResources(closestDropsite);
};
Worker.prototype.getResourceType = function(type){
if (!type || !type.generic){
return undefined;
}
if (type.generic === "treasure"){
return type.specific;
}else{
return type.generic;
}
};
\ No newline at end of file
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/qbot.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/qbot.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/qbot.js (revision 12343)
@@ -1,187 +1,201 @@
function QBotAI(settings) {
BaseAI.call(this, settings);
this.turn = 0;
this.playedTurn = 0;
this.modules = {
"economy": new EconomyManager(),
"military": new MilitaryAttackManager(),
"housing": new HousingManager()
};
// this.queues can only be modified by the queue manager or things will go awry.
this.queues = {
house : new Queue(),
citizenSoldier : new Queue(),
villager : new Queue(),
economicBuilding : new Queue(),
field : new Queue(),
advancedSoldier : new Queue(),
siege : new Queue(),
militaryBuilding : new Queue(),
defenceBuilding : new Queue(),
civilCentre: new Queue()
};
this.productionQueues = [];
this.priorities = Config.priorities;
this.queueManager = new QueueManager(this.queues, this.priorities);
this.firstTime = true;
this.savedEvents = [];
+
+ this.defcon = 5;
+ this.defconChangeTime = -10000000;
}
QBotAI.prototype = new BaseAI();
//Some modules need the gameState to fully initialise
QBotAI.prototype.runInit = function(gameState){
if (this.firstTime){
for (var i in this.modules){
if (this.modules[i].init){
this.modules[i].init(gameState);
}
}
this.timer = new Timer();
this.firstTime = false;
var myKeyEntities = gameState.getOwnEntities().filter(function(ent) {
return ent.hasClass("CivCentre");
});
if (myKeyEntities.length == 0){
myKeyEntities = gameState.getOwnEntities();
}
var filter = Filters.byClass("CivCentre");
var enemyKeyEntities = gameState.getEnemyEntities().filter(filter);
if (enemyKeyEntities.length == 0){
enemyKeyEntities = gameState.getEnemyEntities();
}
this.accessibility = new Accessibility(gameState, myKeyEntities.toEntityArray()[0].position());
if (enemyKeyEntities.length == 0)
return;
var pathFinder = new PathFinder(gameState);
this.pathsToMe = pathFinder.getPaths(enemyKeyEntities.toEntityArray()[0].position(), myKeyEntities.toEntityArray()[0].position(), 'entryPoints');
this.templateManager = new TemplateManager(gameState);
-
-
+ this.distanceFromMeMap = new Map(gameState);
+ this.distanceFromMeMap.drawDistance(gameState,myKeyEntities.toEntityArray());
+ //this.distanceFromMeMap.dumpIm("dumping.png", this.distanceFromMeMap.width*1.5);
}
};
QBotAI.prototype.OnUpdate = function() {
if (this.gameFinished){
return;
}
if (this.events.length > 0){
this.savedEvents = this.savedEvents.concat(this.events);
}
+ if (this.turn == 0) {
+ debug ("Initializing");
+ var gameState = new GameState(this);
+ this.runInit(gameState);
+ }
+
// Run the update every n turns, offset depending on player ID to balance
// the load
if ((this.turn + this.player) % 10 == 0) {
Engine.ProfileStart("qBot-xp");
this.playedTurn++;
var gameState = new GameState(this);
if (gameState.getOwnEntities().length === 0){
Engine.ProfileStop();
return; // With no entities to control the AI cannot do anything
}
+ // defcon cooldown
+ if (this.defcon < 5 && gameState.timeSinceDefconChange() > 20000)
+ this.defcon++;
+
this.runInit(gameState);
for (var i in this.modules){
this.modules[i].update(gameState, this.queues, this.savedEvents);
}
- this.updateDynamicPriorities(gameState, this.queues);
+ //this.updateDynamicPriorities(gameState, this.queues);
this.queueManager.update(gameState);
// Generate some entropy in the random numbers (against humans) until the engine gets random initialised numbers
// TODO: remove this when the engine gives a random seed
var n = this.savedEvents.length % 29;
for (var i = 0; i < n; i++){
Math.random();
}
delete this.savedEvents;
this.savedEvents = [];
Engine.ProfileStop();
}
this.turn++;
};
QBotAI.prototype.updateDynamicPriorities = function(gameState, queues){
// Dynamically change priorities
Engine.ProfileStart("Change Priorities");
var females = gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen"));
var femalesTarget = this.modules["economy"].targetNumWorkers;
var enemyStrength = this.modules["military"].measureEnemyStrength(gameState);
var availableStrength = this.modules["military"].measureAvailableStrength();
var additionalPriority = (enemyStrength - availableStrength) * 5;
additionalPriority = Math.min(Math.max(additionalPriority, -50), 220);
var advancedProportion = (availableStrength / 40) * (females/femalesTarget);
advancedProportion = Math.min(advancedProportion, 0.7);
this.priorities.advancedSoldier = advancedProportion * (150 + additionalPriority) + 1;
if (females/femalesTarget > 0.7){
this.priorities.defenceBuilding = 70;
}
Engine.ProfileStop();
};
// TODO: Remove override when the whole AI state is serialised
QBotAI.prototype.Deserialize = function(data)
{
BaseAI.prototype.Deserialize.call(this, data);
};
// Override the default serializer
QBotAI.prototype.Serialize = function()
{
var ret = BaseAI.prototype.Serialize.call(this);
ret._entityMetadata = {};
return ret;
};
function debug(output){
if (Config.debug){
if (typeof output === "string"){
warn(output);
}else{
warn(uneval(output));
}
}
}
function copyPrototype(descendant, parent) {
var sConstructor = parent.toString();
var aMatch = sConstructor.match( /\s*function (.*)\(/ );
if ( aMatch != null ) { descendant.prototype[aMatch[1]] = parent; }
for (var m in parent.prototype) {
descendant.prototype[m] = parent.prototype[m];
}
}
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/config.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/config.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/config.js (revision 12343)
@@ -1,64 +1,64 @@
var baseConfig = {
"attack" : {
"minAttackSize" : 20, // attackMoveToLocation
"maxAttackSize" : 60, // attackMoveToLocation
"enemyRatio" : 1.5, // attackMoveToLocation
"groupSize" : 10 // military
},
// defence
"defence" : {
"acquireDistance" : 220,
"releaseDistance" : 250,
"groupRadius" : 20,
"groupBreakRadius" : 40,
"groupMergeRadius" : 10,
"defenderRatio" : 2
},
// military
"buildings" : {
"moderate" : {
"default" : [ "structures/{civ}_barracks" ]
},
"advanced" : {
"hele" : [ "structures/{civ}_gymnasion", "structures/{civ}_fortress" ],
"athen" : [ "structures/{civ}_gymnasion", "structures/{civ}_fortress" ],
"spart" : [ "structures/{civ}_syssiton", "structures/{civ}_fortress" ],
"mace" : [ "structures/{civ}_fortress" ],
"cart" : [ "structures/{civ}_fortress", "structures/{civ}_embassy_celtic",
"structures/{civ}_embassy_iberian", "structures/{civ}_embassy_italiote" ],
"celt" : [ "structures/{civ}_kennel", "structures/{civ}_fortress_b", "structures/{civ}_fortress_g" ],
"iber" : [ "structures/{civ}_fortress" ],
"pers" : [ "structures/{civ}_fortress", "structures/{civ}_stables", "structures/{civ}_apadana" ],
"rome" : [ "structures/{civ}_army_camp", "structures/{civ}_fortress" ],
"maur" : [ "structures/{civ}_elephant_stables", "structures/{civ}_fortress" ]
},
"fort" : {
"default" : [ "structures/{civ}_fortress" ],
"celt" : [ "structures/{civ}_fortress_b", "structures/{civ}_fortress_g" ]
}
},
// qbot
"priorities" : { // Note these are dynamic, you are only setting the initial values
"house" : 500,
- "citizenSoldier" : 100,
- "villager" : 150,
- "economicBuilding" : 50,
+ "citizenSoldier" : 65,
+ "villager" : 95,
+ "economicBuilding" : 80,
"field" : 20,
"advancedSoldier" : 30,
"siege" : 10,
- "militaryBuilding" : 80,
+ "militaryBuilding" : 90,
"defenceBuilding" : 17,
"civilCentre" : 1000
},
"debug" : false
};
var Config = {
- "debug": false
+ "debug": true
};
Config.__proto__ = baseConfig;
\ No newline at end of file
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/enemy-watcher.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/enemy-watcher.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/enemy-watcher.js (revision 12343)
@@ -1,195 +1,198 @@
/*
* A class that keeps track of enem buildings, units, and pretty much anything I can think of (still a LOT TODO here)
* Only watches one enemy, you'll need one per enemy.
*/
var enemyWatcher = function(gameState, playerToWatch) {
this.watched = playerToWatch;
// creating fitting entity collections
var filter = Filters.and(Filters.byClass("Structure"), Filters.byOwner(this.watched));
this.enemyBuildings = gameState.getEnemyEntities().filter(filter);
this.enemyBuildings.registerUpdates();
filter = Filters.and(Filters.byClass("Worker"), Filters.byOwner(this.watched));
this.enemyCivilians = gameState.getEnemyEntities().filter(filter);
this.enemyCivilians.registerUpdates();
filter = Filters.and(Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), Filters.byOwner(this.watched));
this.enemySoldiers = gameState.getEnemyEntities().filter(filter);
this.enemySoldiers.registerUpdates();
// okay now we register here only enemy soldiers that we are monitoring (ie we see as part of an army…)
filter = Filters.and(Filters.byClassesOr(["CitizenSoldier", "Hero", "Champion", "Siege"]), Filters.and(Filters.byMetadata("monitored","true"),Filters.byOwner(this.watched)));
this.monitoredEnemySoldiers = gameState.getEnemyEntities().filter(filter);
this.monitoredEnemySoldiers.registerUpdates();
// and here those that we do not monitor
filter = Filters.and(Filters.byClassesOr(["CitizenSoldier","Hero","Champion","Siege"]), Filters.and(Filters.not(Filters.byMetadata("monitored","true")),Filters.byOwner(this.watched)));
this.unmonitoredEnemySoldiers = gameState.getEnemyEntities().filter(filter);
this.unmonitoredEnemySoldiers.registerUpdates();
// entity collections too.
this.armies = {};
this.enemyBuildingClass = {};
this.totalNBofArmies = 0;
// this is an array of integers, refering to "this.armies[ XX ]"
this.dangerousArmies = [];
};
enemyWatcher.prototype.getAllEnemySoldiers = function() {
return this.enemySoldiers;
};
enemyWatcher.prototype.getAllEnemyBuildings = function() {
return this.enemyBuildings;
};
enemyWatcher.prototype.getEnemyBuildings = function(specialClass, OneTime) {
var filter = Filters.byClass(specialClass);
var returnable = this.enemyBuildings.filter(filter);
if (!this.enemyBuildingClass[specialClass] && !OneTime) {
this.enemyBuildingClass[specialClass] = returnable;
this.enemyBuildingClass[specialClass].registerUpdates();
return this.enemyBuildingClass[specialClass];
}
return returnable;
};
enemyWatcher.prototype.getDangerousArmies = function() {
var toreturn = {};
for (i in this.dangerousArmies)
toreturn[this.dangerousArmies[i]] = this.armies[this.dangerousArmies[i]];
return toreturn;
};
enemyWatcher.prototype.getSafeArmies = function() {
var toreturn = {};
for (i in this.armies)
if (this.dangerousArmies.indexOf(i) == -1)
toreturn[i] = this.armies[i];
return toreturn;
};
enemyWatcher.prototype.resetDangerousArmies = function() {
this.dangerousArmies = [];
};
enemyWatcher.prototype.setAsDangerous = function(armyID) {
if (this.dangerousArmies.indexOf(armyID) === -1)
this.dangerousArmies.push(armyID);
};
enemyWatcher.prototype.isDangerous = function(armyID) {
if (this.dangerousArmies.indexOf(armyID) === -1)
return false;
return true;
};
// returns [id, army]
enemyWatcher.prototype.getArmyFromMember = function(memberID) {
for (i in this.armies) {
if (this.armies[i].toIdArray().indexOf(memberID) !== -1)
return [i,this.armies[i]];
}
return undefined;
};
enemyWatcher.prototype.isPartOfDangerousArmy = function(memberID) {
var armyID = this.getArmyFromMember(memberID)[0];
if (this.isDangerous(armyID))
return true;
return false;
};
enemyWatcher.prototype.cleanDebug = function() {
for (armyID in this.armies) {
var army = this.armies[armyID];
debug ("Army " +armyID);
debug (army.length +" members, centered around " +army.getCentrePosition());
}
}
// this will monitor any unmonitored soldier.
enemyWatcher.prototype.detectArmies = function(gameState){
//this.cleanDebug();
var self = this;
// let's loop through unmonitored enemy soldiers
this.unmonitoredEnemySoldiers.forEach( function (enemy) {
if (enemy.position() == undefined)
return;
// this was an unmonitored unit, we do not know any army associated with it. We assign it a new army (we'll merge later if needed)
enemy.setMetadata("monitored","true");
var armyID = uneval( gameState.player + "" + self.totalNBofArmies);
self.totalNBofArmies++,
enemy.setMetadata("EnemyWatcherArmy",armyID);
var filter = Filters.byMetadata("EnemyWatcherArmy",armyID);
var army = self.enemySoldiers.filter(filter);
self.armies[armyID] = army;
self.armies[armyID].registerUpdates();
self.armies[armyID].length;
});
this.mergeArmies(); // calls "scrap empty armies"
this.splitArmies(gameState);
};
// this will merge any two army who are too close together. The distance for "army" is fairly big.
// note: this doesn't actually merge two entity collections... It simply changes the unit metadatas, and will clear the empty entity collection
enemyWatcher.prototype.mergeArmies = function(){
for (army in this.armies) {
var firstArmy = this.armies[army];
if (firstArmy.length > 0)
for (otherArmy in this.armies) {
if (otherArmy !== army && this.armies[otherArmy].length > 0) {
var secondArmy = this.armies[otherArmy];
// we're not self merging, so we check if the two armies are close together
if (inRange(firstArmy.getCentrePosition(),secondArmy.getCentrePosition(), 3000 ) ) {
// okay so we merge the two together
// if the other one was dangerous and we weren't, we're now.
if (this.dangerousArmies.indexOf(otherArmy) !== -1 && this.dangerousArmies.indexOf(army) === -1)
this.dangerousArmies.push(army);
secondArmy.forEach( function(ent) {
ent.setMetadata("EnemyWatcherArmy",army);
});
}
}
}
}
this.ScrapEmptyArmies();
};
enemyWatcher.prototype.ScrapEmptyArmies = function(){
var removelist = [];
for (army in this.armies) {
if (this.armies[army].length === 0) {
removelist.push(army);
// if the army was dangerous, we remove it from the list
if (this.dangerousArmies.indexOf(army) !== -1)
this.dangerousArmies.splice(this.dangerousArmies.indexOf(army),1);
}
}
for each (toRemove in removelist) {
delete this.armies[toRemove];
}
};
// splits any unit too far from the centerposition
enemyWatcher.prototype.splitArmies = function(gameState){
var self = this;
for (armyID in this.armies) {
var army = this.armies[armyID];
var centre = army.getCentrePosition();
army.forEach( function (enemy) {
if (enemy.position() == undefined)
return;
+
+ // debug ("entity " +enemy.templateName() + " is currently " +enemy.visibility(gameState.player));
+
if (!inRange(enemy.position(),centre, 3500) ) {
var newArmyID = uneval( gameState.player + "" + self.totalNBofArmies);
if (self.dangerousArmies.indexOf(armyID) !== -1)
self.dangerousArmies.push(newArmyID);
self.totalNBofArmies++,
enemy.setMetadata("EnemyWatcherArmy",newArmyID);
var filter = Filters.byMetadata("EnemyWatcherArmy",newArmyID);
var newArmy = self.enemySoldiers.filter(filter);
self.armies[newArmyID] = newArmy;
self.armies[newArmyID].registerUpdates();
self.armies[newArmyID].length;
}
});
}
};
\ No newline at end of file
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/worker.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/worker.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/worker.js (revision 12343)
@@ -1,274 +1,350 @@
/**
* This class makes a worker do as instructed by the economy manager
*/
var Worker = function(ent) {
this.ent = ent;
this.approachCount = 0;
};
Worker.prototype.update = function(gameState) {
var subrole = this.ent.getMetadata("subrole");
if (!this.ent.position()){
// If the worker has no position then no work can be done
return;
}
if (subrole === "gatherer"){
- if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData().type
- && this.getResourceType(this.ent.unitAIOrderData().type) === this.ent.getMetadata("gather-type"))
+ if (!(this.ent.unitAIState().split(".")[1] === "GATHER" && this.ent.unitAIOrderData()[0].type
+ && this.getResourceType(this.ent.unitAIOrderData()[0].type) === this.ent.getMetadata("gather-type"))
&& !(this.ent.unitAIState().split(".")[1] === "RETURNRESOURCE")){
// TODO: handle combat for hunting animals
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0 ||
this.ent.resourceCarrying()[0].type === this.ent.getMetadata("gather-type")){
Engine.ProfileStart("Start Gathering");
this.startGathering(gameState);
Engine.ProfileStop();
} else if (this.ent.unitAIState().split(".")[1] !== "RETURNRESOURCE") {
// Should deposit resources
Engine.ProfileStart("Return Resources");
this.returnResources(gameState);
Engine.ProfileStop();
}
this.startApproachingResourceTime = gameState.getTimeElapsed();
//Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [10,0,0]});
}else{
// If we haven't reached the resource in 1 minutes twice in a row and none of the resource has been
// gathered then mark it as inaccessible.
if (gameState.getTimeElapsed() - this.startApproachingResourceTime > 60000){
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
if (ent && ent.resourceSupplyAmount() == ent.resourceSupplyMax()){
- if (this.approachCount > 0){
+ // if someone gathers from it, it's only that the pathfinder sucks.
+ if (this.approachCount > 0 && ent.getMetadata("gatherer-count") <= 2){
ent.setMetadata("inaccessible", true);
this.ent.setMetadata("subrole", "idle");
+ this.ent.flee(ent);
}
this.approachCount++;
}else{
this.approachCount = 0;
}
this.startApproachingResourceTime = gameState.getTimeElapsed();
}
}
}
- }else if(subrole === "builder"){
+ } else if(subrole === "builder") {
if (this.ent.unitAIState().split(".")[1] !== "REPAIR"){
var target = this.ent.getMetadata("target-foundation");
this.ent.repair(target);
}
//Engine.PostCommand({"type": "set-shading-color", "entities": [this.ent.id()], "rgb": [0,10,0]});
}
Engine.ProfileStart("Update Gatherer Counts");
this.updateGathererCounts(gameState);
Engine.ProfileStop();
};
Worker.prototype.updateGathererCounts = function(gameState, dead){
// update gatherer counts for the resources
if (this.ent.unitAIState().split(".")[2] === "GATHERING" && !dead){
- if (this.gatheringFrom !== this.ent.unitAIOrderData().target){
+ if (this.gatheringFrom !== this.ent.unitAIOrderData()[0].target){
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
- if (ent){
+ if (ent && ent.resourceSupplyType()){
ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
- this.markFull(ent);
+ this.markFull(gameState,ent);
}
}
- this.gatheringFrom = this.ent.unitAIOrderData().target;
+ this.gatheringFrom = this.ent.unitAIOrderData()[0].target;
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
- if (ent){
+ if (ent && ent.resourceSupplyType()){
ent.setMetadata("gatherer-count", (ent.getMetadata("gatherer-count") || 0) + 1);
- this.markFull(ent);
+ this.markFull(gameState,ent);
}
}
}
- }else{
+ } else if (this.ent.unitAIState().split(".")[2] === "RETURNRESOURCE" && !dead) {
+ // We remove us from the counting is we have no following order or its not "return to collected resource".
+ if (this.ent.unitAIOrderData().length === 1) {
+ var ent = gameState.getEntityById(this.gatheringFrom);
+ if (ent && ent.resourceSupplyType()){
+ ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
+ this.markFull(gameState,ent);
+ }
+ this.gatheringFrom = undefined;
+ }
+ if (!this.ent.unitAIOrderData()[1].target || this.gatheringFrom !== this.ent.unitAIOrderData()[1].target){
+ if (this.gatheringFrom){
+ var ent = gameState.getEntityById(this.gatheringFrom);
+ if (ent && ent.resourceSupplyType()){
+ ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
+ this.markFull(gameState,ent);
+ }
+ }
+ this.gatheringFrom = undefined;
+ }
+ } else {
if (this.gatheringFrom){
var ent = gameState.getEntityById(this.gatheringFrom);
- if (ent){
+ if (ent && ent.resourceSupplyType()){
ent.setMetadata("gatherer-count", ent.getMetadata("gatherer-count") - 1);
- this.markFull(ent);
+ this.markFull(gameState,ent);
}
this.gatheringFrom = undefined;
}
}
};
-Worker.prototype.markFull = function(ent){
- var maxCounts = {"food": 15, "wood": 5, "metal": 15, "stone": 15, "treasure": 1};
- if (ent.resourceSupplyType() && ent.getMetadata("gatherer-count") >= maxCounts[ent.resourceSupplyType().generic]){
+Worker.prototype.markFull = function(gameState,ent){
+ var maxCounts = {"food": 15, "wood": 6, "metal": 15, "stone": 15, "treasure": 1};
+ var resource = ent.resourceSupplyType().generic;
+ if (ent.resourceSupplyType() && ent.getMetadata("gatherer-count") >= maxCounts[resource]){
if (!ent.getMetadata("full")){
ent.setMetadata("full", true);
+ // update the dropsite
+ var dropsite = gameState.getEntityById(ent.getMetadata("linked-dropsite"));
+ if (dropsite == undefined || dropsite.getMetadata("linked-resources-" + resource) === undefined)
+ return;
+ if (ent.getMetadata("linked-dropsite-nearby") == true) {
+ dropsite.setMetadata("resource-quantity-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) - (+ent.getMetadata("dp-update-value")));
+ dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent);
+ dropsite.getMetadata("nearby-resources-" + resource).updateEnt(ent);
+ } else {
+ dropsite.setMetadata("resource-quantity-far-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) - (+ent.getMetadata("dp-update-value")));
+ dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent);
+ }
}
}else{
if (ent.getMetadata("full")){
ent.setMetadata("full", false);
+ // update the dropsite
+ var dropsite = gameState.getEntityById(ent.getMetadata("linked-dropsite"));
+ if (dropsite == undefined || dropsite.getMetadata("linked-resources-" + resource) === undefined)
+ return;
+ if (ent.getMetadata("linked-dropsite-nearby") == true) {
+ dropsite.setMetadata("resource-quantity-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) + ent.resourceSupplyAmount());
+ dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent);
+ dropsite.getMetadata("nearby-resources-" + resource).updateEnt(ent);
+ } else {
+ dropsite.setMetadata("resource-quantity-far-" + resource, +dropsite.getMetadata("resource-quantity-" + resource) + ent.resourceSupplyAmount());
+ dropsite.getMetadata("linked-resources-" + resource).updateEnt(ent);
+ }
}
}
};
Worker.prototype.startGathering = function(gameState){
var resource = this.ent.getMetadata("gather-type");
var ent = this.ent;
if (!ent.position()){
// TODO: work out what to do when entity has no position
return;
}
+ // TODO: this is not necessarily optimal.
+
// find closest dropsite which has nearby resources of the correct type
var minDropsiteDist = Math.min(); // set to infinity initially
var nearestResources = undefined;
var nearestDropsite = undefined;
- gameState.updatingCollection("active-dropsite-" + resource, Filters.byMetadata("active-dropsite-" + resource, true),
- gameState.getOwnDropsites(resource)).forEach(function (dropsite){
- if (dropsite.position()){
+ // first, look for nearby resources.
+ var number = 0;
+ gameState.getOwnDropsites(resource).forEach(function (dropsite){ if (dropsite.getMetadata("linked-resources-" +resource) !== undefined
+ && dropsite.getMetadata("linked-resources-" +resource).length > 3) { number++; } });
+
+ gameState.getOwnDropsites(resource).forEach(function (dropsite){ //}){
+ if (dropsite.getMetadata("resource-quantity-" +resource) == undefined)
+ return;
+ if (dropsite.position() && (dropsite.getMetadata("resource-quantity-" +resource) > 10 || number <= 1) ) {
var dist = SquareVectorDistance(ent.position(), dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
- nearestResources = dropsite.getMetadata("nearby-resources-" + resource);
+ nearestResources = dropsite.getMetadata("linked-resources-" + resource);
nearestDropsite = dropsite;
}
}
});
-
+ // none, check even low level of resources and far away
+ if (!nearestResources || nearestResources.length === 0){
+ gameState.getOwnDropsites(resource).forEach(function (dropsite){ //}){
+ if (dropsite.position() &&
+ (dropsite.getMetadata("resource-quantity-" +resource)+dropsite.getMetadata("resource-quantity-far-" +resource) > 10 || number <= 1)) {
+ var dist = SquareVectorDistance(ent.position(), dropsite.position());
+ if (dist < minDropsiteDist){
+ minDropsiteDist = dist;
+ nearestResources = dropsite.getMetadata("linked-resources-" + resource);
+ nearestDropsite = dropsite;
+ }
+ }
+ });
+ }
+ // else, just get the closest to our closest dropsite.
if (!nearestResources || nearestResources.length === 0){
nearestResources = gameState.getResourceSupplies(resource);
gameState.getOwnDropsites(resource).forEach(function (dropsite){
if (dropsite.position()){
var dist = SquareVectorDistance(ent.position(), dropsite.position());
if (dist < minDropsiteDist){
minDropsiteDist = dist;
nearestDropsite = dropsite;
}
}
});
}
if (nearestResources.length === 0){
if (resource === "food" && !this.buildAnyField(gameState)) // try to go build a farm
debug("No " + resource + " found! (1)");
else
debug("No " + resource + " found! (1)");
return;
}
+ if (!nearestDropsite) {
+ debug ("No dropsite for " +resource);
+ return;
+ }
+
var supplies = [];
var nearestSupplyDist = Math.min();
var nearestSupply = undefined;
- nearestResources.forEach(function(supply) {
+ nearestResources.forEach(function(supply) { //}){
// TODO: handle enemy territories
if (!supply.position()){
return;
}
- if (supply.getMetadata("full") == true) {
- return;
- }
- // measure the distance to the resource
- var dist = VectorDistance(supply.position(), ent.position());
+ // measure the distance to the resource (largely irrelevant)
+ var dist = SquareVectorDistance(supply.position(), ent.position());
+
// Add on a factor for the nearest dropsite if one exists
- if (nearestDropsite){
- dist += 5 * VectorDistance(supply.position(), nearestDropsite.position());
+ if (supply.getMetadata("linked-dropsite") !== undefined){
+ dist += 4*SquareVectorDistance(supply.position(), nearestDropsite.position());
+ dist /= 5.0;
}
// Go for treasure as a priority
- if (dist < 1000 && supply.resourceSupplyType().generic == "treasure"){
+ if (dist < 40000 && supply.resourceSupplyType().generic == "treasure"){
dist /= 1000;
}
if (dist < nearestSupplyDist){
nearestSupplyDist = dist;
nearestSupply = supply;
}
});
if (nearestSupply) {
var pos = nearestSupply.position();
// if the resource is far away, try to build a farm instead.
var tried = false;
- if (resource === "food" && SquareVectorDistance(pos,this.ent.position()) > 50000)
+ if (resource === "food" && SquareVectorDistance(pos,this.ent.position()) > 22500)
tried = this.buildAnyField(gameState);
+ if (!tried && SquareVectorDistance(pos,this.ent.position()) > 62500) {
+ return; // wait. a farm should appear.
+ }
if (!tried) {
var territoryOwner = gameState.getTerritoryMap().getOwner(pos);
if (!gameState.ai.accessibility.isAccessible(pos) ||
(territoryOwner != gameState.getPlayerID() && territoryOwner != 0)){
nearestSupply.setMetadata("inaccessible", true);
}else{
ent.gather(nearestSupply);
}
}
}else{
debug("No " + resource + " found! (2)");
}
};
// Makes the worker deposit the currently carried resources at the closest dropsite
Worker.prototype.returnResources = function(gameState){
if (!this.ent.resourceCarrying() || this.ent.resourceCarrying().length === 0){
return;
}
var resource = this.ent.resourceCarrying()[0].type;
var self = this;
if (!this.ent.position()){
// TODO: work out what to do when entity has no position
return;
}
var closestDropsite = undefined;
var dist = Math.min();
gameState.getOwnDropsites(resource).forEach(function(dropsite){
if (dropsite.position()){
var d = SquareVectorDistance(self.ent.position(), dropsite.position());
if (d < dist){
dist = d;
closestDropsite = dropsite;
}
}
});
if (!closestDropsite){
debug("No dropsite found for " + resource);
return;
}
this.ent.returnResources(closestDropsite);
};
Worker.prototype.getResourceType = function(type){
if (!type || !type.generic){
return undefined;
}
if (type.generic === "treasure"){
return type.specific;
}else{
return type.generic;
}
};
Worker.prototype.buildAnyField = function(gameState){
var self = this;
var okay = false;
var foundations = gameState.getOwnFoundations();
foundations.filterNearest(this.ent.position(), foundations.length);
foundations.forEach(function (found) {
if (found._template.BuildRestrictions.Category === "Field" && !okay) {
self.ent.repair(found);
okay = true;
return;
}
});
return okay;
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/economy.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/economy.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/economy.js (revision 12343)
@@ -1,632 +1,813 @@
var EconomyManager = function() {
this.targetNumBuilders = 3; // number of workers we want building stuff
this.targetNumFields = 3;
this.resourceMaps = {}; // Contains maps showing the density of wood, stone and metal
this.setCount = 0; //stops villagers being reassigned to other resources too frequently, count a set number of
//turns before trying to reassign them.
+ // this means we'll have about a big third of women, and thus we can maximize resource gathering rates.
this.femaleRatio = 0.4;
this.farmingFields = false;
- this.dropsiteNumbers = {wood: 2, stone: 1, metal: 1};
+ this.dropsiteNumbers = {"wood": 1, "stone": 0.5, "metal": 0.5};
};
// More initialisation for stuff that needs the gameState
EconomyManager.prototype.init = function(gameState){
- this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()/2.5), 1);
+ this.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*0.55), 1);
+
+ // initialize once all the resource maps.
+ this.updateResourceMaps(gameState, ["food","wood","stone","metal"]);
+ this.updateResourceConcentrations(gameState,"food");
+ this.updateResourceConcentrations(gameState,"wood");
+ this.updateResourceConcentrations(gameState,"stone");
+ this.updateResourceConcentrations(gameState,"metal");
+ this.updateNearbyResources(gameState, "food");
+ this.updateNearbyResources(gameState, "wood");
+ this.updateNearbyResources(gameState, "stone");
+ this.updateNearbyResources(gameState, "metal");
};
// okay, so here we'll create both females and male workers.
// We'll try to keep close to the "ratio" defined atop.
// qBot picks the best citizen soldier available: the cheapest and the fastest walker
// some civs such as Macedonia have 2 kinds of citizen soldiers: phalanx that are slow
// (speed:6) and peltasts that are very fast (speed: 11). Here, qBot will choose the peltast
// resulting in faster resource gathering.
// I'll also avoid creating citizen soldiers in the beginning because it's slower.
EconomyManager.prototype.trainMoreWorkers = function(gameState, queues) {
// Count the workers in the world and in progress
var numFemales = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("units/{civ}_support_female_citizen"));
numFemales += queues.villager.countTotalQueuedUnits();
- var numWorkers = gameState.countOwnEntitiesAndQueuedWithRole("worker");
- numWorkers += queues.citizenSoldier.countTotalQueuedUnits();
- var numTotal = numWorkers + queues.villager.countTotalQueuedUnits() + queues.citizenSoldier.countTotalQueuedUnits();
+ // counting the workers that aren't part of a plan
+ var numWorkers = 0;
+ gameState.getOwnEntities().forEach (function (ent) {
+ if (ent.getMetadata("role") == "worker" && ent.getMetadata("plan") == undefined)
+ numWorkers++;
+ });
+ gameState.getOwnTrainingFacilities().forEach(function(ent) {
+ ent.trainingQueue().forEach(function(item) {
+ if (item.metadata && item.metadata.role == "worker" && item.metadata.plan == undefined)
+ numWorkers += item.count;
+ });
+ });
+ var numQueued = queues.villager.countTotalQueuedUnits() + queues.citizenSoldier.countTotalQueuedUnits();
+ var numTotal = numWorkers + numQueued;
+
+ this.targetNumFields = numFemales/15;
+ if ((gameState.ai.playedTurn+2) % 3 === 0) {
+ this.dropsiteNumbers = {"wood": Math.ceil((numWorkers)/25)/2, "stone": Math.ceil((numWorkers)/40)/2, "metal": Math.ceil((numWorkers)/30)/2};
+ }
+
+ //debug (numTotal + "/" +this.targetNumWorkers + ", " +numFemales +"/" +numTotal);
+
// If we have too few, train more
- if (numTotal < this.targetNumWorkers && (queues.villager.countTotalQueuedUnits() < 10 || queues.citizenSoldier.countTotalQueuedUnits() < 10) ) {
- var template = "units/{civ}_support_female_citizen";
- var size = 1;
- if (numFemales/numWorkers > this.femaleRatio && gameState.getTimeElapsed() > 60*1000) {
+ // should plan enough to always have females…
+ if (numTotal < this.targetNumWorkers && numQueued < 20) {
+ var template = gameState.applyCiv("units/{civ}_support_female_citizen");
+ var size = Math.min(Math.ceil(gameState.getTimeElapsed() / 240000),5);
+ if (numFemales/numTotal > this.femaleRatio && gameState.getTimeElapsed() > 60*1000) {
+ var size = Math.min(Math.ceil(gameState.getTimeElapsed() / 120000),5);
template = this.findBestTrainableUnit(gameState, ["CitizenSoldier", "Infantry"], [ ["cost",1], ["speed",0.5]]);
- size = 5;
if (!template) {
- template = "units/{civ}_support_female_citizen";
- size = 1;
+ template = gameState.applyCiv("units/{civ}_support_female_citizen");
}
}
- if (size == 5)
- queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, template, { "role" : "worker" },size ));
- else
+ if (template === gameState.applyCiv("units/{civ}_support_female_citizen"))
queues.villager.addItem(new UnitTrainingPlan(gameState, template, { "role" : "worker" },size ));
+ else
+ queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, template, { "role" : "worker" },size ));
}
};
// picks the best template based on parameters and classes
EconomyManager.prototype.findBestTrainableUnit = function(gameState, classes, parameters) {
var units = gameState.findTrainableUnits(classes);
if (units.length === 0)
return undefined;
units.sort(function(a, b) { //}) {
var aDivParam = 0, bDivParam = 0;
var aTopParam = 0, bTopParam = 0;
for (i in parameters) {
var param = parameters[i];
if (param[0] == "base") {
aTopParam = param[1];
bTopParam = param[1];
}
if (param[0] == "strength") {
aTopParam += a[1].getMaxStrength() * param[1];
bTopParam += b[1].getMaxStrength() * param[1];
}
if (param[0] == "speed") {
aTopParam += a[1].walkSpeed() * param[1];
bTopParam += b[1].walkSpeed() * param[1];
}
if (param[0] == "cost") {
aDivParam += a[1].costSum() * param[1];
bDivParam += b[1].costSum() * param[1];
}
}
return -(aTopParam/(aDivParam+1)) + (bTopParam/(bDivParam+1));
});
return units[0][0];
};
// Pick the resource which most needs another worker
EconomyManager.prototype.pickMostNeededResources = function(gameState) {
var self = this;
// Find what resource type we're most in need of
if (!gameState.turnCache["gather-weights-calculated"]){
this.gatherWeights = gameState.ai.queueManager.futureNeeds(gameState);
gameState.turnCache["gather-weights-calculated"] = true;
}
var numGatherers = {};
- for ( var type in this.gatherWeights){
+ for (type in this.gatherWeights){
numGatherers[type] = gameState.updatingCollection("workers-gathering-" + type,
Filters.byMetadata("gather-type", type), gameState.getOwnEntitiesByRole("worker")).length;
}
var types = Object.keys(this.gatherWeights);
types.sort(function(a, b) {
// Prefer fewer gatherers (divided by weight)
var va = numGatherers[a] / (self.gatherWeights[a]+1);
var vb = numGatherers[b] / (self.gatherWeights[b]+1);
return va-vb;
});
return types;
};
EconomyManager.prototype.reassignRolelessUnits = function(gameState) {
//TODO: Move this out of the economic section
var roleless = gameState.getOwnEntitiesByRole(undefined);
roleless.forEach(function(ent) {
if (ent.hasClass("Worker")){
ent.setMetadata("role", "worker");
}else if(ent.hasClass("CitizenSoldier") || ent.hasClass("Champion")){
ent.setMetadata("role", "soldier");
}else{
ent.setMetadata("role", "unknown");
}
});
};
// If the numbers of workers on the resources is unbalanced then set some of workers to idle so
// they can be reassigned by reassignIdleWorkers.
EconomyManager.prototype.setWorkersIdleByPriority = function(gameState){
this.gatherWeights = gameState.ai.queueManager.futureNeeds(gameState);
var numGatherers = {};
var totalGatherers = 0;
var totalWeight = 0;
for ( var type in this.gatherWeights){
numGatherers[type] = 0;
totalWeight += this.gatherWeights[type];
}
gameState.getOwnEntitiesByRole("worker").forEach(function(ent) {
if (ent.getMetadata("subrole") === "gatherer"){
numGatherers[ent.getMetadata("gather-type")] += 1;
totalGatherers += 1;
}
});
for ( var type in this.gatherWeights){
var allocation = Math.floor(totalGatherers * (this.gatherWeights[type]/totalWeight));
if (allocation < numGatherers[type]){
var numToTake = numGatherers[type] - allocation;
gameState.getOwnEntitiesByRole("worker").forEach(function(ent) {
if (ent.getMetadata("subrole") === "gatherer" && ent.getMetadata("gather-type") === type && numToTake > 0){
ent.setMetadata("subrole", "idle");
numToTake -= 1;
}
});
}
}
};
EconomyManager.prototype.reassignIdleWorkers = function(gameState) {
var self = this;
// Search for idle workers, and tell them to gather resources based on demand
var filter = Filters.or(Filters.isIdle(), Filters.byMetadata("subrole", "idle"));
var idleWorkers = gameState.updatingCollection("idle-workers", filter, gameState.getOwnEntitiesByRole("worker"));
if (idleWorkers.length) {
var resourceSupplies;
idleWorkers.forEach(function(ent) {
// Check that the worker isn't garrisoned
if (ent.position() === undefined){
return;
}
var types = self.pickMostNeededResources(gameState);
ent.setMetadata("subrole", "gatherer");
ent.setMetadata("gather-type", types[0]);
});
}
};
EconomyManager.prototype.workersBySubrole = function(gameState, subrole) {
var workers = gameState.getOwnEntitiesByRole("worker");
return gameState.updatingCollection("subrole-" + subrole, Filters.byMetadata("subrole", subrole), workers);
};
EconomyManager.prototype.assignToFoundations = function(gameState) {
// If we have some foundations, and we don't have enough
// builder-workers,
// try reassigning some other workers who are nearby
+
+ // up to 2.5 buildings at once (that is 3, but one won't be complete).
- var foundations = gameState.getOwnFoundations();
+ var foundations = gameState.getOwnFoundations().toEntityArray();
+ var damagedBuildings = gameState.getOwnEntities().filter(function (ent) { if (ent.needsRepair() && ent.getMetadata("plan") == undefined) { return true; } return false; }).toEntityArray();
// Check if nothing to build
- if (!foundations.length){
+ if (!foundations.length && !damagedBuildings.length){
return;
}
var workers = gameState.getOwnEntitiesByRole("worker");
-
var builderWorkers = this.workersBySubrole(gameState, "builder");
- // Check if enough builders
- var extraNeeded = this.targetNumBuilders*foundations.length - builderWorkers.length;
- if (extraNeeded <= 0){
- return;
+ var addedWorkers = 0;
+
+ for (i in foundations) {
+ var target = foundations[i];
+ if (target._template.BuildRestrictions.Category === "Field")
+ continue; // we do not build fields
+ var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target).length;
+ if (assigned < this.targetNumBuilders) {
+ if (builderWorkers.length + addedWorkers < this.targetNumBuilders*Math.min(2.5,gameState.getTimeElapsed()/60000)) {
+ var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata("subrole") !== "builder" && ent.getMetadata("gather-type") !== "food" && ent.position() !== undefined); });
+ var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), this.targetNumBuilders - assigned);
+
+ nearestNonBuilders.forEach(function(ent) {
+ addedWorkers++;
+ ent.setMetadata("subrole", "builder");
+ ent.setMetadata("target-foundation", target);
+ });
+ if (this.targetNumBuilders - assigned - nearestNonBuilders.length > 0) {
+ var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata("subrole") !== "builder" && ent.position() !== undefined); });
+ var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), this.targetNumBuilders - assigned);
+ nearestNonBuilders.forEach(function(ent) {
+ addedWorkers++;
+ ent.setMetadata("subrole", "builder");
+ ent.setMetadata("target-foundation", target);
+ });
+ }
+ }
+ }
+ }
+ // don't repair if we're still under attack, unless it's like a vital (civcentre or wall) building that's getting destroyed.
+ for (i in damagedBuildings) {
+ var target = damagedBuildings[i];
+ if (gameState.defcon() < 5) {
+ if (target.healthLevel() > 0.5 || !target.hasClass("CivCentre") || !target.hasClass("StoneWall")) {
+ continue;
+ }
+ }
+ var assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target).length;
+ if (assigned < this.targetNumBuilders) {
+ if (builderWorkers.length + addedWorkers < this.targetNumBuilders*2.5) {
+
+ var nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata("subrole") !== "builder" && ent.position() !== undefined); });
+ if (gameState.defcon() < 5)
+ nonBuilderWorkers = workers.filter(function(ent) { return (ent.getMetadata("subrole") !== "builder" && ent.hasClass("Female") && ent.position() !== undefined); });
+ var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), this.targetNumBuilders - assigned);
+
+ nearestNonBuilders.forEach(function(ent) {
+ addedWorkers++;
+ ent.setMetadata("subrole", "builder");
+ ent.setMetadata("target-foundation", target);
+ });
+ }
+ }
}
-
- // Pick non-builders who are closest to the first foundation,
- // and tell them to start building it
-
- var target = foundations.toEntityArray()[0];
-
- var nonBuilderWorkers = workers.filter(function(ent) {
- // check position so garrisoned units aren't tasked
- return (ent.getMetadata("subrole") !== "builder" && ent.position() !== undefined);
- });
-
- var nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), extraNeeded);
-
- // Order each builder individually, not as a formation
- nearestNonBuilders.forEach(function(ent) {
- ent.setMetadata("subrole", "builder");
- ent.setMetadata("target-foundation", target);
- });
};
EconomyManager.prototype.buildMoreFields = function(gameState, queues) {
- if (this.farmingFields) {
+ if (this.farmingFields === true) {
var numFarms = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_field"));
numFarms += queues.field.countTotalQueuedUnits();
if (numFarms < this.targetNumFields + Math.floor(gameState.getTimeElapsed() / 900000))
queues.field.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_field"));
} else {
var foodAmount = 0;
- gameState.updatingCollection("active-dropsite-food", Filters.byMetadata("active-dropsite-food", true),
- gameState.getOwnDropsites("food")).forEach(function (dropsite){
- dropsite.getMetadata("nearby-resources-food").forEach(function (supply) {
- foodAmount += supply.resourceSupplyAmount();
- });
- });
+ gameState.getOwnDropsites("food").forEach( function (ent) { //}){
+ if (ent.getMetadata("resource-quantity-food") != undefined) {
+ foodAmount += ent.getMetadata("resource-quantity-food");
+ } else {
+ foodAmount = 300; // wait till we initialize
+ }
+ });
if (foodAmount < 300)
this.farmingFields = true;
}
};
// If all the CC's are destroyed then build a new one
EconomyManager.prototype.buildNewCC= function(gameState, queues) {
var numCCs = gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_civil_centre"));
numCCs += queues.civilCentre.totalLength();
for ( var i = numCCs; i < 1; i++) {
queues.civilCentre.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre"));
}
};
-//creates and maintains a map of tree density
-EconomyManager.prototype.updateResourceMaps = function(gameState, events){
- // The weight of the influence function is amountOfResource/decreaseFactor
- var decreaseFactor = {'wood': 15, 'stone': 100, 'metal': 100, 'food': 20};
+// creates and maintains a map of unused resource density
+// this also takes dropsites into account.
+// resources that are "part" of a dropsite are not counted.
+EconomyManager.prototype.updateResourceMaps = function(gameState, events) {
+
+ // TODO: centralize with that other function that uses the same variables
+ // The weight of the influence function is amountOfResource/decreaseFactor
+ var decreaseFactor = {'wood': 12.0, 'stone': 10.0, 'metal': 10.0, 'food': 20.0};
// This is the maximum radius of the influence
- var radius = {'wood':9, 'stone': 10, 'metal': 10, 'food': 12};
-
+ var radius = {'wood':25.0, 'stone': 24.0, 'metal': 24.0, 'food': 24.0};
+ // smallRadius is the distance necessary to mark a resource as linked to a dropsite.
+ var smallRadius = { 'food':70*70,'wood':120*120,'stone':60*60,'metal':60*60 };
+ // bigRadius is the distance for a weak link (resources are considered when building other dropsites)
+ // and their resource amount is divided by 3 when checking for dropsite resource level.
+ var bigRadius = { 'food':100*100,'wood':180*180,'stone':120*120,'metal':120*120 };
+
var self = this;
for (var resource in radius){
// if there is no resourceMap create one with an influence for everything with that resource
if (! this.resourceMaps[resource]){
this.resourceMaps[resource] = new Map(gameState);
var supplies = gameState.getResourceSupplies(resource);
supplies.forEach(function(ent){
if (!ent.position()){
return;
}
var x = Math.round(ent.position()[0] / gameState.cellSize);
var z = Math.round(ent.position()[1] / gameState.cellSize);
var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]);
self.resourceMaps[resource].addInfluence(x, z, radius[resource], strength);
});
}
- // TODO: fix for treasure and move out of loop
- // Look for destroy events and subtract the entities original influence from the resourceMap
- for (var i in events) {
- var e = events[i];
-
- if (e.type === "Destroy") {
- if (e.msg.entityObj){
- var ent = e.msg.entityObj;
- if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic === resource){
+ }
+ // Look for destroy events and subtract the entities original influence from the resourceMap
+ // also look for dropsite destruction and add the associated entities (along with unmarking them)
+ for (var i in events) {
+ var e = events[i];
+ if (e.type === "Destroy") {
+
+ if (e.msg.entityObj){
+ var ent = e.msg.entityObj;
+ if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure") {
+ if (e.msg.metadata[gameState.getPlayerID()] && !e.msg.metadata[gameState.getPlayerID()]["linked-dropsite"]) {
+ var resource = ent.resourceSupplyType().generic;
var x = Math.round(ent.position()[0] / gameState.cellSize);
var z = Math.round(ent.position()[1] / gameState.cellSize);
var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]);
this.resourceMaps[resource].addInfluence(x, z, radius[resource], -strength);
}
}
- }else if (e.type === "Create") {
- if (e.msg.entityObj){
- var ent = e.msg.entityObj;
- if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic === resource){
+ if (ent && ent.owner() == gameState.player && ent.resourceDropsiteTypes() !== undefined) {
+ var resources = ent.resourceDropsiteTypes();
+ for (i in resources) {
+ var resource = resources[i];
+ // loop through all dropsites to see if the resources of his entity collection could
+ // be taken over by another dropsite
+ var dropsites = gameState.getOwnDropsites(resource);
+ var metadata = e.msg.metadata[gameState.getPlayerID()];
+ metadata["linked-resources-" + resource].filter( function (supply) { //}){
+ var takenOver = false;
+ dropsites.forEach( function (otherDropsite) { //}) {
+ var distance = SquareVectorDistance(supply.position(), otherDropsite.position());
+ if (supply.getMetadata("linked-dropsite") == undefined || supply.getMetadata("linked-dropsite-dist") > distance) {
+ if (distance < bigRadius[resource]) {
+ supply.setMetadata("linked-dropsite", otherDropsite.id() );
+ supply.setMetadata("linked-dropsite-dist", +distance);
+ if (distance < smallRadius[resource]) {
+ takenOver = true;
+ supply.setMetadata("linked-dropsite-nearby", true );
+ } else {
+ supply.setMetadata("linked-dropsite-nearby", false );
+ }
+ }
+ }
+ });
+ if (!takenOver) {
+ var x = Math.round(supply.position()[0] / gameState.cellSize);
+ var z = Math.round(supply.position()[1] / gameState.cellSize);
+ var strength = Math.round(supply.resourceSupplyMax()/decreaseFactor[resource]);
+ self.resourceMaps[resource].addInfluence(x, z, radius[resource], strength);
+ }
+ });
+ }
+ }
+ }
+ } else if (e.type === "Create") {
+ if (e.msg.entityObj){
+ var ent = e.msg.entityObj;
+ if (ent && ent.position() && ent.resourceSupplyType() && ent.resourceSupplyType().generic !== "treasure"){
+ var resource = ent.resourceSupplyType().generic;
+
+ var addToMap = true;
+ var dropsites = gameState.getOwnDropsites(resource);
+ dropsites.forEach( function (otherDropsite) { //}) {
+ var distance = SquareVectorDistance(ent.position(), otherDropsite.position());
+ if (ent.getMetadata("linked-dropsite") == undefined || ent.getMetadata("linked-dropsite-dist") > distance) {
+ if (distance < bigRadius[resource]) {
+ if (distance < smallRadius[resource]) {
+ if (ent.getMetadata("linked-dropsite") == undefined)
+ addToMap = false;
+ ent.setMetadata("linked-dropsite-nearby", true );
+ } else {
+ ent.setMetadata("linked-dropsite-nearby", false );
+ }
+ ent.setMetadata("linked-dropsite", otherDropsite.id() );
+ ent.setMetadata("linked-dropsite-dist", +distance);
+ }
+ }
+ });
+ if (addToMap) {
var x = Math.round(ent.position()[0] / gameState.cellSize);
var z = Math.round(ent.position()[1] / gameState.cellSize);
var strength = Math.round(ent.resourceSupplyMax()/decreaseFactor[resource]);
this.resourceMaps[resource].addInfluence(x, z, radius[resource], strength);
}
}
}
}
- }
-
+ }
//this.resourceMaps['wood'].dumpIm("tree_density.png");
};
// Returns the position of the best place to build a new dropsite for the specified resource
EconomyManager.prototype.getBestResourceBuildSpot = function(gameState, resource){
- // A map which gives a positive weight for all CCs and adds a negative weight near all dropsites
+
var friendlyTiles = new Map(gameState);
- gameState.getOwnEntities().forEach(function(ent) {
- // We want to build near a CC of ours
- if (ent.hasClass("CivCentre")){
- var infl = 200;
+ friendlyTiles.add(this.resourceMaps[resource]);
+
+ for (i in this.resourceMaps)
+ if (i !== "food")
+ friendlyTiles.multiply(this.resourceMaps[i],true,100,1.5);
+
+ //friendlyTiles.dumpIm(gameState.getTimeElapsed() + "_" + resource + "_density_fade_base.png", 65000);
+
+ var territory = Map.createTerritoryMap(gameState);
+ friendlyTiles.multiplyTerritory(gameState,territory);
+
+ var resources = ["wood","stone","metal"];
+ for (i in resources) {
+ gameState.getOwnDropsites(resources[i]).forEach(function(ent) { //)){
+ // We don't want multiple dropsites at one spot so set to zero if too close.
var pos = ent.position();
var x = Math.round(pos[0] / gameState.cellSize);
var z = Math.round(pos[1] / gameState.cellSize);
- friendlyTiles.addInfluence(x, z, infl, 0.1 * infl);
- friendlyTiles.addInfluence(x, z, infl/2, 0.1 * infl);
- }
- // We don't want multiple dropsites at one spot so add a negative for all dropsites
- if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){
- var infl = 20;
-
- var pos = ent.position();
- var x = Math.round(pos[0] / gameState.cellSize);
- var z = Math.round(pos[1] / gameState.cellSize);
-
- friendlyTiles.addInfluence(x, z, infl, -50, 'quadratic');
- }
- });
-
- // Multiply by tree density to get a combination of the two maps
- friendlyTiles.multiply(this.resourceMaps[resource]);
-
- //friendlyTiles.dumpIm(resource + "_density_fade.png", 10000);
-
+ friendlyTiles.setInfluence(x, z, 17, 0);
+ });
+ }
+ //friendlyTiles.dumpIm(gameState.getTimeElapsed() + "_" + resource + "_density_fade_final.png", 10000);
+ friendlyTiles.multiply(gameState.ai.distanceFromMeMap,true,gameState.ai.distanceFromMeMap.width/3,2);
+ //friendlyTiles.dumpIm(gameState.getTimeElapsed() + "_" + resource + "_density_fade_final2.png", 10000);
+
var obstructions = Map.createObstructionMap(gameState);
obstructions.expandInfluences();
- var bestIdx = friendlyTiles.findBestTile(4, obstructions)[0];
-
- // Convert from 1d map pixel coordinates to game engine coordinates
+ var bestIdx = friendlyTiles.findBestTile(2, obstructions)[0];
var x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize;
var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize;
- return [x,z];
-};
-EconomyManager.prototype.updateResourceConcentrations = function(gameState){
- var self = this;
- var resources = ["food", "wood", "stone", "metal"];
- for (var key in resources){
- var resource = resources[key];
- gameState.getOwnEntities().forEach(function(ent) {
- if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){
- var radius = 14;
-
- var pos = ent.position();
- var x = Math.round(pos[0] / gameState.cellSize);
- var z = Math.round(pos[1] / gameState.cellSize);
-
- var quantity = self.resourceMaps[resource].sumInfluence(x, z, radius);
-
- ent.setMetadata("resourceQuantity_" + resource, quantity);
- }
- });
+ if (territory.getOwner([x,z]) === 0) {
+ bestIdx = friendlyTiles.findBestTile(4, obstructions)[0];
+ x = ((bestIdx % friendlyTiles.width) + 0.5) * gameState.cellSize;
+ z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * gameState.cellSize;
+ return [true, [x,z]];
}
+ return [false, [x,z]];
+};
+EconomyManager.prototype.updateResourceConcentrations = function(gameState, resource){
+ var self = this;
+ gameState.getOwnDropsites(resource).forEach(function(dropsite) { //}){
+ var amount = 0;
+ var amountFar = 0;
+ if (dropsite.getMetadata("linked-resources-" + resource) == undefined)
+ return;
+ dropsite.getMetadata("linked-resources-" + resource).forEach(function(supply){ //}){
+ if (supply.getMetadata("full") == true)
+ return;
+ if (supply.getMetadata("linked-dropsite-nearby") == true)
+ amount += supply.resourceSupplyAmount();
+ else
+ amountFar += supply.resourceSupplyAmount();
+ supply.setMetadata("dp-update-value",supply.resourceSupplyAmount());
+ });
+ dropsite.setMetadata("resource-quantity-" + resource, amount);
+ dropsite.setMetadata("resource-quantity-far-" + resource, amountFar);
+ });
};
// Stores lists of nearby resources
-EconomyManager.prototype.updateNearbyResources = function(gameState){
+EconomyManager.prototype.updateNearbyResources = function(gameState,resource){
var self = this;
var resources = ["food", "wood", "stone", "metal"];
var resourceSupplies;
- var radius = 100;
- for (var key in resources){
- var resource = resources[key];
+
+ // TODO: centralize with that other function that uses the same variables
+ // The weight of the influence function is amountOfResource/decreaseFactor
+ var decreaseFactor = {'wood': 12.0, 'stone': 10.0, 'metal': 10.0, 'food': 20.0};
+ // This is the maximum radius of the influence
+ var radius = {'wood':25.0, 'stone': 24.0, 'metal': 24.0, 'food': 24.0};
+ // smallRadius is the distance necessary to mark a resource as linked to a dropsite.
+ var smallRadius = { 'food':80*80,'wood':60*60,'stone':70*70,'metal':70*70 };
+ // bigRadius is the distance for a weak link (resources are considered when building other dropsites)
+ // and their resource amount is divided by 3 when checking for dropsite resource level.
+ var bigRadius = { 'food':140*140,'wood':140*140,'stone':140*140,'metal':140*140 };
+
+ gameState.getOwnDropsites(resource).forEach(function(ent) { //}){
- gameState.getOwnDropsites(resource).forEach(function(ent) {
- if (ent.getMetadata("nearby-resources-" + resource) === undefined){
- var filterPos = Filters.byStaticDistance(ent.position(), radius);
+ if (ent.getMetadata("nearby-resources-" + resource) === undefined){
+ // let's defined the entity collections (by metadata)
+ gameState.getResourceSupplies(resource).filter( function (supply) { //}){
+ var distance = SquareVectorDistance(supply.position(), ent.position());
+ // if we're close than the current linked-dropsite, or if it's not linked
+ // TODO: change when actualy resource counting is implemented.
- var collection = gameState.getResourceSupplies(resource).filter(filterPos);
- collection.registerUpdates();
-
- ent.setMetadata("nearby-resources-" + resource, collection);
- ent.setMetadata("active-dropsite-" + resource, true);
- }
+ if (supply.getMetadata("linked-dropsite") == undefined || supply.getMetadata("linked-dropsite-dist") > distance) {
+ if (distance < bigRadius[resource]) {
+ if (distance < smallRadius[resource]) {
+ // it's new to the game, remove it from the resource maps
+ if (supply.getMetadata("linked-dropsite") == undefined || supply.getMetadata("linked-dropsite-nearby") == false) {
+ var x = Math.round(supply.position()[0] / gameState.cellSize);
+ var z = Math.round(supply.position()[1] / gameState.cellSize);
+ var strength = Math.round(supply.resourceSupplyMax()/decreaseFactor[resource]);
+ self.resourceMaps[resource].addInfluence(x, z, radius[resource], -strength);
+ }
+ supply.setMetadata("linked-dropsite-nearby", true );
+ } else {
+ supply.setMetadata("linked-dropsite-nearby", false );
+ }
+ supply.setMetadata("linked-dropsite", ent.id() );
+ supply.setMetadata("linked-dropsite-dist", +distance);
+ }
+ }
+ });
+ // This one is both for the nearby and the linked
+ var filter = Filters.byMetadata("linked-dropsite", ent.id());
+ var collection = gameState.getResourceSupplies(resource).filter(filter);
+ collection.registerUpdates();
+ ent.setMetadata("linked-resources-" + resource, collection);
- if (ent.getMetadata("nearby-resources-" + resource).length === 0){
- ent.setMetadata("active-dropsite-" + resource, false);
- }else{
- ent.setMetadata("active-dropsite-" + resource, true);
- }
- /*
- // Make resources glow wildly
- if (resource == "food"){
- ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){
- Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]});
- });
- }
- if (resource == "wood"){
- ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){
- Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]});
- });
- }
- if (resource == "metal"){
- ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){
- Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,10]});
- });
- }*/
- });
- }
+ filter = Filters.byMetadata("linked-dropsite-nearby",true);
+ var collection2 = collection.filter(filter);
+ collection2.registerUpdates();
+ ent.setMetadata("nearby-resources-" + resource, collection2);
+
+ }
+
+ /*
+ // Make resources glow wildly
+ if (resource == "food"){
+ ent.getMetadata("linked-resources-" + resource).forEach(function(ent){
+ Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [1,0,0]});
+ });
+ ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){
+ Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [10,0,0]});
+ });
+ }
+ if (resource == "wood"){
+ ent.getMetadata("linked-resources-" + resource).forEach(function(ent){
+ Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,1,0]});
+ });
+ ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){
+ Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,10,0]});
+ });
+ }
+ if (resource == "metal"){
+ ent.getMetadata("linked-resources-" + resource).forEach(function(ent){
+ Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,1]});
+ });
+ ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){
+ Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0,10]});
+ });
+ }
+ if (resource == "stone"){
+ ent.getMetadata("linked-resources-" + resource).forEach(function(ent){
+ Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,0.5,1]});
+ });
+ ent.getMetadata("nearby-resources-" + resource).forEach(function(ent){
+ Engine.PostCommand({"type": "set-shading-color", "entities": [ent.id()], "rgb": [0,5,10]});
+ });
+ }*/
+ });
};
//return the number of resource dropsites with an acceptable amount of the resource nearby
EconomyManager.prototype.checkResourceConcentrations = function(gameState, resource){
- //TODO: make these values adaptive
- var requiredInfluence = {wood: 1400, stone: 200, metal: 200};
+ //TODO: make these values adaptive
+ var requiredInfluence = {"wood": 2500, "stone": 600, "metal": 600};
var count = 0;
- gameState.getOwnEntities().forEach(function(ent) {
- if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resource) !== -1){
- var quantity = ent.getMetadata("resourceQuantity_" + resource);
-
- if (quantity >= requiredInfluence[resource]){
- count ++;
- }
+ gameState.getOwnDropsites(resource).forEach(function(ent) { //}){
+ if (ent.getMetadata("resource-quantity-" + resource) == undefined || typeof(ent.getMetadata("resource-quantity-" + resource)) !== "number") {
+ count++; // assume it's OK if we don't know.
+ return;
+ }
+ var quantity = +ent.getMetadata("resource-quantity-" + resource);
+ var quantityFar = +ent.getMetadata("resource-quantity-far-" + resource);
+
+ if (quantity >= requiredInfluence[resource]) {
+ count++;
+ } else if (quantity + quantityFar >= requiredInfluence[resource]) {
+ count += 0.5 + (quantity/requiredInfluence[resource])/2;
+ } else {
+ count += ((quantity + quantityFar)/requiredInfluence[resource])/2;
}
});
return count;
};
EconomyManager.prototype.buildMarket = function(gameState, queues){
- if (gameState.getTimeElapsed() > 360 * 1000){
+ if (gameState.getTimeElapsed() > 620 * 1000){
if (queues.economicBuilding.countTotalQueuedUnitsWithClass("BarterMarket") === 0 &&
gameState.countEntitiesAndQueuedByType(gameState.applyCiv("structures/{civ}_market")) === 0){
//only ever build one mill/CC/market at a time
queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_market"));
}
}
};
// if qBot has resources it doesn't need, it'll try to barter it for resources it needs
// once per turn because the info doesn't update between a turn and I don't want to fix it.
// pretty efficient.
EconomyManager.prototype.tryBartering = function(gameState){
var done = false;
if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_market")) >= 1) {
var needs = gameState.ai.queueManager.futureNeeds(gameState,true);
var ress = gameState.ai.queueManager.getAvailableResources(gameState);
for (sell in needs) {
for (buy in needs) {
if (!done && buy != sell && needs[sell] <= 0 && ress[sell] > 400) { // if we don't need it and have a buffer
if ( (ress[buy] < 400) || needs[buy] > 0) { // if we need that other resource/ have too little of it
var markets = gameState.getOwnEntitiesByType(gameState.applyCiv("structures/{civ}_market")).toEntityArray();
markets[0].barter(buy,sell,100);
//debug ("bartered " +sell +" for " + buy + ", value 100");
done = true;
}
}
}
}
}
};
// so this always try to build dropsites.
EconomyManager.prototype.buildDropsites = function(gameState, queues){
- if (queues.economicBuilding.totalLength() === 0 &&
- gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_mill")) === 0 &&
+ if (queues.economicBuilding.totalLength() === 0 && gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_mill")) === 0 &&
gameState.countFoundationsWithType(gameState.applyCiv("structures/{civ}_civil_centre")) === 0){
//only ever build one mill/CC/market at a time
if (gameState.getTimeElapsed() > 30 * 1000){
for (var resource in this.dropsiteNumbers){
if (this.checkResourceConcentrations(gameState, resource) < this.dropsiteNumbers[resource]){
- var spot = this.getBestResourceBuildSpot(gameState, resource);
- var myCivCentres = gameState.getOwnEntities().filter(function(ent) {
- if (!ent.hasClass("CivCentre") || ent.position() === undefined){
- return false;
- }
- var dx = (spot[0]-ent.position()[0]);
- var dy = (spot[1]-ent.position()[1]);
- var dist2 = dx*dx + dy*dy;
- return (ent.hasClass("CivCentre") && dist2 < 180*180);
- });
+ var spot = this.getBestResourceBuildSpot(gameState, resource);
- if (myCivCentres.length === 0){
- queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre", spot));
- }else{
- queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_mill", spot));
+ if (spot[0] === true){
+ queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_civil_centre", spot[1]));
+ } else {
+ queues.economicBuilding.addItem(new BuildingConstructionPlan(gameState, "structures/{civ}_mill", spot[1]));
}
break;
}
}
}
}
};
EconomyManager.prototype.update = function(gameState, queues, events) {
Engine.ProfileStart("economy update");
this.reassignRolelessUnits(gameState);
this.buildNewCC(gameState,queues);
+ // this function also deals with a few things that are number-of-workers related
Engine.ProfileStart("Train workers and build farms");
this.trainMoreWorkers(gameState, queues);
- if (gameState.getTimeElapsed() > 5000)
+ if ((gameState.ai.playedTurn+1) % 3 === 0)
this.buildMoreFields(gameState,queues);
+
Engine.ProfileStop();
//Later in the game we want to build stuff faster.
- if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > this.targetNumWorkers * 0.5) {
+ if (gameState.getTimeElapsed() > 15*60*1000) {
this.targetNumBuilders = 6;
}else{
this.targetNumBuilders = 3;
}
-
- if (gameState.getTimeElapsed() > 20*60*1000) {
- this.dropsiteNumbers = {wood: 3, stone: 2, metal: 2};
- }else{
- this.dropsiteNumbers = {wood: 2, stone: 1, metal: 1};
- }
-
+
Engine.ProfileStart("Update Resource Maps and Concentrations");
+ this.updateResourceMaps(gameState, events);
if (gameState.ai.playedTurn % 2 === 0) {
- this.updateResourceMaps(gameState, events);
- this.updateResourceConcentrations(gameState);
- this.updateNearbyResources(gameState);
+ var resources = ["food", "wood", "stone", "metal"];
+ this.updateNearbyResources(gameState, resources[(gameState.ai.playedTurn % 8)/2]);
+ } else if (gameState.ai.playedTurn % 2 === 1) {
+ var resources = ["food", "wood", "stone", "metal"];
+ this.updateResourceConcentrations(gameState, resources[((gameState.ai.playedTurn+1) % 8)/2]);
}
Engine.ProfileStop();
- Engine.ProfileStart("Build new Dropsites");
- this.buildDropsites(gameState, queues);
- Engine.ProfileStop();
-
+ if (gameState.ai.playedTurn % 8 === 0) {
+ Engine.ProfileStart("Build new Dropsites");
+ this.buildDropsites(gameState, queues);
+ Engine.ProfileStop();
+ }
this.tryBartering(gameState);
this.buildMarket(gameState, queues);
// TODO: implement a timer based system for this
this.setCount += 1;
if (this.setCount >= 20){
this.setWorkersIdleByPriority(gameState);
this.setCount = 0;
}
Engine.ProfileStart("Assign builders");
this.assignToFoundations(gameState);
Engine.ProfileStop();
Engine.ProfileStart("Reassign Idle Workers");
this.reassignIdleWorkers(gameState);
Engine.ProfileStop();
- Engine.ProfileStart("Swap Workers");
- var gathererGroups = {};
- gameState.getOwnEntitiesByRole("worker").forEach(function(ent){
- var key = uneval(ent.resourceGatherRates());
- if (!gathererGroups[key]){
- gathererGroups[key] = {"food": [], "wood": [], "metal": [], "stone": []};
- }
- if (ent.getMetadata("gather-type") in gathererGroups[key]){
- gathererGroups[key][ent.getMetadata("gather-type")].push(ent);
- }
- });
-
- for (var i in gathererGroups){
- for (var j in gathererGroups){
- var a = eval(i);
- var b = eval(j);
- if (a["food.grain"]/b["food.grain"] > a["wood.tree"]/b["wood.tree"] && gathererGroups[i]["wood"].length > 0 && gathererGroups[j]["food"].length > 0){
- for (var k = 0; k < Math.min(gathererGroups[i]["wood"].length, gathererGroups[j]["food"].length); k++){
- gathererGroups[i]["wood"][k].setMetadata("gather-type", "food");
- gathererGroups[j]["food"][k].setMetadata("gather-type", "wood");
+ // this is pretty slow, run it once in a while
+ if (gameState.ai.playedTurn % 4 === 0) {
+ Engine.ProfileStart("Swap Workers");
+ var gathererGroups = {};
+ gameState.getOwnEntitiesByRole("worker").forEach(function(ent){
+ var key = uneval(ent.resourceGatherRates());
+ if (!gathererGroups[key]){
+ gathererGroups[key] = {"food": [], "wood": [], "metal": [], "stone": []};
+ }
+ if (ent.getMetadata("gather-type") in gathererGroups[key]){
+ gathererGroups[key][ent.getMetadata("gather-type")].push(ent);
+ }
+ });
+ for (var i in gathererGroups){
+ for (var j in gathererGroups){
+ var a = eval(i);
+ var b = eval(j);
+ if (a["food.grain"]/b["food.grain"] > a["wood.tree"]/b["wood.tree"] && gathererGroups[i]["wood"].length > 0 && gathererGroups[j]["food"].length > 0){
+ for (var k = 0; k < Math.min(gathererGroups[i]["wood"].length, gathererGroups[j]["food"].length); k++){
+ gathererGroups[i]["wood"][k].setMetadata("gather-type", "food");
+ gathererGroups[j]["food"][k].setMetadata("gather-type", "wood");
+ }
}
}
}
+ Engine.ProfileStop();
}
- Engine.ProfileStop();
-
Engine.ProfileStart("Run Workers");
gameState.getOwnEntitiesByRole("worker").forEach(function(ent){
if (!ent.getMetadata("worker-object")){
ent.setMetadata("worker-object", new Worker(ent));
}
ent.getMetadata("worker-object").update(gameState);
});
// Gatherer count updates for non-workers
var filter = Filters.and(Filters.not(Filters.byMetadata("worker-object", undefined)),
Filters.not(Filters.byMetadata("role", "worker")));
gameState.updatingCollection("reassigned-workers", filter, gameState.getOwnEntities()).forEach(function(ent){
ent.getMetadata("worker-object").updateGathererCounts(gameState);
});
// Gatherer count updates for destroyed units
for (var i in events) {
var e = events[i];
if (e.type === "Destroy") {
if (e.msg.metadata && e.msg.metadata[gameState.getPlayerID()] && e.msg.metadata[gameState.getPlayerID()]["worker-object"]){
e.msg.metadata[gameState.getPlayerID()]["worker-object"].updateGathererCounts(gameState, true);
}
}
}
Engine.ProfileStop();
Engine.ProfileStop();
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/gamestate.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/gamestate.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/gamestate.js (revision 12343)
@@ -1,358 +1,372 @@
/**
* Provides an API for the rest of the AI scripts to query the world state at a
* higher level than the raw data.
*/
var GameState = function(ai) {
MemoizeInit(this);
this.ai = ai;
this.timeElapsed = ai.timeElapsed;
this.templates = ai.templates;
this.entities = ai.entities;
this.player = ai.player;
this.playerData = ai.playerData;
this.buildingsBuilt = 0;
if (!this.ai._gameStateStore){
this.ai._gameStateStore = {};
}
this.store = this.ai._gameStateStore;
this.cellSize = 4; // Size of each map tile
this.turnCache = {};
};
GameState.prototype.updatingCollection = function(id, filter, collection){
if (!this.store[id]){
this.store[id] = collection.filter(filter);
this.store[id].registerUpdates();
}
return this.store[id];
};
GameState.prototype.getTimeElapsed = function() {
return this.timeElapsed;
};
GameState.prototype.getTemplate = function(type) {
if (!this.templates[type]){
return null;
}
return new EntityTemplate(this.templates[type]);
};
GameState.prototype.applyCiv = function(str) {
return str.replace(/\{civ\}/g, this.playerData.civ);
};
/**
* @returns {Resources}
*/
GameState.prototype.getResources = function() {
return new Resources(this.playerData.resourceCounts);
};
GameState.prototype.getMap = function() {
return this.ai.passabilityMap;
};
GameState.prototype.getTerritoryMap = function() {
return Map.createTerritoryMap(this);
};
GameState.prototype.getPopulation = function() {
return this.playerData.popCount;
};
GameState.prototype.getPopulationLimit = function() {
return this.playerData.popLimit;
};
GameState.prototype.getPopulationMax = function() {
return this.playerData.popMax;
};
GameState.prototype.getPassabilityClassMask = function(name) {
if (!(name in this.ai.passabilityClasses)){
error("Tried to use invalid passability class name '" + name + "'");
}
return this.ai.passabilityClasses[name];
};
GameState.prototype.getPlayerID = function() {
return this.player;
};
GameState.prototype.isPlayerAlly = function(id) {
return this.playerData.isAlly[id];
};
GameState.prototype.isPlayerEnemy = function(id) {
return this.playerData.isEnemy[id];
};
GameState.prototype.getEnemies = function(){
var ret = [];
for (var i in this.playerData.isEnemy){
if (this.playerData.isEnemy[i]){
ret.push(i);
}
}
return ret;
};
GameState.prototype.isEntityAlly = function(ent) {
if (ent && ent.owner && (typeof ent.owner) === "function"){
return this.playerData.isAlly[ent.owner()];
} else if (ent && ent.owner){
return this.playerData.isAlly[ent.owner];
}
return false;
};
GameState.prototype.isEntityEnemy = function(ent) {
if (ent && ent.owner && (typeof ent.owner) === "function"){
return this.playerData.isEnemy[ent.owner()];
} else if (ent && ent.owner){
return this.playerData.isEnemy[ent.owner];
}
return false;
};
GameState.prototype.isEntityOwn = function(ent) {
if (ent && ent.owner && (typeof ent.owner) === "function"){
return ent.owner() == this.player;
} else if (ent && ent.owner){
return ent.owner == this.player;
}
return false;
};
GameState.prototype.getOwnEntities = function() {
if (!this.store.ownEntities){
this.store.ownEntities = this.getEntities().filter(Filters.byOwner(this.player));
this.store.ownEntities.registerUpdates();
}
return this.store.ownEntities;
};
GameState.prototype.getEnemyEntities = function() {
var diplomacyChange = false;
var enemies = this.getEnemies();
if (this.store.enemies){
if (this.store.enemies.length != enemies.length){
diplomacyChange = true;
}else{
for (var i = 0; i < enemies.length; i++){
if (enemies[i] !== this.store.enemies[i]){
diplomacyChange = true;
}
}
}
}
if (diplomacyChange || !this.store.enemyEntities){
var filter = Filters.byOwners(enemies);
this.store.enemyEntities = this.getEntities().filter(filter);
this.store.enemyEntities.registerUpdates();
this.store.enemies = enemies;
}
return this.store.enemyEntities;
};
GameState.prototype.getEntities = function() {
return this.entities;
};
GameState.prototype.getEntityById = function(id){
if (this.entities._entities[id]) {
return this.entities._entities[id];
}else{
//debug("Entity " + id + " requested does not exist");
}
return undefined;
};
GameState.prototype.getOwnEntitiesByMetadata = function(key, value){
if (!this.store[key + "-" + value]){
var filter = Filters.byMetadata(key, value);
this.store[key + "-" + value] = this.getOwnEntities().filter(filter);
this.store[key + "-" + value].registerUpdates();
}
return this.store[key + "-" + value];
};
GameState.prototype.getOwnEntitiesByRole = function(role){
return this.getOwnEntitiesByMetadata("role", role);
};
// TODO: fix this so it picks up not in use training stuff
GameState.prototype.getOwnTrainingFacilities = function(){
return this.updatingCollection("own-training-facilities", Filters.byTrainingQueue(), this.getOwnEntities());
};
GameState.prototype.getOwnEntitiesByType = function(type){
var filter = Filters.byType(type);
return this.updatingCollection("own-by-type-" + type, filter, this.getOwnEntities());
};
GameState.prototype.countEntitiesByType = function(type) {
return this.getOwnEntitiesByType(type).length;
};
GameState.prototype.countEntitiesAndQueuedByType = function(type) {
var count = this.countEntitiesByType(type);
// Count building foundations
count += this.countEntitiesByType("foundation|" + type);
// Count entities in building production queues
this.getOwnTrainingFacilities().forEach(function(ent){
ent.trainingQueue().forEach(function(item) {
if (item.template == type){
count += item.count;
}
});
});
return count;
};
GameState.prototype.countFoundationsWithType = function(type) {
var foundationType = "foundation|" + type;
var count = 0;
this.getOwnEntities().forEach(function(ent) {
var t = ent.templateName();
if (t == foundationType)
++count;
});
return count;
};
GameState.prototype.countOwnEntitiesByRole = function(role) {
return this.getOwnEntitiesByRole(role).length;
};
GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) {
var count = this.countOwnEntitiesByRole(role);
// Count entities in building production queues
this.getOwnTrainingFacilities().forEach(function(ent) {
ent.trainingQueue().forEach(function(item) {
if (item.metadata && item.metadata.role == role)
count += item.count;
});
});
return count;
};
GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) {
// Count entities in building production queues
var count = 0;
this.getOwnTrainingFacilities().forEach(function(ent) {
ent.trainingQueue().forEach(function(item) {
- if (item.metadata && item.metadata.data && item.metadata.data == value)
+ if (item.metadata && item.metadata[data] && item.metadata[data] == value)
count += item.count;
});
});
return count;
};
/**
* Find buildings that are capable of training the given unit type, and aren't
* already too busy.
*/
GameState.prototype.findTrainers = function(template) {
var maxQueueLength = 2; // avoid tying up resources in giant training queues
return this.getOwnTrainingFacilities().filter(function(ent) {
var trainable = ent.trainableEntities();
if (!trainable || trainable.indexOf(template) == -1)
return false;
var queue = ent.trainingQueue();
if (queue) {
if (queue.length >= maxQueueLength)
return false;
}
return true;
});
};
/**
* Find units that are capable of constructing the given building type.
*/
GameState.prototype.findBuilders = function(template) {
return this.getOwnEntities().filter(function(ent) {
var buildable = ent.buildableEntities();
if (!buildable || buildable.indexOf(template) == -1)
return false;
return true;
});
};
GameState.prototype.getOwnFoundations = function() {
return this.updatingCollection("ownFoundations", Filters.isFoundation(), this.getOwnEntities());
};
GameState.prototype.getOwnDropsites = function(resource){
return this.updatingCollection("dropsite-own-" + resource, Filters.isDropsite(resource), this.getOwnEntities());
};
GameState.prototype.getResourceSupplies = function(resource){
return this.updatingCollection("resource-" + resource, Filters.byResource(resource), this.getEntities());
};
GameState.prototype.getBuildLimits = function() {
return this.playerData.buildLimits;
};
GameState.prototype.getBuildCounts = function() {
return this.playerData.buildCounts;
};
// Checks whether the maximum number of buildings have been cnstructed for a certain catergory
GameState.prototype.isBuildLimitReached = function(category) {
if(this.playerData.buildLimits[category] === undefined || this.playerData.buildCounts[category] === undefined)
return false;
if(this.playerData.buildLimits[category].LimitsPerCivCentre != undefined)
return (this.playerData.buildCounts[category] >= this.playerData.buildCounts["CivilCentre"]*this.playerData.buildLimits[category].LimitPerCivCentre);
else
return (this.playerData.buildCounts[category] >= this.playerData.buildLimits[category]);
};
GameState.prototype.findTrainableUnits = function(classes){
var allTrainable = [];
this.getOwnEntities().forEach(function(ent) {
var trainable = ent.trainableEntities();
for (var i in trainable){
if (allTrainable.indexOf(trainable[i]) === -1){
allTrainable.push(trainable[i]);
}
}
});
var ret = [];
for (var i in allTrainable) {
var template = this.getTemplate(allTrainable[i]);
var okay = true;
+
for (o in classes)
if (!template.hasClass(classes[o]))
okay = false;
+
if (template.hasClass("Hero")) // disabling heroes for now
okay = false;
+
if (okay)
ret.push( [allTrainable[i], template] );
}
return ret;
};
-
-
+// defcon utilities
+GameState.prototype.timeSinceDefconChange = function() {
+ return this.getTimeElapsed()-this.ai.defconChangeTime;
+};
+GameState.prototype.setDefcon = function(level,force) {
+ if (this.ai.defcon >= level || force) {
+ this.ai.defcon = level;
+ this.ai.defconChangeTime = this.getTimeElapsed();
+ }
+};
+GameState.prototype.defcon = function() {
+ return this.ai.defcon;
+};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/entitycollection-extend.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/entitycollection-extend.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/entitycollection-extend.js (revision 12343)
@@ -1,69 +1,68 @@
EntityCollection.prototype.attack = function(unit)
{
var unitId;
if (typeof(unit) === "Entity"){
unitId = unit.id();
}else{
unitId = unit;
}
Engine.PostCommand({"type": "attack", "entities": this.toIdArray(), "target": unitId, "queued": false});
return this;
};
-
-EntityCollection.prototype.attackMove = function(x, z){
- Engine.PostCommand({"type": "attack-move", "entities": this.toIdArray(), "x": x, "z": z, "queued": false});
+// violent, aggressive, defensive, passive, standground
+EntityCollection.prototype.setStance = function(stance){
+ Engine.PostCommand({"type": "stance", "entities": this.toIdArray(), "name" : stance, "queued": false});
return this;
};
-
function EntityCollectionFromIds(gameState, idList){
var ents = {};
for (var i in idList){
var id = idList[i];
if (gameState.entities._entities[id]) {
ents[id] = gameState.entities._entities[id];
}
}
return new EntityCollection(gameState.ai, ents);
}
EntityCollection.prototype.getCentrePosition = function(){
var sumPos = [0, 0];
var count = 0;
this.forEach(function(ent){
if (ent.position()){
sumPos[0] += ent.position()[0];
sumPos[1] += ent.position()[1];
count ++;
}
});
if (count === 0){
return undefined;
}else{
return [sumPos[0]/count, sumPos[1]/count];
}
};
EntityCollection.prototype.filterNearest = function(targetPos, n)
{
// Compute the distance of each entity
var data = []; // [ [id, ent, distance], ... ]
for (var id in this._entities)
{
var ent = this._entities[id];
if (ent.position())
data.push([id, ent, SquareVectorDistance(targetPos, ent.position())]);
}
// Sort by increasing distance
data.sort(function (a, b) { return (a[2] - b[2]); });
if (n === undefined)
n = this._length;
// Extract the first n
var ret = {};
for each (var val in data.slice(0, n))
ret[val[0]] = val[1];
return new EntityCollection(this._ai, ret);
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/attack_plan.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/attack_plan.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/attack_plan.js (revision 12343)
@@ -1,680 +1,759 @@
// basically an attack plan. The name is an artifact.
-function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , targetFinder){
+function CityAttack(gameState, militaryManager, uniqueID, targetEnemy, type , targetFinder) {
//This is the list of IDs of the units in the plan
this.idList=[];
this.state = "unexecuted";
this.targetPlayer = targetEnemy;
if (this.targetPlayer === -1 || this.targetPlayer === undefined) {
// let's find our prefered target, basically counting our enemies units.
var enemyCount = {};
for (var i = 1; i <=8; i++)
enemyCount[i] = 0;
gameState.getEntities().forEach(function(ent) { if (gameState.isEntityEnemy(ent) && ent.owner() !== 0) { enemyCount[ent.owner()]++; } });
var max = 0;
for (i in enemyCount)
if (enemyCount[i] >= max)
{
this.targetPlayer = +i;
max = enemyCount[i];
}
}
debug ("Target = " +this.targetPlayer);
this.targetFinder = targetFinder || this.defaultTargetFinder;
this.type = type || "normal";
this.name = uniqueID;
this.healthRecord = [];
this.timeOfPlanStart = gameState.getTimeElapsed(); // we get the time at which we decided to start the attack
this.maxPreparationTime = 300*1000;
this.pausingStart = 0;
this.totalPausingTime = 0;
this.paused = false;
this.onArrivalReaction = "proceedOnTargets";
+ // priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize".
+ // if not, this is a "bonus". The higher the priority, the more this unit will get built.
+ // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm)
+ // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built
+ // only once every other category is at least 50% of its target size.
this.unitStat = {};
this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Ranged"], "templates" : [] };
this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Melee"], "templates" : [] };
this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Melee"], "templates" : [] };
this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Ranged"], "templates" : [] };
- this.unitStat["Siege"] = { "priority" : 1, "minSize" : 0, "targetSize" : 3 , "batchSize" : 1, "classes" : ["Siege"], "templates" : [] };
+ this.unitStat["Siege"] = { "priority" : 0.5, "minSize" : 0, "targetSize" : 3 , "batchSize" : 1, "classes" : ["Siege"], "templates" : [] };
+
+
+ if (type === "superSized") {
+ this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 18, "batchSize" : 5, "classes" : ["Infantry","Ranged"], "templates" : [] };
+ this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 6, "targetSize" : 24, "batchSize" : 5, "classes" : ["Infantry","Melee"], "templates" : [] };
+ this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 12 , "batchSize" : 5, "classes" : ["Cavalry","Melee"], "templates" : [] };
+ this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 12 , "batchSize" : 5, "classes" : ["Cavalry","Ranged"], "templates" : [] };
+ this.unitStat["Siege"] = { "priority" : 0.5, "minSize" : 3, "targetSize" : 6 , "batchSize" : 3, "classes" : ["Siege"], "templates" : [] };
+ this.maxPreparationTime = 450*1000;
+ }
+
+ /*
+ this.unitStat["Siege"]["filter"] = function (ent) {
+ var strength = [ent.attackStrengths("Melee")["crush"],ent.attackStrengths("Ranged")["crush"]];
+ return (strength[0] > 15 || strength[1] > 15);
+ };*/
var filter = Filters.and(Filters.byMetadata("plan",this.name),Filters.byOwner(gameState.player));
this.unitCollection = gameState.getOwnEntities().filter(filter);
this.unitCollection.registerUpdates();
this.unitCollection.length;
this.unit = {};
// each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ]
this.buildOrder = [];
// defining the entity collections. Will look for units I own, that are part of this plan.
// Also defining the buildOrders.
for (unitCat in this.unitStat) {
var cat = unitCat;
var Unit = this.unitStat[cat];
- var filter = Filters.and(Filters.byClassesAnd(Unit["classes"]),Filters.and(Filters.byMetadata("plan",this.name),Filters.byOwner(gameState.player)));
+ filter = Filters.and(Filters.byClassesAnd(Unit["classes"]),Filters.and(Filters.byMetadata("plan",this.name),Filters.byOwner(gameState.player)));
this.unit[cat] = gameState.getOwnEntities().filter(filter);
this.unit[cat].registerUpdates();
this.unit[cat].length;
this.buildOrder.push([0, Unit["classes"], this.unit[cat], Unit, cat]);
}
/*if (gameState.getTimeElapsed() > 900000) // 15 minutes
{
this.unitStat.Cavalry.Ranged["minSize"] = 5;
this.unitStat.Cavalry.Melee["minSize"] = 5;
this.unitStat.Infantry.Ranged["minSize"] = 10;
this.unitStat.Infantry.Melee["minSize"] = 10;
this.unitStat.Cavalry.Ranged["targetSize"] = 10;
this.unitStat.Cavalry.Melee["targetSize"] = 10;
this.unitStat.Infantry.Ranged["targetSize"] = 20;
this.unitStat.Infantry.Melee["targetSize"] = 20;
this.unitStat.Siege["targetSize"] = 5;
this.unitStat.Siege["minSize"] = 2;
} else {
this.maxPreparationTime = 180000;
}*/
// todo: REACTIVATE (in all caps)
if (type === "harass_raid" && 0 == 1)
{
this.targetFinder = this.raidingTargetFinder;
this.onArrivalReaction = "huntVillagers";
this.type = "harass_raid";
// This is a Cavalry raid against villagers. A Cavalry Swordsman has a bonus against these. Only build these
this.maxPreparationTime = 180000; // 3 minutes.
if (gameState.playerData.civ === "hele") // hellenes have an ealry Cavalry Swordsman
{
this.unitCount.Cavalry.Melee = { "subCat" : ["Swordsman"] , "usesSubcategories" : true, "Swordsman" : undefined, "priority" : 1, "currentAmount" : 0, "minimalAmount" : 0, "preferedAmount" : 0 };
this.unitCount.Cavalry.Melee.Swordsman = { "priority" : 1, "currentAmount" : 0, "minimalAmount" : 4, "preferedAmount" : 7, "fallback" : "abort" };
} else {
this.unitCount.Cavalry.Melee = { "subCat" : undefined , "usesSubcategories" : false, "priority" : 1, "currentAmount" : 0, "minimalAmount" : 4, "preferedAmount" : 7 };
}
this.unitCount.Cavalry.Ranged["minimalAmount"] = 0;
this.unitCount.Cavalry.Ranged["preferedAmount"] = 0;
this.unitCount.Infantry.Ranged["minimalAmount"] = 0;
this.unitCount.Infantry.Ranged["preferedAmount"] = 0;
this.unitCount.Infantry.Melee["minimalAmount"] = 0;
this.unitCount.Infantry.Melee["preferedAmount"] = 0;
this.unitCount.Siege["preferedAmount"] = 0;
}
this.anyNotMinimal = true; // used for support plans
// taking this so that fortresses won't crash it for now. TODO: change the rally point if it becomes invalid
if(gameState.ai.pathsToMe.length > 1)
var position = [(gameState.ai.pathsToMe[0][0]+gameState.ai.pathsToMe[1][0])/2.0,(gameState.ai.pathsToMe[0][1]+gameState.ai.pathsToMe[1][1])/2.0];
- else
+ else if (gameState.ai.pathsToMe.length !== 0)
var position = [gameState.ai.pathsToMe[0][0],gameState.ai.pathsToMe[0][1]];
+ else
+ var position = [-1,-1];
var CCs = gameState.getOwnEntities().filter(Filters.byClass("CivCentre"));
var nearestCCArray = CCs.filterNearest(position, 1).toEntityArray();
var CCpos = nearestCCArray[0].position();
this.rallyPoint = [0,0];
- this.rallyPoint[0] = (position[0]*3 + CCpos[0]) / 4.0;
- this.rallyPoint[1] = (position[1]*3 + CCpos[1]) / 4.0;
+ if (position[0] !== -1) {
+ this.rallyPoint[0] = (position[0]*3 + CCpos[0]) / 4.0;
+ this.rallyPoint[1] = (position[1]*3 + CCpos[1]) / 4.0;
+ } else {
+ this.rallyPoint[0] = CCpos[0];
+ this.rallyPoint[1] = CCpos[1];
+ }
if (type == 'harass_raid')
{
this.rallyPoint[0] = (position[0]*3.9 + 0.1 * CCpos[0]) / 4.0;
this.rallyPoint[1] = (position[1]*3.9 + 0.1 * CCpos[1]) / 4.0;
}
// some variables for during the attack
this.lastPosition = [0,0];
this.position = [0,0];
this.threatList = []; // sounds so FBI
this.tactics = undefined;
- gameState.ai.queueManager.addQueue("plan_" + this.name, 130); // high priority: some may gather anyway
+ gameState.ai.queueManager.addQueue("plan_" + this.name, 100); // high priority: some may gather anyway
this.queue = gameState.ai.queues["plan_" + this.name];
-
this.assignUnits(gameState);
-
+
+ // get a good path to an estimated target.
+ this.pathFinder = new aStarPath(gameState,false);
};
CityAttack.prototype.getName = function(){
return this.name;
};
CityAttack.prototype.getType = function(){
return this.type;
};
// Returns true if the attack can be executed at the current time
// Basically his checks we have enough units.
// We run a count of our units.
CityAttack.prototype.canStart = function(gameState){
for (unitCat in this.unitStat) {
var Unit = this.unitStat[unitCat];
if (this.unit[unitCat].length < Unit["minSize"])
return false;
}
return true;
// TODO: check if our target is valid and a few other stuffs (good moment to attack?)
};
CityAttack.prototype.isStarted = function(){
+ if ((this.state !== "unexecuted"))
+ debug ("Attack plan already started");
return !(this.state == "unexecuted");
};
CityAttack.prototype.isPaused = function(){
return this.paused;
};
CityAttack.prototype.setPaused = function(gameState, boolValue){
if (!this.paused && boolValue === true) {
this.pausingStart = gameState.getTimeElapsed();
this.paused = true;
debug ("Pausing attack plan " +this.name);
} else if (this.paused && boolValue === false) {
this.totalPausingTime += gameState.getTimeElapsed() - this.pausingStart;
this.paused = false;
debug ("Unpausing attack plan " +this.name);
}
};
CityAttack.prototype.mustStart = function(gameState){
+ if (this.isPaused())
+ return false;
var MaxReachedEverywhere = true;
for (unitCat in this.unitStat) {
var Unit = this.unitStat[unitCat];
if (this.unit[unitCat].length < Unit["targetSize"]) {
MaxReachedEverywhere = false;
}
}
if (MaxReachedEverywhere)
return true;
return (this.maxPreparationTime + this.timeOfPlanStart + this.totalPausingTime < gameState.getTimeElapsed());
};
// Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start"
+// 3 is a special case: no valid path returned. Right now I stop attacking alltogether.
CityAttack.prototype.updatePreparation = function(gameState, militaryManager,events) {
- if (this.isPaused())
- return 1; // continue
-
- Engine.ProfileStart("Update Preparation");
-
- // let's sort by training advancement, ie 'current size / target size'
- this.buildOrder.sort(function (a,b) {
- a[0] = a[2].length/a[3]["targetSize"];
- b[0] = b[2].length/b[3]["targetSize"];
- return (a[0]) - (b[0]);
- });
+ var self = this;
- this.assignUnits(gameState);
+ if (this.path == undefined || this.target == undefined) {
+ // find our target
+ var targets = this.targetFinder(gameState, militaryManager);
+ if (targets.length === 0){
+ targets = this.defaultTargetFinder(gameState, militaryManager);
+ }
+ if (targets.length) {
+ var rand = Math.floor((Math.random()*targets.length));
+ this.targetPos = undefined;
+ var count = 0;
+ while (!this.targetPos){
+ this.target = targets.toEntityArray()[rand];
+ this.targetPos = this.target.position();
+ count++;
+ if (count > 1000){
+ debug("No target with a valid position found");
+ return false;
+ }
+ }
+ this.path = this.pathFinder.getPath(this.rallyPoint,this.targetPos, false, 2);
+ if (this.path === undefined || this.path[1] === true) {
+ return 3;
+ }
+ this.path = this.path[0];
+ } else if (targets.length == 0 ) {
+ gameState.ai.gameFinished = true;
+ debug ("I do not have any target. So I'll just assume I won the game.");
+ return 0;
+ }
+ }
- if ( (gameState.ai.turn + gameState.ai.player) % 40 == 0)
- this.AllToRallyPoint(gameState, false);
-
- var canstart = this.canStart(gameState);
-
- Engine.ProfileStart("Creating units and looking through events");
+ Engine.ProfileStart("Update Preparation");
- // gets the number in training of the same kind as the first one.
- var specialData = "Plan_"+this.name+"_"+this.buildOrder[0][4];
- var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special",specialData);
- if (this.queue.countTotalQueuedUnits() + inTraining + this.buildOrder[0][2].length < Math.min(15,this.buildOrder[0][3]["targetSize"]) ) {
- if (this.buildOrder[0][0] < 1 && this.queue.countTotalQueuedUnits() < 5) {
-
- var template = militaryManager.findBestTrainableUnit(gameState, this.buildOrder[0][1], [ ["strength",1], ["cost",1] ] );
- //debug ("tried " + uneval(this.buildOrder[0][1]) +", and " + template);
- // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, effectively removing the unit from the plan.
- if (template === undefined) {
- delete this.unitStat[this.buildOrder[0][4]]; // deleting the associated unitstat.
- this.buildOrder.splice(0,1);
-
- } else {
- if (gameState.getTemplate(template).hasClasses(["CitizenSoldier", "Infantry"]))
- this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData },this.buildOrder[0][3]["batchSize"] ) );
- else
- this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData },this.buildOrder[0][3]["batchSize"] ) );
+ // keep on while the units finish being trained.
+ if (this.mustStart(gameState) && gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) ) {
+ this.assignUnits(gameState);
+ if ( (gameState.ai.turn + gameState.ai.player) % 40 == 0) {
+ this.AllToRallyPoint(gameState, true); // gain some time, start regrouping
+ this.unitCollection.forEach(function (entity) { entity.setMetadata("role","attack"); });
+ }
+ Engine.ProfileStop();
+ return 1;
+ } else if (!this.mustStart(gameState)) {
+ // We still have time left to recruit units and do stuffs.
+
+ // let's sort by training advancement, ie 'current size / target size'
+ // count the number of queued units too.
+ // substract priority.
+ this.buildOrder.sort(function (a,b) { //}) {
+
+ var aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+self.buildOrder[0][4]);
+ aQueued += self.queue.countTotalQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+self.buildOrder[0][4]);
+ a[0] = (a[2].length + aQueued)/a[3]["targetSize"];
+
+ var bQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+self.buildOrder[0][4]);
+ bQueued += self.queue.countTotalQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+self.buildOrder[0][4]);
+ b[0] = (b[2].length + bQueued)/b[3]["targetSize"];
+
+ a[0] -= a[3]["priority"];
+ b[0] -= b[3]["priority"];
+ return (a[0]) - (b[0]);
+ });
+
+ if (!this.isPaused()) {
+ this.assignUnits(gameState);
+
+ if ( (gameState.ai.turn + gameState.ai.player) % 40 == 0) {
+ this.AllToRallyPoint(gameState, false);
+ this.unitCollection.setStance("defensive"); // make sure units won't disperse out of control
}
}
- }
- // can happen for now
- if (this.buildOrder.length === 0) {
- debug ("Ending plan: no build orders");
- return 0; // will abort the plan, should return something else
- }
-
- for (var key in events){
- var e = events[key];
- if (e.type === "Attacked" && e.msg){
- if (this.unitCollection.toIdArray().indexOf(e.msg.target) !== -1){
- var attacker = gameState.getEntityById(e.msg.attacker);
- if (attacker && attacker.position()) {
- this.unitCollection.attack(e.msg.attacker);
- break;
+
+ Engine.ProfileStart("Creating units.");
+
+ // gets the number in training of the same kind as the first one.
+ var specialData = "Plan_"+this.name+"_"+this.buildOrder[0][4];
+ var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special",specialData);
+ if (this.queue.countTotalQueuedUnits() + inTraining + this.buildOrder[0][2].length < Math.min(15,this.buildOrder[0][3]["targetSize"]) ) {
+ if (this.buildOrder[0][0] < 1 && this.queue.length() < 4) {
+
+ var template = militaryManager.findBestTrainableUnit(gameState, this.buildOrder[0][1], [ ["strength",1], ["cost",1] ] );
+ //debug ("tried " + uneval(this.buildOrder[0][1]) +", and " + template);
+ // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, effectively removing the unit from the plan.
+ if (template === undefined) {
+ delete this.unitStat[this.buildOrder[0][4]]; // deleting the associated unitstat.
+ this.buildOrder.splice(0,1);
+
+ } else {
+ if (gameState.getTemplate(template).hasClasses(["CitizenSoldier", "Infantry"]))
+ this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData },this.buildOrder[0][3]["batchSize"] ) );
+ else
+ this.queue.addItem( new UnitTrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData },this.buildOrder[0][3]["batchSize"] ) );
+ }
+ }
+ }
+ /*
+ if (!this.startedPathing && this.path === undefined) {
+
+ // find our target
+ var targets = this.targetFinder(gameState, militaryManager);
+ if (targets.length === 0){
+ targets = this.defaultTargetFinder(gameState, militaryManager);
+ }
+ if (targets.length) {
+ var rand = Math.floor((Math.random()*targets.length));
+ this.targetPos = undefined;
+ var count = 0;
+ while (!this.targetPos){
+ var target = targets.toEntityArray()[rand];
+ this.targetPos = target.position();
+ count++;
+ if (count > 1000){
+ debug("No target with a valid position found");
+ return false;
+ }
}
+ this.startedPathing = true;
+ // Start pathfinding using the optimized version, with a minimal sampling of 2
+ this.pathFinder.getPath(this.rallyPoint,this.targetPos, false, 2, gameState);
+ }
+ } else if (this.startedPathing) {
+ var path = this.pathFinder.continuePath(gameState);
+ if (path !== "toBeContinued") {
+ this.startedPathing = false;
+ this.path = path;
+ debug("Pathing ended");
}
}
+ */
+ // can happen for now
+ if (this.buildOrder.length === 0) {
+ debug ("Ending plan: no build orders");
+ return 0; // will abort the plan, should return something else
+ }
+ Engine.ProfileStop();
+ Engine.ProfileStop();
+ return 1;
}
Engine.ProfileStop();
- // we count our units by triggering "canStart"
- // returns false if we can no longer have time and cannot start.
- // returns 0 if I must start and can't, returns 1 if I don't have to start, and returns 2 if I must start and can
- if (!this.mustStart(gameState))
- return 1;
- else if (canstart)
+ // if we're here, it means we must start (and have no units in training left).
+ // if we can, do, else, abort.
+ if (this.canStart(gameState))
return 2;
else
return 0;
return 0;
};
CityAttack.prototype.assignUnits = function(gameState){
var self = this;
// TODO: assign myself units that fit only, right now I'm getting anything.
-
- /*
- // I'll take any unit set to "Defense" that has no subrole (ie is set to be a defensive unit, but has no particular task)
- // I assign it to myself, and then it's mine, the entity collection will detect it.
- var Defenders = gameState.getOwnEntitiesByRole("defence");
- Defenders.forEach(function(ent) {
- if (ent.getMetadata("subrole") == "idle" || !ent.getMetadata("subrole")) {
- ent.setMetadata("role", "attack");
- ent.setMetadata("plan", self.name);
- }
- });*/
// Assign all no-roles that fit (after a plan aborts, for example).
var NoRole = gameState.getOwnEntitiesByRole(undefined);
NoRole.forEach(function(ent) {
- ent.setMetadata("role", "attack");
+ if (ent.hasClasses(["CitizenSoldier", "Infantry"]))
+ ent.setMetadata("role", "worker");
+ else
+ ent.setMetadata("role", "attack");
ent.setMetadata("plan", self.name);
});
};
// this sends a unit by ID back to the "rally point"
CityAttack.prototype.ToRallyPoint = function(gameState,id)
{
// Move back to nearest rallypoint
gameState.getEntityById(id).move(this.rallyPoint[0],this.rallyPoint[1]);
}
// this sends all units back to the "rally point" by entity collections.
CityAttack.prototype.AllToRallyPoint = function(gameState, evenWorkers) {
var self = this;
if (evenWorkers) {
for (unitCat in this.unit) {
this.unit[unitCat].move(this.rallyPoint[0],this.rallyPoint[1]);
}
} else {
for (unitCat in this.unit) {
this.unit[unitCat].forEach(function (ent) {
if (ent.getMetadata("role") != "worker")
ent.move(self.rallyPoint[0],self.rallyPoint[1]);
});
}
}
}
// Default target finder aims for conquest critical targets
CityAttack.prototype.defaultTargetFinder = function(gameState, militaryManager){
var targets = undefined;
- targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("ConquestCritical");
+ targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("CivCentre");
+ if (targets.length == 0) {
+ targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("ConquestCritical");
+ }
// If there's nothing, attack anything else that's less critical
if (targets.length == 0) {
targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("Town");
}
if (targets.length == 0) {
targets = militaryManager.enemyWatchers[this.targetPlayer].getEnemyBuildings("Village");
}
+ // no buildings, attack anything conquest critical, even units (it's assuming it won't move).
+ if (targets.length == 0) {
+ targets = gameState.getEnemyEntities().filter(Filters.byClass("ConquestCritical"));
+ }
return targets;
};
// tupdate
CityAttack.prototype.raidingTargetFinder = function(gameState, militaryManager, Target){
var targets = undefined;
if (Target == "villager")
{
// let's aim for any resource dropsite. We assume villagers are in the neighborhood (note: the human player could certainly troll us... small (scouting) TODO here.)
targets = gameState.entities.filter(function(ent) {
return (ent.hasClass("Structure") && ent.resourceDropsiteTypes() !== undefined && !ent.hasClass("CivCentre") && ent.owner() === this.targetPlayer && ent.position());
});
if (targets.length == 0) {
targets = gameState.entities.filter(function(ent) {
return (ent.hasClass("CivCentre") && ent.resourceDropsiteTypes() !== undefined && ent.owner() === this.targetPlayer && ent.position());
});
}
if (targets.length == 0) {
// if we're here, it means they also don't have no CC... So I'll just take any building at this point.
targets = gameState.entities.filter(function(ent) {
return (ent.hasClass("Structure") && ent.owner() === this.targetPlayer && ent.position());
});
}
return targets;
} else {
return this.defaultTargetFinder(gameState, militaryManager);
}
};
// Executes the attack plan, after this is executed the update function will be run every turn
// If we're here, it's because we have in our IDlist enough units.
// now the IDlist units are treated turn by turn
CityAttack.prototype.StartAttack = function(gameState, militaryManager){
-
- var targets = [];
- if (this.type === "harass_raid")
- targets = this.targetFinder(gameState, militaryManager, "villager");
- else
- {
- targets = this.targetFinder(gameState, militaryManager);
- if (targets.length === 0){
- targets = this.defaultTargetFinder(gameState, militaryManager);
- }
- }
+ // check we have a target and a path.
- // If we have a target, move to it
- if (targets.length) {
- var curPos = this.unitCollection.getCentrePosition();
-
- // pick a random target from the list
- var rand = Math.floor((Math.random()*targets.length));
- this.targetPos = undefined;
- var count = 0;
- while (!this.targetPos){
- var target = targets.toEntityArray()[rand];
- this.targetPos = target.position();
- count++;
- if (count > 1000){
- warn("No target with a valid position found");
- return false;
- }
- }
+ if (this.targetPos && this.path !== undefined) {
+ // erase our queue. This will stop any leftover unit from being trained.
+ gameState.ai.queueManager.removeQueue("plan_" + this.name);
- // Find possible distinct paths to the enemy
- var pathFinder = new PathFinder(gameState);
- var pathsToEnemy = pathFinder.getPaths(curPos, this.targetPos);
- if (! pathsToEnemy){
- pathsToEnemy = [[this.targetPos]];
- }
- this.path = [];
-
- if (this.type !== "harass_raid")
- {
- var rand = Math.floor(Math.random() * pathsToEnemy.length);
- this.path = pathsToEnemy[rand];
- } else {
- this.path = pathsToEnemy[Math.min(2,pathsToEnemy.length-1)];
- }
+ var curPos = this.unitCollection.getCentrePosition();
this.unitCollection.forEach(function(ent) { ent.setMetadata("subrole", "attacking"); ent.setMetadata("role", "attack") ;});
// filtering by those that started to attack only
var filter = Filters.byMetadata("subrole","attacking");
this.unitCollection = this.unitCollection.filter(filter);
- //this.unitCollection.registerUpdates();
+ this.unitCollection.registerUpdates();
//this.unitCollection.length;
for (unitCat in this.unitStat) {
var cat = unitCat;
this.unit[cat] = this.unit[cat].filter(filter);
}
this.unitCollection.move(this.path[0][0], this.path[0][1]);
debug ("Started to attack with the plan " + this.name);
this.state = "walking";
- } else if (targets.length == 0 ) {
+ } else {
gameState.ai.gameFinished = true;
debug ("I do not have any target. So I'll just assume I won the game.");
return true;
}
return true;
};
// Runs every turn after the attack is executed
CityAttack.prototype.update = function(gameState, militaryManager, events){
+ var self = this;
Engine.ProfileStart("Update Attack");
// we're marching towards the target
// Check for attacked units in our band.
var bool_attacked = false;
// raids don't care about attacks much
this.position = this.unitCollection.getCentrePosition();
var IDs = this.unitCollection.toIdArray();
// this actually doesn't do anything right now.
if (this.state === "walking") {
var toProcess = {};
var armyToProcess = {};
// Let's check if any of our unit has been attacked. In case yes, we'll determine if we're simply off against an enemy army, a lone unit/builing
// or if we reached the enemy base. Different plans may react differently.
for (var key in events) {
var e = events[key];
if (e.type === "Attacked" && e.msg) {
if (IDs.indexOf(e.msg.target) !== -1) {
var attacker = gameState.getEntityById(e.msg.attacker);
var ourUnit = gameState.getEntityById(e.msg.target);
if (attacker && attacker.position() && attacker.hasClass("Unit") && attacker.owner() != 0 && attacker.owner() != gameState.player) {
var territoryMap = Map.createTerritoryMap(gameState);
if ( +territoryMap.point(attacker.position()) - 64 === +this.targetPlayer)
{
debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination.");
// we must assume we've arrived at the end of the trail.
this.state = "arrived";
}
if (militaryManager.enemyWatchers[attacker.owner()]) {
toProcess[attacker.id()] = attacker;
var armyID = militaryManager.enemyWatchers[attacker.owner()].getArmyFromMember(attacker.id());
armyToProcess[armyID[0]] = armyID[1];
}
}
// if we're being attacked by a building, flee.
if (attacker && ourUnit && attacker.hasClass("Structure")) {
ourUnit.flee(attacker);
}
}
}
}
// I don't process attacks if I'm in their base because I'll have already gone to "attacking" mode.
// I'll process by army
var total = 0;
for (armyID in armyToProcess) {
total += armyToProcess[armyID].length;
// TODO: if it's a big army, we may want to refer the scouting/defense manager
}
/*
}&& this.type !== "harass_raid"){ // walking toward the target
var sumAttackerPos = [0,0];
var numAttackers = 0;
// let's check if one of our unit is not under attack, by any chance.
for (var key in events){
var e = events[key];
if (e.type === "Attacked" && e.msg){
if (this.unitCollection.toIdArray().indexOf(e.msg.target) !== -1){
var attacker = HeadQuarters.entity(e.msg.attacker);
if (attacker && attacker.position()){
sumAttackerPos[0] += attacker.position()[0];
sumAttackerPos[1] += attacker.position()[1];
numAttackers += 1;
bool_attacked = true;
// todo: differentiate depending on attacker type... If it's a ship, let's not do anythin, a building, depends on the attack type/
if (this.threatList.indexOf(e.msg.attacker) === -1)
{
var enemySoldiers = HeadQuarters.getEnemySoldiers().toEntityArray();
for (j in enemySoldiers)
{
var enemy = enemySoldiers[j];
if (enemy.position() === undefined) // likely garrisoned
continue;
if (inRange(enemy.position(), attacker.position(), 1000) && this.threatList.indexOf(enemy.id()) === -1)
this.threatList.push(enemy.id());
}
this.threatList.push(e.msg.attacker);
}
}
}
}
}
if (bool_attacked > 0){
var avgAttackerPos = [sumAttackerPos[0]/numAttackers, sumAttackerPos[1]/numAttackers];
units.move(avgAttackerPos[0], avgAttackerPos[1]); // let's run towards it.
this.tactics = new Tactics(gameState,HeadQuarters, this.idList,this.threatList,true);
this.state = "attacking_threat";
}
}else if (this.state === "attacking_threat"){
this.tactics.eventMetadataCleanup(events,HeadQuarters);
var removeList = this.tactics.removeTheirDeads(HeadQuarters);
this.tactics.removeMyDeads(HeadQuarters);
for (var i in removeList){
this.threatList.splice(this.threatList.indexOf(removeList[i]),1);
}
if (this.threatList.length <= 0)
{
this.tactics.disband(HeadQuarters,events);
this.tactics = undefined;
this.state = "walking";
units.move(this.path[0][0], this.path[0][1]);
}else
{
this.tactics.reassignAttacks(HeadQuarters);
}
}*/
}
if (this.state === "walking"){
- if (SquareVectorDistance(this.position, this.lastPosition) < 400 && this.path.length > 0) {
+ if (SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) {
this.unitCollection.move(this.path[0][0], this.path[0][1]);
}
- if (SquareVectorDistance(this.unitCollection.getCentrePosition(), this.path[0]) < 400){
+ if (SquareVectorDistance(this.unitCollection.getCentrePosition(), this.path[0]) < 900){
this.path.shift();
if (this.path.length > 0){
this.unitCollection.move(this.path[0][0], this.path[0][1]);
} else {
debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination.");
// we must assume we've arrived at the end of the trail.
this.state = "arrived";
}
}
}
// todo: re-implement raiding
if (this.state === "arrived"){
// let's proceed on with whatever happens now.
// There's a ton of TODOs on this part.
if (this.onArrivalReaction == "proceedOnTargets") {
- // Each unit will randomly pick a target and attack it and then they'll do what they feel like doing for now. TODO
- // only the targeted enemy. I've seen the units attack gazelles otherwise.
- var enemyUnits = gameState.getEnemyEntities().filter(Filters.and(Filters.byStaticDistance(this.unitCollection.getCentrePosition(), 100), Filters.byClass("Unit")));
- enemyUnits = enemyUnits.filter(Filters.byOwner(this.targetEnemy));
- var enemyStructures = gameState.getEnemyEntities().filter(Filters.and(Filters.byStaticDistance(this.unitCollection.getCentrePosition(), 150), Filters.byClass("Structure")));
- enemyStructures = enemyStructures.filter(Filters.byOwner(this.targetEnemy));
- enemyUnits = enemyUnits.toEntityArray();
- enemyStructures = enemyStructures.toEntityArray();
-
+ this.state = "";
this.unitCollection.forEach( function (ent) { //}) {
- if (ent.hasClass("Siege")) {
- if (enemyStructures.length !== 0) {
- var rand = Math.floor(Math.random() * enemyStructures.length*0.99);
- ent.attack(enemyStructures[+rand].id());
- } else
- ent.stopMoving();
- } else {
- if (enemyUnits.length !== 0) {
- var rand = Math.floor(Math.random() * enemyUnits.length*0.99);
- ent.attack(enemyUnits[(+rand)].id());
- } else
- ent.stopMoving();
- }
+ ent.stopMoving();
});
- this.state = "";
} else if (this.onArrivalReaction == "huntVillagers") {
// let's get any villager and target them with a tactics manager
var enemyCitizens = gameState.entities.filter(function(ent) {
return (gameState.isEntityEnemy(ent) && ent.hasClass("Support") && ent.owner() !== 0 && ent.position());
});
var targetList = [];
enemyCitizens.forEach( function (enemy) {
if (inRange(enemy.position(), units.getCentrePosition(), 2500) && targetList.indexOf(enemy.id()) === -1)
targetList.push(enemy.id());
});
if (targetList.length > 0)
{
this.tactics = new Tactics(gameState,HeadQuarters, this.idList,targetList);
this.state = "huntVillagers";
} else {
this.state = "";
}
}
}
- if (this.state === ""){
+
+ if (this.state === "" && gameState.ai.playedTurn % 3 === 0) {
// Each unit will randomly pick a target and attack it and then they'll do what they feel like doing for now. TODO
// only the targeted enemy. I've seen the units attack gazelles otherwise.
- var enemyUnits = gameState.getEnemyEntities().filter(Filters.and(Filters.byStaticDistance(this.unitCollection.getCentrePosition(), 200), Filters.byClass("Unit")));
- enemyUnits = enemyUnits.filter(Filters.byOwner(this.targetEnemy));
- var enemyStructures = gameState.getEnemyEntities().filter(Filters.and(Filters.byStaticDistance(this.unitCollection.getCentrePosition(), 250), Filters.byClass("Structure")));
- enemyStructures = enemyStructures.filter(Filters.byOwner(this.targetEnemy));
- enemyUnits = enemyUnits.toEntityArray();
- enemyStructures = enemyStructures.toEntityArray();
-
+ var enemyUnits = gameState.getEnemyEntities().filter(Filters.and(Filters.byOwner(this.targetPlayer), Filters.byClass("Unit")));
+ var enemyStructures = gameState.getEnemyEntities().filter(Filters.and(Filters.byOwner(this.targetPlayer), Filters.byClass("Structure")));
this.unitCollection.forEach( function (ent) { //}) {
if (ent.isIdle()) {
+ var mStruct = enemyStructures.filter(function (enemy) {// }){
+ if (!enemy.position()) {
+ return false;
+ }
+ if (SquareVectorDistance(enemy.position(),ent.position()) > ent.visionRange()*ent.visionRange() + 100) {
+ return false;
+ }
+ return true;
+ });
+ var mUnit = enemyUnits.filter(function (enemy) {// }){
+ if (!enemy.position()) {
+ return false;
+ }
+ if (SquareVectorDistance(enemy.position(),ent.position()) > ent.visionRange()*ent.visionRange() + 100) {
+ return false;
+ }
+ return true;
+ });
+ mUnit = mUnit.toEntityArray();
+ mStruct = mStruct.toEntityArray();
if (ent.hasClass("Siege")) {
- if (enemyStructures.length !== 0) {
- var rand = Math.floor(Math.random() * enemyStructures.length*0.99);
- ent.attack(enemyStructures[+rand].id());
- } else
- ent.stopMoving();
+ if (mStruct.length !== 0) {
+ var rand = Math.floor(Math.random() * mStruct.length*0.99);
+ ent.attack(mStruct[+rand].id());
+ //debug ("Siege units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName());
+ } else if (SquareVectorDistance(self.targetPos, ent.position()) > 900 ){
+ //debug ("Siege units moving to " + uneval(self.targetPos));
+ ent.move((self.targetPos[0] + ent.position()[0])/2,(self.targetPos[1] + ent.position()[1])/2);
+ }
} else {
- if (enemyUnits.length !== 0) {
- var rand = Math.floor(Math.random() * enemyUnits.length*0.99);
- ent.attack(enemyUnits[(+rand)].id());
- } else
- ent.stopMoving();
+ if (mUnit.length !== 0) {
+ var rand = Math.floor(Math.random() * mUnit.length*0.99);
+ ent.attack(mUnit[(+rand)].id());
+ //debug ("Units attacking a unit from " +mUnit[+rand].owner() + " , " +mUnit[+rand].templateName());
+ } else if (mStruct.length !== 0) {
+ var rand = Math.floor(Math.random() * mStruct.length*0.99);
+ ent.attack(mStruct[+rand].id());
+ //debug ("Units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName());
+ } else if (SquareVectorDistance(self.targetPos, ent.position()) > 900 ){
+ //debug ("Units moving to " + uneval(self.targetPos));
+ ent.move((self.targetPos[0] + ent.position()[0])/2,(self.targetPos[1] + ent.position()[1])/2);
+ }
}
}
});
}
/*
if (this.state === "huntVillagers")
{
this.tactics.eventMetadataCleanup(events,HeadQuarters);
this.tactics.removeTheirDeads(HeadQuarters);
this.tactics.removeMyDeads(HeadQuarters);
if (this.tactics.isBattleOver())
{
this.tactics.disband(HeadQuarters,events);
this.tactics = undefined;
this.state = "";
return 0; // assume over
} else
this.tactics.reassignAttacks(HeadQuarters);
}*/
this.lastPosition = this.position;
Engine.ProfileStop();
+
return this.unitCollection.length;
};
CityAttack.prototype.totalCountUnits = function(gameState){
var totalcount = 0;
for (i in this.idList)
{
totalcount++;
}
return totalcount;
};
// reset any units
CityAttack.prototype.Abort = function(gameState){
this.unitCollection.forEach(function(ent) {
ent.setMetadata("role",undefined);
ent.setMetadata("subrole",undefined);
ent.setMetadata("plan",undefined);
});
for (unitCat in this.unitStat) {
delete this.unitStat[unitCat];
delete this.unit[unitCat];
}
delete this.unitCollection;
gameState.ai.queueManager.removeQueue("plan_" + this.name);
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/template-manager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/template-manager.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/template-manager.js (revision 12343)
@@ -1,76 +1,115 @@
/*
* Used to know which templates I have, which templates I know I can train, things like that.
*/
var TemplateManager = function(gameState) {
var self = this;
this.knownTemplatesList = [];
this.buildingTemplates = [];
this.unitTemplates = [];
-
+ this.templateCounters = {};
+ this.templateCounteredBy = {};
+
// this will store templates that exist
this.AcknowledgeTemplates(gameState);
this.getBuildableSubtemplates(gameState);
this.getTrainableSubtemplates(gameState);
this.getBuildableSubtemplates(gameState);
this.getTrainableSubtemplates(gameState);
// should be enough in 100% of the cases.
+ this.getTemplateCounters(gameState);
+
};
TemplateManager.prototype.AcknowledgeTemplates = function(gameState)
{
var self = this;
var myEntities = gameState.getOwnEntities();
myEntities.forEach(function(ent) { // }){
var template = ent._templateName;
if (self.knownTemplatesList.indexOf(template) === -1) {
self.knownTemplatesList.push(template);
if (ent.hasClass("Unit") && self.unitTemplates.indexOf(template) === -1)
self.unitTemplates.push(template);
else if (self.buildingTemplates.indexOf(template) === -1)
self.buildingTemplates.push(template);
}
});
}
TemplateManager.prototype.getBuildableSubtemplates = function(gameState)
{
for each (templateName in this.knownTemplatesList) {
var template = gameState.getTemplate(templateName);
if (template !== null) {
var buildable = template.buildableEntities();
if (buildable !== undefined)
for each (subtpname in buildable) {
if (this.knownTemplatesList.indexOf(subtpname) === -1) {
this.knownTemplatesList.push(subtpname);
var subtemplate = gameState.getTemplate(subtpname);
if (subtemplate.hasClass("Unit") && this.unitTemplates.indexOf(subtpname) === -1)
this.unitTemplates.push(subtpname);
else if (this.buildingTemplates.indexOf(subtpname) === -1)
this.buildingTemplates.push(subtpname);
}
}
}
}
}
TemplateManager.prototype.getTrainableSubtemplates = function(gameState)
{
for each (templateName in this.knownTemplatesList) {
var template = gameState.getTemplate(templateName);
if (template !== null) {
var trainables = template.trainableEntities();
if (trainables !== undefined)
for each (subtpname in trainables) {
if (this.knownTemplatesList.indexOf(subtpname) === -1) {
this.knownTemplatesList.push(subtpname);
var subtemplate = gameState.getTemplate(subtpname);
if (subtemplate.hasClass("Unit") && this.unitTemplates.indexOf(subtpname) === -1)
this.unitTemplates.push(subtpname);
else if (this.buildingTemplates.indexOf(subtpname) === -1)
this.buildingTemplates.push(subtpname);
}
}
}
}
}
+TemplateManager.prototype.getTemplateCounters = function(gameState)
+{
+ for (i in this.unitTemplates)
+ {
+ var tp = gameState.getTemplate(this.unitTemplates[i]);
+ var tpname = this.unitTemplates[i];
+ this.templateCounters[tpname] = tp.getCounteredClasses();
+ }
+}
+// features auto-caching
+TemplateManager.prototype.getCountersToClasses = function(gameState,classes,templateName)
+{
+ if (templateName !== undefined && this.templateCounteredBy[templateName])
+ return this.templateCounteredBy[templateName];
+
+ var templates = [];
+ for (i in this.templateCounters) {
+ var okay = false;
+ for each (ticket in this.templateCounters[i]) {
+ var okaya = true;
+ for (a in ticket[0]) {
+ if (classes.indexOf(ticket[0][a]) === -1)
+ okaya = false;
+ }
+ if (okaya && templates.indexOf(i) === -1)
+ templates.push([i, ticket[1]]);
+ }
+ }
+ templates.sort (function (a,b) { return -a[1] + b[1]; });
+
+ if (templateName !== undefined)
+ this.templateCounteredBy[templateName] = templates;
+ return templates;
+}
+
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/queue.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/queue.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/queue.js (revision 12343)
@@ -1,126 +1,138 @@
/*
* Holds a list of wanted items to train or construct
*/
var Queue = function() {
this.queue = [];
this.outQueue = [];
};
Queue.prototype.addItem = function(plan) {
this.queue.push(plan);
};
Queue.prototype.getNext = function() {
if (this.queue.length > 0) {
return this.queue[0];
} else {
return null;
}
};
Queue.prototype.outQueueNext = function(){
if (this.outQueue.length > 0) {
return this.outQueue[0];
} else {
return null;
}
};
Queue.prototype.outQueueCost = function(){
var cost = new Resources();
for (var key in this.outQueue){
cost.add(this.outQueue[key].getCost());
}
return cost;
};
Queue.prototype.nextToOutQueue = function(){
if (this.queue.length > 0){
if (this.outQueue.length > 0 &&
this.getNext().category === "unit" &&
this.outQueue[this.outQueue.length-1].type === this.getNext().type &&
this.outQueue[this.outQueue.length-1].number < 5){
this.queue.shift();
this.outQueue[this.outQueue.length-1].addItem();
}else{
this.outQueue.push(this.queue.shift());
}
}
};
Queue.prototype.executeNext = function(gameState) {
if (this.outQueue.length > 0) {
this.outQueue.shift().execute(gameState);
return true;
} else {
return false;
}
};
Queue.prototype.length = function() {
return this.queue.length;
};
Queue.prototype.countQueuedUnits = function(){
var count = 0;
for (var i in this.queue){
count += this.queue[i].number;
}
return count;
};
Queue.prototype.countOutQueuedUnits = function(){
var count = 0;
for (var i in this.outQueue){
count += this.outQueue[i].number;
}
return count;
};
Queue.prototype.countTotalQueuedUnits = function(){
var count = 0;
for (var i in this.queue){
count += this.queue[i].number;
}
for (var i in this.outQueue){
count += this.outQueue[i].number;
}
return count;
};
Queue.prototype.countTotalQueuedUnitsWithClass = function(classe){
var count = 0;
for (var i in this.queue){
if (this.queue[i].template && this.queue[i].template.hasClass(classe))
count += this.queue[i].number;
}
for (var i in this.outQueue){
if (this.outQueue[i].template && this.outQueue[i].template.hasClass(classe))
count += this.outQueue[i].number;
}
return count;
};
+Queue.prototype.countTotalQueuedUnitsWithMetadata = function(data,value){
+ var count = 0;
+ for (var i in this.queue){
+ if (this.queue[i].metadata[data] && this.queue[i].metadata[data] == value)
+ count += this.queue[i].number;
+ }
+ for (var i in this.outQueue){
+ if (this.outQueue[i].metadata[data] && this.outQueue[i].metadata[data] == value)
+ count += this.outQueue[i].number;
+ }
+ return count;
+};
Queue.prototype.totalLength = function(){
return this.queue.length + this.outQueue.length;
};
Queue.prototype.outQueueLength = function(){
return this.outQueue.length;
};
Queue.prototype.countAllByType = function(t){
var count = 0;
for (var i = 0; i < this.queue.length; i++){
if (this.queue[i].type === t){
count += this.queue[i].number;
}
}
for (var i = 0; i < this.outQueue.length; i++){
if (this.outQueue[i].type === t){
count += this.outQueue[i].number;
}
}
return count;
};
\ No newline at end of file
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/plan-building.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/plan-building.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/plan-building.js (revision 12343)
@@ -1,155 +1,168 @@
var BuildingConstructionPlan = function(gameState, type, position) {
this.type = gameState.applyCiv(type);
this.position = position;
this.template = gameState.getTemplate(this.type);
if (!this.template) {
this.invalidTemplate = true;
this.template = undefined;
debug("Cannot build " + this.type);
return;
}
this.category = "building";
this.cost = new Resources(this.template.cost());
this.number = 1; // The number of buildings to build
};
BuildingConstructionPlan.prototype.canExecute = function(gameState) {
if (this.invalidTemplate){
return false;
}
// TODO: verify numeric limits etc
var builders = gameState.findBuilders(this.type);
return (builders.length != 0);
};
BuildingConstructionPlan.prototype.execute = function(gameState) {
var builders = gameState.findBuilders(this.type).toEntityArray();
// We don't care which builder we assign, since they won't actually
// do the building themselves - all we care about is that there is
// some unit that can start the foundation
var pos = this.findGoodPosition(gameState);
if (!pos){
debug("No room to place " + this.type);
return;
}
builders[0].construct(this.type, pos.x, pos.z, pos.angle);
};
BuildingConstructionPlan.prototype.getCost = function() {
return this.cost;
};
BuildingConstructionPlan.prototype.findGoodPosition = function(gameState) {
var template = gameState.getTemplate(this.type);
var cellSize = gameState.cellSize; // size of each tile
// First, find all tiles that are far enough away from obstructions:
var obstructionMap = Map.createObstructionMap(gameState,template);
- //obstructionMap.dumpIm("obstructions.png");
+ ///obstructionMap.dumpIm("obstructions.png");
obstructionMap.expandInfluences();
// Compute each tile's closeness to friendly structures:
var friendlyTiles = new Map(gameState);
+ var alreadyHasHouses = false;
+
// If a position was specified then place the building as close to it as possible
if (this.position){
var x = Math.round(this.position[0] / cellSize);
var z = Math.round(this.position[1] / cellSize);
friendlyTiles.addInfluence(x, z, 200);
- //friendlyTiles.dumpIm("pos.png", 200);
}else{
// No position was specified so try and find a sensible place to build
gameState.getOwnEntities().forEach(function(ent) {
if (ent.hasClass("Structure")) {
var infl = 32;
if (ent.hasClass("CivCentre"))
infl *= 4;
var pos = ent.position();
var x = Math.round(pos[0] / cellSize);
var z = Math.round(pos[1] / cellSize);
if (ent.buildCategory() == "Wall") { // no real blockers, but can't build where they are
friendlyTiles.addInfluence(x, z, 2,-1000);
return;
}
if (template._template.BuildRestrictions.Category === "Field"){
if (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf("food") !== -1){
if (ent.hasClass("CivCentre"))
friendlyTiles.addInfluence(x, z, infl/4, infl);
else
friendlyTiles.addInfluence(x, z, infl, infl);
}
}else{
- if (template.genericName() == "House" && ent.genericName() == "House")
- friendlyTiles.addInfluence(x, z, infl*2.0); // houses are close to other houses
- else if (template.genericName() == "House") {
+ if (template.genericName() == "House" && ent.genericName() == "House") {
+ friendlyTiles.addInfluence(x, z, 15.0,20,'linear'); // houses are close to other houses
+ alreadyHasHouses = true;
+ } else if (template.genericName() == "House") {
friendlyTiles.addInfluence(x, z, Math.ceil(infl/2.0),infl); // houses are farther away from other buildings but houses
friendlyTiles.addInfluence(x, z, Math.ceil(infl/4.0),-infl/2.0); // houses are farther away from other buildings but houses
} else if (ent.genericName() != "House") // houses have no influence on other buildings
friendlyTiles.addInfluence(x, z, infl);
// If this is not a field add a negative influence near the CivCentre because we want to leave this
// area for fields.
if (ent.hasClass("CivCentre") && template.genericName() != "House"){
friendlyTiles.addInfluence(x, z, Math.floor(infl/8), Math.floor(-infl/2));
} else if (ent.hasClass("CivCentre")) {
- friendlyTiles.addInfluence(x, z, Math.floor(infl/3.0), infl + 1);
- friendlyTiles.addInfluence(x, z, Math.floor(infl/4), -Math.floor(infl));
+ friendlyTiles.addInfluence(x, z, infl/3.0, infl + 1);
+ friendlyTiles.addInfluence(x, z, Math.ceil(infl/5.0), -(infl/2.0), 'linear');
}
}
}
});
}
+ //friendlyTiles.dumpIm("Building " +gameState.getTimeElapsed() + ".png", 200);
+
+
// Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, this
// allows room for units to walk between buildings.
// note: not for houses and dropsites who ought to be closer to either each other or a resource.
// also not for fields who can be stacked quite a bit
if (template.genericName() == "Field")
var radius = Math.ceil(template.obstructionRadius() / cellSize) - 0.7;
else if (template.buildCategory() === "Dock")
var radius = 0;
else if (template.genericName() != "House" && !template.hasClass("DropsiteWood") && !template.hasClass("DropsiteStone") && !template.hasClass("DropsiteMetal"))
- var radius = Math.ceil(template.obstructionRadius() / cellSize) + 2;
+ var radius = Math.ceil(template.obstructionRadius() / cellSize) + 1;
else
var radius = Math.ceil(template.obstructionRadius() / cellSize);
+ // further contract cause walls
if (gameState.playerData.civ == "iber")
radius *= 0.95;
- // Find the best non-obstructed tile
- var bestTile = friendlyTiles.findBestTile(radius, obstructionMap);
- var bestIdx = bestTile[0];
- var bestVal = bestTile[1];
-
+ // Find the best non-obstructed
+ if (template.genericName() == "House" && !alreadyHasHouses) {
+ // try to get some space first
+ var bestTile = friendlyTiles.findBestTile(10, obstructionMap);
+ var bestIdx = bestTile[0];
+ var bestVal = bestTile[1];
+ }
+ if (bestVal === undefined || bestVal === -1) {
+ var bestTile = friendlyTiles.findBestTile(radius, obstructionMap);
+ var bestIdx = bestTile[0];
+ var bestVal = bestTile[1];
+ }
if (bestVal === -1){
return false;
}
var x = ((bestIdx % friendlyTiles.width) + 0.5) * cellSize;
var z = (Math.floor(bestIdx / friendlyTiles.width) + 0.5) * cellSize;
// default angle
var angle = 3*Math.PI/4;
return {
"x" : x,
"z" : z,
"angle" : angle
};
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/terrain-analysis.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/terrain-analysis.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/terrain-analysis.js (revision 12343)
@@ -1,350 +1,711 @@
/*
* TerrainAnalysis inherits from Map
*
* This creates a suitable passability map for pathfinding units and provides the findClosestPassablePoint() function.
* This is intended to be a base object for the terrain analysis modules to inherit from.
*/
function TerrainAnalysis(gameState){
var passabilityMap = gameState.getMap();
var obstructionMask = gameState.getPassabilityClassMask("pathfinderObstruction");
obstructionMask |= gameState.getPassabilityClassMask("default");
var obstructionTiles = new Uint16Array(passabilityMap.data.length);
for (var i = 0; i < passabilityMap.data.length; ++i)
{
obstructionTiles[i] = (passabilityMap.data[i] & obstructionMask) ? 0 : 65535;
}
this.Map(gameState, obstructionTiles);
};
copyPrototype(TerrainAnalysis, Map);
// Returns the (approximately) closest point which is passable by searching in a spiral pattern
TerrainAnalysis.prototype.findClosestPassablePoint = function(startPoint, quick, limitDistance){
var w = this.width;
var p = startPoint;
var direction = 1;
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
this.map[p[0] + w*p[1]] != 0){
if (this.countConnected(p, 10) >= 10){
return p;
}
}
var count = 0;
// search in a spiral pattern.
for (var i = 1; i < w; i++){
for (var j = 0; j < 2; j++){
for (var k = 0; k < i; k++){
p[j] += direction;
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
this.map[p[0] + w*p[1]] != 0){
if (quick || this.countConnected(p, 10) >= 10){
return p;
}
}
if (limitDistance && count > 40){
return undefined;
}
count += 1;
}
}
direction *= -1;
}
return undefined;
};
// Counts how many accessible tiles there are connected to the start Point. If there are >= maxCount then it stops.
// This is inefficient for large areas so maxCount should be kept small for efficiency.
TerrainAnalysis.prototype.countConnected = function(startPoint, maxCount, curCount, checked){
curCount = curCount || 0;
checked = checked || [];
var w = this.width;
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
curCount += 1; // add 1 for the current point
checked.push(startPoint);
if (curCount >= maxCount){
return curCount;
}
for (var i in positions){
var p = [startPoint[0] + positions[i][0], startPoint[1] + positions[i][1]];
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
this.map[p[0] + w*p[1]] != 0 && !(p in checked)){
curCount += this.countConnected(p, maxCount, curCount, checked);
}
}
return curCount;
};
/*
* PathFinder inherits from TerrainAnalysis
*
* Used to create a list of distinct paths between two points.
*
* Currently it works with a basic implementation which should be improved.
*
* TODO: Make this use territories.
*/
function PathFinder(gameState){
this.TerrainAnalysis(gameState);
}
copyPrototype(PathFinder, TerrainAnalysis);
/*
* Returns a list of distinct paths to the destination. Currently paths are distinct if they are more than
* blockRadius apart at a distance of blockPlacementRadius from the destination. Where blockRadius and
* blockPlacementRadius are defined in walkGradient
*/
PathFinder.prototype.getPaths = function(start, end, mode){
var s = this.findClosestPassablePoint(this.gamePosToMapPos(start));
var e = this.findClosestPassablePoint(this.gamePosToMapPos(end));
if (!s || !e){
return undefined;
}
var paths = [];
+ var i = 0;
while (true){
+ i++;
+ //this.dumpIm("terrainanalysis_"+i+".png", 511);
this.makeGradient(s,e);
var curPath = this.walkGradient(e, mode);
if (curPath !== undefined){
paths.push(curPath);
}else{
break;
}
this.wipeGradient();
}
//this.dumpIm("terrainanalysis.png", 511);
if (paths.length > 0){
return paths;
}else{
- return undefined;
+ return [];
}
};
// Creates a potential gradient with the start point having the lowest potential
PathFinder.prototype.makeGradient = function(start, end){
var w = this.width;
var map = this.map;
// Holds the list of current points to work outwards from
var stack = [];
// We store the next level in its own stack
var newStack = [];
// Relative positions or new cells from the current one. We alternate between the adjacent 4 and 8 cells
// so that there is an average 1.5 distance for diagonals which is close to the actual sqrt(2) ~ 1.41
var positions = [[[0,1], [0,-1], [1,0], [-1,0]],
[[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]]];
//Set the distance of the start point to be 1 to distinguish it from the impassable areas
map[start[0] + w*(start[1])] = 1;
stack.push(start);
// while there are new points being added to the stack
while (stack.length > 0){
//run through the current stack
while (stack.length > 0){
var cur = stack.pop();
// stop when we reach the end point
if (cur[0] == end[0] && cur[1] == end[1]){
return;
}
var dist = map[cur[0] + w*(cur[1])] + 1;
// Check the positions adjacent to the current cell
for (var i = 0; i < positions[dist % 2].length; i++){
var pos = positions[dist % 2][i];
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
if (cell >= 0 && cell < this.length && map[cell] > dist){
map[cell] = dist;
newStack.push([cur[0]+pos[0], cur[1]+pos[1]]);
}
}
}
// Replace the old empty stack with the newly filled one.
stack = newStack;
newStack = [];
}
};
// Clears the map to just have the obstructions marked on it.
PathFinder.prototype.wipeGradient = function(){
for (var i = 0; i < this.length; i++){
if (this.map[i] > 0){
this.map[i] = 65535;
}
}
};
// Returns the path down a gradient from the start to the bottom of the gradient, returns a point for every 20 cells in normal mode
// in entryPoints mode this returns the point where the path enters the region near the destination, currently defined
// by blockPlacementRadius. Note doesn't return a path when the destination is within the blockpoint radius.
PathFinder.prototype.walkGradient = function(start, mode){
var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]];
var path = [[start[0]*this.cellSize, start[1]*this.cellSize]];
var blockPoint = undefined;
var blockPlacementRadius = 45;
var blockRadius = 23;
var count = 0;
var cur = start;
var w = this.width;
var dist = this.map[cur[0] + w*cur[1]];
var moved = false;
while (this.map[cur[0] + w*cur[1]] !== 0){
for (var i = 0; i < positions.length; i++){
var pos = positions[i];
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
if (cell >= 0 && cell < this.length && this.map[cell] > 0 && this.map[cell] < dist){
dist = this.map[cell];
cur = [cur[0]+pos[0], cur[1]+pos[1]];
moved = true;
count++;
// Mark the point to put an obstruction at before calculating the next path
if (count === blockPlacementRadius){
blockPoint = cur;
}
// Add waypoints to the path, fairly well spaced apart.
if (count % 40 === 0){
path.unshift([cur[0]*this.cellSize, cur[1]*this.cellSize]);
}
break;
}
}
if (!moved){
break;
}
moved = false;
}
if (blockPoint === undefined){
return undefined;
}
// Add an obstruction to the map at the blockpoint so the next path will take a different route.
this.addInfluence(blockPoint[0], blockPoint[1], blockRadius, -1000000, 'constant');
if (mode === 'entryPoints'){
// returns the point where the path enters the blockPlacementRadius
return [blockPoint[0] * this.cellSize, blockPoint[1] * this.cellSize];
}else{
// return a path of points 20 squares apart on the route
return path;
}
};
// Would be used to calculate the width of a chokepoint
// NOTE: Doesn't currently work.
PathFinder.prototype.countAttached = function(pos){
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
var w = this.width;
var val = this.map[pos[0] + w*pos[1]];
var stack = [pos];
var used = {};
while (stack.length > 0){
var cur = stack.pop();
used[cur[0] + " " + cur[1]] = true;
for (var i = 0; i < positions.length; i++){
var p = positions[i];
var cell = cur[0]+p[0] + w*(cur[1]+p[1]);
}
}
};
/*
* Accessibility inherits from TerrainAnalysis
*
* Determines whether there is a path from one point to another. It is initialised with a single point (p1) and then
* can efficiently determine if another point is reachable from p1. Initialising the object is costly so it should be
* cached.
*/
function Accessibility(gameState, location){
this.TerrainAnalysis(gameState);
var start = this.findClosestPassablePoint(this.gamePosToMapPos(location));
// Check that the accessible region is a decent size, otherwise obstacles close to the start point can create
// tiny accessible areas which makes the rest of the map inaceesible.
var iterations = 0;
while (this.floodFill(start) < 20 && iterations < 30){
this.map[start[0] + this.width*(start[1])] = 0;
start = this.findClosestPassablePoint(this.gamePosToMapPos(location));
iterations += 1;
}
-
+ //this.dumpIm("accessibility.png");
}
copyPrototype(Accessibility, TerrainAnalysis);
// Return true if the given point is accessible from the point given when initialising the Accessibility object. #
// If the given point is impassable the closest passable point is used.
Accessibility.prototype.isAccessible = function(position){
var s = this.findClosestPassablePoint(this.gamePosToMapPos(position), true, true);
if (!s)
return false;
return this.map[s[0] + this.width * s[1]] === 1;
};
// fill all of the accessible areas with value 1
Accessibility.prototype.floodFill = function(start){
var w = this.width;
var map = this.map;
// Holds the list of current points to work outwards from
var stack = [];
// We store new points to be added to the stack temporarily in here while we run through the current stack
var newStack = [];
// Relative positions or new cells from the current one.
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
// Set the start point to be accessible
map[start[0] + w*(start[1])] = 1;
stack.push(start);
var count = 0;
// while there are new points being added to the stack
while (stack.length > 0){
//run through the current stack
while (stack.length > 0){
var cur = stack.pop();
// Check the positions adjacent to the current cell
for (var i = 0; i < positions.length; i++){
var pos = positions[i];
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
if (cell >= 0 && cell < this.length && map[cell] > 1){
map[cell] = 1;
newStack.push([cur[0]+pos[0], cur[1]+pos[1]]);
count += 1;
}
}
}
// Replace the old empty stack with the newly filled one.
stack = newStack;
newStack = [];
}
return count;
-};
\ No newline at end of file
+};
+
+
+
+
+// Some different take on the idea of Quantumstate... What I'll do is make a list of any terrain obstruction...
+
+function aStarPath(gameState, onWater){
+ var self = this;
+
+ this.passabilityMap = gameState.getMap();
+
+ var obstructionMaskLand = gameState.getPassabilityClassMask("default");
+ var obstructionMaskWater = gameState.getPassabilityClassMask("ship");
+
+ var obstructionTiles = new Uint16Array(this.passabilityMap.data.length);
+ for (var i = 0; i < this.passabilityMap.data.length; ++i)
+ {
+ if (onWater) {
+ obstructionTiles[i] = (this.passabilityMap.data[i] & obstructionMaskWater) ? 0 : 255;
+ } else {
+ obstructionTiles[i] = (this.passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255;
+ // We allow water, but we set it at a different index.
+ if (!(this.passabilityMap.data[i] & obstructionMaskWater) && obstructionTiles[i] === 0)
+ obstructionTiles[i] = 200;
+ }
+ }
+ if (onWater)
+ this.onWater = true;
+ else
+ this.onWater = false;
+ this.pathRequiresWater = this.onWater;
+
+ this.cellSize = gameState.cellSize;
+
+ this.Map(gameState, obstructionTiles);
+ this.passabilityMap = new Map(gameState, obstructionTiles, true);
+
+ var type = ["wood","stone", "metal"];
+ if (onWater) // trees can perhaps be put into water, I'd doubt so about the rest.
+ type = ["wood"];
+ for (o in type) {
+ var entities = gameState.getResourceSupplies(type[o]);
+ entities.forEach(function (supply) { //}){
+ var radius = Math.floor(supply.obstructionRadius() / self.cellSize);
+ if (type[o] === "wood") {
+ for (var xx = -1; xx <= 1;xx++)
+ for (var yy = -1; yy <= 1;yy++)
+ {
+ var x = self.gamePosToMapPos(supply.position())[0];
+ var y = self.gamePosToMapPos(supply.position())[1];
+ if (x+xx >= 0 && x+xx < self.width && y+yy >= 0 && y+yy < self.height)
+ {
+ self.passabilityMap.map[x+xx + (y+yy)*self.width] = 100; // tree
+ }
+ }
+ self.map[x + y*self.width] = 0;
+ self.passabilityMap.map[x + y*self.width] = 0;
+ } else {
+ for (var xx = -radius; xx <= radius;xx++)
+ for (var yy = -radius; yy <= radius;yy++)
+ {
+ var x = self.gamePosToMapPos(supply.position())[0];
+ var y = self.gamePosToMapPos(supply.position())[1];
+ if (x+xx >= 0 && x+xx < self.width && y+yy >= 0 && y+yy < self.height)
+ {
+ self.map[x+xx + (y+yy)*self.width] = 0;
+ self.passabilityMap.map[x+xx + (y+yy)*self.width] = 0;
+ }
+ }
+ }
+ });
+ }
+ //this.dumpIm("Non-Expanded Obstructions.png",255);
+ this.expandInfluences();
+ //this.dumpIm("Expanded Obstructions.png",10);
+ //this.BluringRadius = 10;
+ //this.Blur(this.BluringRadius); // first steop of bluring
+}
+copyPrototype(aStarPath, TerrainAnalysis);
+
+aStarPath.prototype.getPath = function(start,end,optimized, minSampling, iterationLimit , gamestate)
+{
+ if (minSampling === undefined)
+ this.minSampling = 2;
+ else this.minSampling = minSampling;
+
+ if (start[0] < 0 || this.gamePosToMapPos(start)[0] >= this.width || start[1] < 0 || this.gamePosToMapPos(start)[1] >= this.height)
+ return undefined;
+
+ var s = this.findClosestPassablePoint(this.gamePosToMapPos(start));
+ var e = this.findClosestPassablePoint(this.gamePosToMapPos(end));
+
+ if (!s || !e){
+ return undefined;
+ }
+
+ var w = this.width;
+ var h = this.height;
+
+ this.optimized = optimized;
+ if (this.minSampling < 1)
+ this.minSampling = 1;
+
+ if (gamestate !== undefined)
+ {
+ this.TotorMap = new Map(gamestate);
+ this.TotorMap.addInfluence(s[0],s[1],1,200,'constant');
+ this.TotorMap.addInfluence(e[0],e[1],1,200,'constant');
+ }
+ this.iterationLimit = 65500;
+ if (iterationLimit !== undefined)
+ this.iterationLimit = iterationLimit;
+
+ this.s = s[0] + w*s[1];
+ this.e = e[0] + w*e[1];
+
+ // I was using incredibly slow associative arrays before…
+ this.openList = [];
+ this.parentSquare = new Uint32Array(this.map.length);
+ this.isOpened = new Boolean(this.map.length);
+ this.fCostArray = new Uint32Array(this.map.length);
+ this.gCostArray = new Uint32Array(this.map.length);
+ this.currentSquare = this.s;
+
+ this.totalIteration = 0;
+
+ this.isOpened[this.s] = true;
+ this.openList.push(this.s);
+ this.fCostArray[this.s] = SquareVectorDistance([this.s%w, Math.floor(this.s/w)], [this.e%w, Math.floor(this.e/w)]);
+ this.gCostArray[this.s] = 0;
+ this.parentSquare[this.s] = this.s;
+ //debug ("Initialized okay");
+ return this.continuePath(gamestate);
+
+}
+// in case it's not over yet, this can carry on the calculation of a path over multiple turn until it's over
+aStarPath.prototype.continuePath = function(gamestate)
+{
+ var w = this.width;
+ var h = this.height;
+ var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]];
+ var cost = [100,100,100,100,150,150,150,150];
+ var invCost = [1,1,1,1,0.8,0.8,0.8,0.8];
+ //creation of variables used in the loop
+ var found = false;
+ var nouveau = false;
+ var shortcut = false;
+ var Sampling = this.minSampling;
+ var closeToEnd = false;
+ var infinity = Math.min();
+ var currentDist = infinity;
+ var e = this.e;
+ var s = this.s;
+
+ var iteration = 0;
+ // on to A*
+ while (found === false && this.openList.length !== 0 && iteration < this.iterationLimit){
+ currentDist = infinity;
+
+ if (shortcut === true) {
+ this.currentSquare = this.openList.shift();
+ } else {
+ for (i in this.openList)
+ {
+ var sum = this.fCostArray[this.openList[i]] + this.gCostArray[this.openList[i]];
+ if (sum < currentDist)
+ {
+ this.currentSquare = this.openList[i];
+ currentDist = sum;
+ }
+ }
+ this.openList.splice(this.openList.indexOf(this.currentSquare),1);
+ }
+ if (!this.onWater && this.passabilityMap.map[this.currentSquare] === 200) {
+ this.onWater = true;
+ this.pathRequiresWater = true;
+ } else if (this.onWater && this.passabilityMap.map[this.currentSquare] !== 200)
+ this.onWater = false;
+
+ shortcut = false;
+ this.isOpened[this.currentSquare] = false;
+
+ // optimizaiton: can make huge jumps if I know there's nothing in the way
+ Sampling = this.minSampling;
+ if (this.optimized === true) {
+ Sampling = Math.floor( (+this.map[this.currentSquare]-this.minSampling)/Sampling )*Sampling;
+ if (Sampling < this.minSampling)
+ Sampling = this.minSampling;
+ }
+ /*
+ var diagSampling = Math.floor(Sampling / 1.5);
+ if (diagSampling < this.minSampling)
+ diagSampling = this.minSampling;
+ */
+ var target = [this.e%w, Math.floor(this.e/w)];
+ closeToEnd = false;
+ if (SquareVectorDistance([this.currentSquare%w, Math.floor(this.currentSquare/w)], target) <= Sampling*Sampling)
+ {
+ closeToEnd = true;
+ Sampling = 1;
+ }
+ if (gamestate !== undefined)
+ this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,40,'constant');
+
+ for (i in positions)
+ {
+ //var hereSampling = cost[i] == 1 ? Sampling : diagSampling;
+ var index = 0 + this.currentSquare +positions[i][0]*Sampling +w*Sampling*positions[i][1];
+ if (this.map[index] >= Sampling)
+ {
+ if(this.isOpened[index] === undefined)
+ {
+ this.parentSquare[index] = this.currentSquare;
+
+ this.fCostArray[index] = SquareVectorDistance([index%w, Math.floor(index/w)], target);// * cost[i];
+ this.gCostArray[index] = this.gCostArray[this.currentSquare] + cost[i] * Sampling - this.map[index];
+
+ if (!this.onWater && this.passabilityMap.map[index] === 200) {
+ this.gCostArray[index] += this.fCostArray[index]*2;
+ } else if (this.onWater && this.passabilityMap.map[index] !== 200) {
+ this.gCostArray[index] += this.fCostArray[index]*2;
+ } else if (!this.onWater && this.passabilityMap.map[index] === 100) {
+ this.gCostArray[index] += 100;
+ }
+
+ if (this.openList[0] !== undefined && this.fCostArray[this.openList[0]] + this.gCostArray[this.openList[0]] > this.fCostArray[index] + this.gCostArray[index])
+ {
+ this.openList.unshift(index);
+ shortcut = true;
+ } else {
+ this.openList.push(index);
+ }
+ this.isOpened[index] = true;
+ if (closeToEnd === true && (index === e || index - 1 === e || index + 1 === e || index - w === e || index + w === e
+ || index + 1 + w === e || index + 1 - w === e || index - 1 + w === e|| index - 1 - w === e)) {
+ this.parentSquare[this.e] = this.currentSquare;
+ found = true;
+ break;
+ }
+ } else {
+ var addCost = 0;
+ if (!this.onWater && this.passabilityMap.map[index] === 200) {
+ addCost = this.fCostArray[index]*2;
+ } else if (this.onWater && this.passabilityMap.map[index] !== 200) {
+ addCost = this.fCostArray[index]*2;
+ } else if (!this.onWater && this.passabilityMap.map[index] === 100) {
+ addCost += 100;
+ }
+ addCost -= this.map[index];
+ // already on the Open or closed list
+ if (this.gCostArray[index] > cost[i] * Sampling + addCost + this.gCostArray[this.currentSquare])
+ {
+ this.parentSquare[index] = this.currentSquare;
+ this.gCostArray[index] = cost[i] * Sampling + addCost + this.gCostArray[this.currentSquare];
+ }
+ }
+ }
+ }
+ iteration++;
+ }
+ this.totalIteration += iteration;
+ if (iteration === this.iterationLimit && found === false && this.openList.length !== 0)
+ {
+
+ // we've got to assume that we stopped because we reached the upper limit of iterations
+ return "toBeContinued";
+ }
+
+ //debug (this.totalIteration);
+ var paths = [];
+ if (found) {
+ this.currentSquare = e;
+ var lastPos = [0,0];
+ while (this.parentSquare[this.currentSquare] !== s)
+ {
+ this.currentSquare = this.parentSquare[this.currentSquare];
+ if (gamestate !== undefined)
+ this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,50,'constant');
+ if (SquareVectorDistance(lastPos,[this.currentSquare % w, Math.floor(this.currentSquare / w)]) > 300)
+ {
+ lastPos = [ (this.currentSquare % w) * this.cellSize, Math.floor(this.currentSquare / w) * this.cellSize];
+ paths.push(lastPos);
+ if (gamestate !== undefined)
+ this.TotorMap.addInfluence(this.currentSquare % w, Math.floor(this.currentSquare / w),1,100,'constant');
+ }
+ }
+ }
+
+ if (gamestate !== undefined)
+ this.TotorMap.dumpIm("Path From " +s +" to " +e +".png",255);
+
+ if (paths.length > 0) {
+ return [paths, this.pathRequiresWater];
+ } else {
+ return undefined;
+ }
+
+}
+
+/**
+ * Make each cell's 8-bit value at least one greater than each of its
+ * neighbours' values. (If the grid is initialised with 0s and things high enough (> 100 on most maps), the
+ * result of each cell is its Manhattan distance to the nearest 0.)
+ */
+aStarPath.prototype.expandInfluences = function() {
+ var w = this.width;
+ var h = this.height;
+ var grid = this.map;
+ for ( var y = 0; y < h; ++y) {
+ var min = 8;
+ for ( var x = 0; x < w; ++x) {
+ var g = grid[x + y * w];
+ if (g > min)
+ grid[x + y * w] = min;
+ else if (g < min)
+ min = g;
+ ++min;
+ if (min > 8)
+ min = 8;
+ }
+
+ for ( var x = w - 2; x >= 0; --x) {
+ var g = grid[x + y * w];
+ if (g > min)
+ grid[x + y * w] = min;
+ else if (g < min)
+ min = g;
+ ++min;
+ if (min > 8)
+ min = 8;
+ }
+ }
+
+ for ( var x = 0; x < w; ++x) {
+ var min = 8;
+ for ( var y = 0; y < h; ++y) {
+ var g = grid[x + y * w];
+ if (g > min)
+ grid[x + y * w] = min;
+ else if (g < min)
+ min = g;
+ ++min;
+ if (min > 8)
+ min = 8;
+ }
+
+ for ( var y = h - 2; y >= 0; --y) {
+ var g = grid[x + y * w];
+ if (g > min)
+ grid[x + y * w] = min;
+ else if (g < min)
+ min = g;
+ ++min;
+ if (min > 8)
+ min = 8;
+ }
+ }
+};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/defence.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/defence.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/defence.js (revision 12343)
@@ -1,483 +1,536 @@
// directly imported from Marilyn, with slight modifications to work with qBot.
function Defence(){
this.defenceRatio = 1.8; // How many defenders we want per attacker. Need to balance fewer losses vs. lost economy
// note: the choice should be a no-brainer most of the time: better deflect the attack.
this.totalAttackNb = 0; // used for attack IDs
this.attacks = [];
this.toKill = [];
// keeps a list of targeted enemy at instant T
this.attackerCache = {};
this.listOfEnemies = {};
this.listedEnemyCollection = null; // entity collection of this.listOfEnemies
// boolean 0/1 that's for optimization
this.attackerCacheLoopIndicator = 0;
// this is a list of units to kill. They should be gaia animals, or lonely units. Works the same as listOfEnemies, ie an entityColelction which I'll have to cleanup
this.listOfWantedUnits = {};
this.WantedUnitsAttacker = {}; // same as attackerCache.
this.defenders = null;
this.idleDefs = null;
}
// DO NOTE: the Defence manager, when it calls for Defence, makes the military manager go into "Defence mode"... This makes it not update any plan that's started or not.
// This allows the Defence manager to take units from the plans for Defence.
// Defcon levels
-// 6 (or more): no danger whatsoever detected
-// 5: local zones of danger (ie a tower somewhere, things like that)
-// 4: a few enemy units inbound, like a scout or something. (local danger). Usually seen as the last level before a true "attack"
-// 3: reasonnably sized enemy army inbound, local danger
-// 2: well sized enemy army inbound, general danger
-// 1: Sizable enemy army inside of my base, general danger.
+// 5: no danger whatsoever detected
+// 4: a few enemy units are being dealt with, but nothing too dangerous.
+// 3: A reasonnably sized enemy army is being dealt with, but it should not be a problem.
+// 2: A big enemy army is in the base, but we are not outnumbered
+// 1: Huge army in the base, outnumbering us.
Defence.prototype.update = function(gameState, events, militaryManager){
Engine.ProfileStart("Defence Manager");
// a litlle cache-ing
if (!this.idleDefs) {
var filter = Filters.and(Filters.byMetadata("role", "defence"), Filters.isIdle());
this.idleDefs = gameState.getOwnEntities().filter(filter);
this.idleDefs.registerUpdates();
}
if (!this.defenders) {
var filter = Filters.byMetadata("role", "defence");
this.defenders = gameState.getOwnEntities().filter(filter);
this.defenders.registerUpdates();
}
if (!this.listedEnemyCollection) {
var filter = Filters.byMetadata("listed-enemy", true);
this.listedEnemyCollection = gameState.getEnemyEntities().filter(filter);
this.listedEnemyCollection.registerUpdates();
}
this.myBuildings = gameState.getOwnEntities().filter(Filters.byClass("Structure")).toEntityArray();
this.myUnits = gameState.getOwnEntities().filter(Filters.byClass("Unit"));
this.territoryMap = Map.createTerritoryMap(gameState); // used by many func
// First step: we deal with enemy armies, those are the highest priority.
this.defendFromEnemyArmies(gameState, events, militaryManager);
// second step: we loop through messages, and sort things as needed (dangerous buildings, attack by animals, ships, lone units, whatever).
// TODO
this.MessageProcess(gameState,events,militaryManager);
this.DealWithWantedUnits(gameState,events,militaryManager);
// putting unneeded units at rest
this.idleDefs.forEach(function(ent) {
ent.setMetadata("role", ent.getMetadata("formerrole") );
ent.setMetadata("subrole", undefined);
});
Engine.ProfileStop();
return;
};
// returns armies that are still seen as dangerous (in the LOS of any of my buildings for now)
Defence.prototype.reevaluateDangerousArmies = function(gameState, armies) {
var stillDangerousArmies = {};
for (i in armies) {
var pos = armies[i].getCentrePosition();
if (armies[i].getCentrePosition() && +this.territoryMap.point(armies[i].getCentrePosition()) - 64 === +gameState.player) {
stillDangerousArmies[i] = armies[i];
continue;
}
for (o in this.myBuildings) {
// if the armies out of my buildings LOS (with a little more, because we're cheating right now and big armies could go undetected)
if (inRange(pos, this.myBuildings[o].position(),this.myBuildings[o].visionRange()*this.myBuildings[o].visionRange() + 2500)) {
stillDangerousArmies[i] = armies[i];
break;
}
}
}
return stillDangerousArmies;
}
// returns armies we now see as dangerous, ie in my territory
Defence.prototype.evaluateArmies = function(gameState, armies) {
var DangerousArmies = {};
for (i in armies) {
if (armies[i].getCentrePosition() && +this.territoryMap.point(armies[i].getCentrePosition()) - 64 === +gameState.player) {
DangerousArmies[i] = armies[i];
}
}
return DangerousArmies;
}
// This deals with incoming enemy armies, setting the defcon if needed. It will take new soldiers, and assign them to attack
// it's still a fair share of dumb, so TODO improve
Defence.prototype.defendFromEnemyArmies = function(gameState, events, militaryManager) {
// The enemy Watchers keep a list of armies. This class here tells them if an army is dangerous, and they manage the merging/splitting/disbanding.
// With this system, we can get any dangerous armies. Thus, we can know where the danger is, and react.
// So Defence deals with attacks from animals too (which aren't watched).
// The attackrs here are dealt with on a per unit basis.
// We keep a list of idle defenders. For any new attacker, we'll check if we have any idle defender available, and if not, we assign available units.
// At the end of each turn, if we still have idle defenders, we either assign them to neighboring units, or we release them.
var dangerArmies = {};
this.enemyUnits = {};
// for now armies are never seen as "no longer dangerous"... TODO
for (enemyID in militaryManager.enemyWatchers) {
this.enemyUnits[enemyID] = militaryManager.enemyWatchers[enemyID].getAllEnemySoldiers();
var dangerousArmies = militaryManager.enemyWatchers[enemyID].getDangerousArmies();
// we check if all the dangerous armies are still dangerous.
var newDangerArmies = this.reevaluateDangerousArmies(gameState,dangerousArmies);
var safeArmies = militaryManager.enemyWatchers[enemyID].getSafeArmies();
// we check not dangerous armies, to see if they suddenly became dangerous
var unsafeArmies = this.evaluateArmies(gameState,safeArmies);
for (i in unsafeArmies)
newDangerArmies[i] = unsafeArmies[i];
// and any dangerous armies we push in "dangerArmies"
militaryManager.enemyWatchers[enemyID].resetDangerousArmies();
for (o in newDangerArmies)
militaryManager.enemyWatchers[enemyID].setAsDangerous(o);
for (i in newDangerArmies)
dangerArmies[i] = newDangerArmies[i];
}
var self = this;
var nbOfAttackers = 0;
+ var newEnemies = [];
// clean up before adding new units (slight speeding up, since new units can't already be dead)
for (i in this.listOfEnemies) {
if (this.listOfEnemies[i].length === 0) {
// if we had defined the attackerCache, ie if we had tried to attack this unit.
if (this.attackerCache[i] !== undefined) {
this.attackerCache[i].forEach(function(ent) { ent.stopMoving(); });
delete this.attackerCache[i];
}
delete this.listOfEnemies[i];
} else {
var unit = this.listOfEnemies[i].toEntityArray()[0];
var enemyWatcher = militaryManager.enemyWatchers[unit.owner()];
if (enemyWatcher.isPartOfDangerousArmy(unit.id())) {
nbOfAttackers++;
+ if (this.attackerCache[unit.id()].length == 0) {
+ newEnemies.push(unit);
+ }
} else {
// if we had defined the attackerCache, ie if we had tried to attack this unit.
if (this.attackerCache[unit.id()] != undefined) {
this.attackerCache[unit.id()].forEach(function(ent) { ent.stopMoving(); });
delete this.attackerCache[unit.id()];
}
this.listOfEnemies[unit.id()].toEntityArray()[0].setMetadata("listed-enemy",undefined);
delete this.listOfEnemies[unit.id()];
}
}
}
-
// okay so now, for every dangerous armies, we loop.
for (armyID in dangerArmies) {
// looping through army units
dangerArmies[armyID].forEach(function(ent) {
// do we have already registered an entityCollection for it?
if (self.listOfEnemies[ent.id()] === undefined) {
// no, we register a new entity collection in listOfEnemies, listing exactly one unit as long as it remains alive and owned by my enemy.
// can't be bothered to recode everything
var owner = ent.owner();
var filter = Filters.and(Filters.byOwner(owner),Filters.byID(ent.id()));
self.listOfEnemies[ent.id()] = self.enemyUnits[owner].filter(filter);
self.listOfEnemies[ent.id()].registerUpdates();
self.listOfEnemies[ent.id()].length;
self.listOfEnemies[ent.id()].toEntityArray()[0].setMetadata("listed-enemy",true);
// let's also register an entity collection for units attacking this unit (so we can new if it's attacked)
filter = Filters.and(Filters.byOwner(gameState.player),Filters.byTargetedEntity(ent.id()));
self.attackerCache[ent.id()] = self.myUnits.filter(filter);
self.attackerCache[ent.id()].registerUpdates();
nbOfAttackers++;
+ newEnemies.push(ent);
}
});
}
// Reordering attack because the pathfinder is for now not dynamically updated
for (o in this.attackerCache) {
if ((this.attackerCacheLoopIndicator + o) % 2 === 0) {
this.attackerCache[o].forEach(function (ent) {
ent.attack(+o);
});
}
}
this.attackerCacheLoopIndicator++;
this.attackerCacheLoopIndicator = this.attackerCacheLoopIndicator % 2;
if (nbOfAttackers === 0) {
militaryManager.unpauseAllPlans(gameState);
return;
}
+
// If I'm here, I have a list of enemy units, and a list of my units attacking it (in absolute terms, I could use a list of any unit attacking it).
// now I'll list my idle defenders, then my idle soldiers that could defend.
// and then I'll assign my units.
// and then rock on.
- /*
- if (nbOfAttackers === 0) {
- return;
- } else if (nbOfAttackers < 5){
- gameState.upDefcon(4); // few local units
- } else if (nbOfAttackers >= 5){
- gameState.upDefcon(3); // local attack, dangerous but not hugely threatening for my survival
+
+ if (nbOfAttackers < 10){
+ gameState.setDefcon(4); // few local units
+ } else if (nbOfAttackers >= 10){
+ gameState.setDefcon(3);
}
- if (this.idleDefs.length < nbOfAttackers) {
- gameState.upDefcon(2); // general danger
+ var nonDefenders = this.myUnits.filter(Filters.or( Filters.not(Filters.byMetadata("role","defence")),Filters.isIdle()));
+ nonDefenders = nonDefenders.filter(Filters.not(Filters.byClass("Female")));
+
+ var defenceRatio = this.defenceRatio;
+ if (newEnemies.length * defenceRatio> nonDefenders.length) {
+ defenceRatio = 1;
}
- */
- // todo: improve the logic against attackers.
+ // For each enemy, we'll pick two units.
+ for each (enemy in newEnemies) {
+ if (nonDefenders.length === 0)
+ break;
- // reupdate the existing defenders.
-
- this.idleDefs.forEach(function(ent) {
- ent.setMetadata("subrole","newdefender");
- });
+ var assigned = self.attackerCache[enemy.id()].length;
+ if (assigned >= defenceRatio)
+ return;
-
-
- nbOfAttackers *= this.defenceRatio;
- // Assume those taken care of.
- nbOfAttackers -= +(this.defenders.length);
-
- // need new units?
- if (nbOfAttackers <= 0)
- return;
-
+ // let's check for a counter.
+ //debug ("Enemy is a " + uneval(enemy._template.Identity.Classes._string) );
+ var potCounters = gameState.ai.templateManager.getCountersToClasses(gameState,enemy.classes(),enemy.templateName());
+ //debug ("Counters are" +uneval(potCounters));
+ var counters = [];
+ for (o in potCounters) {
+ var counter = nonDefenders.filter(Filters.and(Filters.byType(potCounters[o][0]), Filters.byStaticDistance(enemy.position(), 150) )).toEntityArray();
+ if (counter.length !== 0)
+ for (unit in counter)
+ counters.push(counter[unit]);
+ }
+ //debug ("I have " +counters.length +"countering units");
+ for (var i = 0; i < defenceRatio && i < counters.length; i++) {
+ if (counters[i].getMetadata("plan") !== undefined)
+ militaryManager.pausePlan(gameState,counters[i].getMetadata("plan"));
+ counters[i].setMetadata("formerrole", counters[i].getMetadata("role"));
+ counters[i].setMetadata("role","defence");
+ counters[i].setMetadata("subrole","defending");
+ counters[i].attack(+enemy.id());
+ nonDefenders.updateEnt(counters[i]);
+ assigned++;
+ //debug ("Sending a " +counters[i].templateName() +" to counter a " + enemy.templateName());
+ }
+ if (assigned !== defenceRatio) {
+ // take closest units
+ nonDefenders.filter(Filters.byClass("CitizenSoldier")).filterNearest(enemy.position(),defenceRatio-assigned).forEach(function (defender) { //}){
+ if (defender.getMetadata("plan") !== undefined)
+ militaryManager.pausePlan(gameState,defender.getMetadata("plan"));
+ defender.setMetadata("formerrole", defender.getMetadata("role"));
+ defender.setMetadata("role","defence");
+ defender.setMetadata("subrole","defending");
+ defender.attack(+enemy.id());
+ nonDefenders.updateEnt(defender);
+ assigned++;
+ });
+ }
+ }
+ /*
// yes. We'll pick new units (pretty randomly for now, todo)
// first from attack plans, then from workers.
var newSoldiers = gameState.getOwnEntities().filter(function (ent) {
- if (ent.getMetadata("plan") != undefined)
+ if (ent.getMetadata("plan") != undefined && ent.getMetadata("role") != "defence")
return true;
return false;
});
newSoldiers.forEach(function(ent) {
if (ent.getMetadata("subrole","attacking")) // gone with the wind to avenge their brothers.
return;
if (nbOfAttackers <= 0)
return;
militaryManager.pausePlan(gameState,ent.getMetadata("plan"));
ent.setMetadata("formerrole", ent.getMetadata("role"));
ent.setMetadata("role","defence");
ent.setMetadata("subrole","newdefender");
nbOfAttackers--;
});
if (nbOfAttackers > 0) {
newSoldiers = gameState.getOwnEntitiesByRole("worker");
newSoldiers.forEach(function(ent) {
if (nbOfAttackers <= 0)
return;
// If we're not female, we attack
// and if we're not already assigned from above (might happen, not sure, rather be cautious)
if (ent.hasClass("CitizenSoldier") && ent.getMetadata("subrole") != "newdefender") {
ent.setMetadata("formerrole", "worker");
ent.setMetadata("role","defence");
ent.setMetadata("subrole","newdefender");
nbOfAttackers--;
}
});
}
// okay
newSoldiers = gameState.getOwnEntitiesByMetadata("subrole","newdefender");
+
+ // we're okay, but there's a big amount of units
+ // todo: check against total number of soldiers
+ if (nbOfAttackers <= 0 && newSoldiers.length > 35)
+ gameState.setDefcon(2);
+ else if (nbOfAttackers > 0) {
+ // we are actually lacking units
+ gameState.setDefcon(1);
+ }
// TODO. For now, each unit will pick the closest unit that is attacked by only one/zero guy, or any if there is none.
// ought to regroup them first for optimization.
newSoldiers.forEach(function(ent) { //}) {
var enemies = self.listedEnemyCollection.filterNearest(ent.position()).toEntityArray();
var target = -1;
var secondaryTarget = enemies[0]; // second best pick
for (o in enemies) {
var enemy = enemies[o];
if (self.attackerCache[enemy.id()].length < 2) {
target = +enemy.id();
break;
}
}
ent.setMetadata("subrole","defending");
ent.attack(+target);
});
-
+*/
return;
}
// this processes the attackmessages
// So that a unit that gets attacked will not be completely dumb.
// warning: huge levels of indentation coming.
Defence.prototype.MessageProcess = function(gameState,events, militaryManager) {
for (var key in events){
var e = events[key];
if (e.type === "Attacked" && e.msg){
if (gameState.isEntityOwn(gameState.getEntityById(e.msg.target))) {
var attacker = gameState.getEntityById(e.msg.attacker);
var ourUnit = gameState.getEntityById(e.msg.target);
// the attacker must not be already dead, and it must not be me (think catapults that miss).
if (attacker !== undefined && attacker.owner() !== gameState.player) {
// note: our unit can already by dead by now... We'll then have to rely on the enemy to react.
// if we're not on enemy territory
var territory = +this.territoryMap.point(attacker.position()) - 64;
+ // we do not consider units that are defenders, and we do not consider units that are part of an attacking attack plan
+ // (attacking attacking plans are dealing with threats on their own).
+ if (ourUnit !== undefined && (ourUnit.getMetadata("role") == "defence" || ourUnit.getMetadata("subrole") == "attacking"))
+ continue;
+
// let's check for animals
if (attacker.owner() == 0) {
// if our unit is still alive, we make it react
// in this case we attack.
if (ourUnit !== undefined) {
if (ourUnit.hasClass("Unit") && !ourUnit.hasClass("Support"))
ourUnit.attack(e.msg.attacker);
else {
ourUnit.flee(attacker);
}
}
// anyway we'll register the animal as dangerous, and attack it.
var filter = Filters.byID(attacker.id());
this.listOfWantedUnits[attacker.id()] = gameState.getEntities().filter(filter);
this.listOfWantedUnits[attacker.id()].registerUpdates();
this.listOfWantedUnits[attacker.id()].length;
filter = Filters.and(Filters.byOwner(gameState.player),Filters.byTargetedEntity(attacker.id()));
this.WantedUnitsAttacker[attacker.id()] = this.myUnits.filter(filter);
this.WantedUnitsAttacker[attacker.id()].registerUpdates();
this.WantedUnitsAttacker[attacker.id()].length;
} else if (territory != attacker.owner()) { // preliminary check: attacks in enemy territory are not counted as attacks
// Also TODO: this does not differentiate with buildings...
// These ought to be treated differently.
// units in attack plans will react independently, but we still list the attacks here.
if (attacker.hasClass("Structure")) {
// todo: we ultimately have to check wether it's a danger point or an isolated area, and if it's a danger point, mark it as so.
} else {
// TODO: right now a soldier always retaliate... Perhaps it should be set in "Defence" mode.
if (!attacker.hasClass("Female") && !attacker.hasClass("Ship")) {
// This unit is dangerous. We'll ask the enemy manager if it's part of a big army, in which case we'll list it as dangerous (so it'll be treated next turn by the other manager)
// If it's not part of a big army, depending on our priority we may want to kill it (using the same things as animals for that)
// TODO (perhaps not any more, but let's mark it anyway)
var army = militaryManager.enemyWatchers[attacker.owner()].getArmyFromMember(attacker.id());
if (army[1].length > 5) {
militaryManager.enemyWatchers[attacker.owner()].setAsDangerous(army[0]);
} else if (!militaryManager.enemyWatchers[attacker.owner()].isDangerous(army[0])) {
// we register this unit as wanted, TODO register the whole army
// another function will deal with it.
var filter = Filters.and(Filters.byOwner(attacker.owner()),Filters.byID(attacker.id()));
this.listOfWantedUnits[attacker.id()] = this.enemyUnits[attacker.owner()].filter(filter);
this.listOfWantedUnits[attacker.id()].registerUpdates();
this.listOfWantedUnits[attacker.id()].length;
filter = Filters.and(Filters.byOwner(gameState.player),Filters.byTargetedEntity(attacker.id()));
this.WantedUnitsAttacker[attacker.id()] = this.myUnits.filter(filter);
this.WantedUnitsAttacker[attacker.id()].registerUpdates();
this.WantedUnitsAttacker[attacker.id()].length;
}
- if (ourUnit && ourUnit.hasClass("Unit") && ourUnit.getMetadata("role") != "attack") {
+ if (ourUnit && ourUnit.hasClass("Unit")) {
if (ourUnit.hasClass("Support")) {
// TODO: it's a villager. Garrison it.
// TODO: make other neighboring villagers garrison
// Right now we'll flee from the attacker.
ourUnit.flee(attacker);
} else {
// It's a soldier. Right now we'll retaliate
// TODO: check for stronger units against this type, check for fleeing options, etc.
ourUnit.attack(e.msg.attacker);
}
}
}
}
}
}
}
}
}
};
-// At most, this will put defcon to 5
+// At most, this will put defcon to 4
Defence.prototype.DealWithWantedUnits = function(gameState, events, militaryManager) {
//if (gameState.defcon() < 3)
// return;
var self = this;
var nbOfAttackers = 0;
var nbOfDealtWith = 0;
// clean up before adding new units (slight speeding up, since new units can't already be dead)
for (i in this.listOfWantedUnits) {
if (this.listOfWantedUnits[i].length === 0 || this.listOfEnemies[i] !== undefined) { // unit died/was converted/is already dealt with as part of an army
delete this.WantedUnitsAttacker[i];
delete this.listOfWantedUnits[i];
} else {
nbOfAttackers++;
if (this.WantedUnitsAttacker[i].length > 0)
nbOfDealtWith++;
}
}
// note: we can deal with units the way we want because anyway, the Army Defender has already done its task.
// If there are still idle defenders here, it's because they aren't needed.
// I can also call other units: they're not needed.
// Note however that if the defcon level is too high, this won't do anything because it's low priority.
// this also won't take units from attack managers
if (nbOfAttackers === 0)
return;
- // at most, we'll deal with two enemies at once.
- if (nbOfDealtWith >= 2)
+ // at most, we'll deal with 3 enemies at once.
+ if (nbOfDealtWith >= 3)
return;
// dynamic properties are not updated nearly fast enough here so a little caching
var addedto = {};
// we send 3 units to each target just to be sure. TODO refine.
// we do not use plan units
this.idleDefs.forEach(function(ent) {
- if (nbOfDealtWith < 2 && nbOfAttackers > 0 && ent.getMetadata("plan") == undefined)
+ if (nbOfDealtWith < 3 && nbOfAttackers > 0 && ent.getMetadata("plan") == undefined)
for (o in self.listOfWantedUnits) {
if ( (addedto[o] == undefined && self.WantedUnitsAttacker[o].length < 3) || (addedto[o] && self.WantedUnitsAttacker[o].length + addedto[o] < 3)) {
if (self.WantedUnitsAttacker[o].length === 0)
nbOfDealtWith++;
+
+ ent.setMetadata("formerrole", ent.getMetadata("role"));
+ ent.setMetadata("role","defence");
ent.setMetadata("subrole", "defending");
ent.attack(+o);
if (addedto[o])
addedto[o]++;
else
addedto[o] = 1;
break;
}
if (self.WantedUnitsAttacker[o].length == 3)
nbOfAttackers--; // we hav eenough units, mark this one as being OKAY
}
});
// still some undealt with attackers, recruit citizen soldiers
if (nbOfAttackers > 0 && nbOfDealtWith < 2) {
- //gameState.upDefcon(5);
+ gameState.setDefcon(4);
var newSoldiers = gameState.getOwnEntitiesByRole("worker");
newSoldiers.forEach(function(ent) {
// If we're not female, we attack
if (ent.hasClass("CitizenSoldier"))
- if (nbOfDealtWith < 2 && nbOfAttackers > 0)
+ if (nbOfDealtWith < 3 && nbOfAttackers > 0)
for (o in self.listOfWantedUnits) {
if ( (addedto[o] == undefined && self.WantedUnitsAttacker[o].length < 3) || (addedto[o] && self.WantedUnitsAttacker[o].length + addedto[o] < 3)) {
if (self.WantedUnitsAttacker[o].length === 0)
nbOfDealtWith++;
+ ent.setMetadata("formerrole", ent.getMetadata("role"));
+ ent.setMetadata("role","defence");
ent.setMetadata("subrole", "defending");
ent.attack(+o);
if (addedto[o])
addedto[o]++;
else
addedto[o] = 1;
break;
}
if (self.WantedUnitsAttacker[o].length == 3)
nbOfAttackers--; // we hav eenough units, mark this one as being OKAY
}
});
}
return;
}
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/military.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/military.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/military.js (revision 12343)
@@ -1,637 +1,635 @@
/*
* Military strategy:
* * Try training an attack squad of a specified size
* * When it's the appropriate size, send it to attack the enemy
* * Repeat forever
*
*/
var MilitaryAttackManager = function() {
- // these use the structure soldiers[unitId] = true|false to register the units
- this.attackManagers = [AttackMoveToLocation];
- this.availableAttacks = [];
- this.currentAttacks = [];
-
- // Counts how many attacks we have sent at the enemy.
- this.attackCount = 0;
- this.lastAttackTime = 0;
-
this.defenceManager = new Defence();
+
this.TotalAttackNumber = 0;
this.upcomingAttacks = { "CityAttack" : [] };
this.startedAttacks = { "CityAttack" : [] };
};
MilitaryAttackManager.prototype.init = function(gameState) {
var civ = gameState.playerData.civ;
// load units and buildings from the config files
if (civ in Config.buildings.moderate){
this.bModerate = Config.buildings.moderate[civ];
}else{
this.bModerate = Config.buildings.moderate['default'];
}
if (civ in Config.buildings.advanced){
this.bAdvanced = Config.buildings.advanced[civ];
}else{
this.bAdvanced = Config.buildings.advanced['default'];
}
if (civ in Config.buildings.fort){
this.bFort = Config.buildings.fort[civ];
}else{
this.bFort = Config.buildings.fort['default'];
}
for (var i in this.bAdvanced){
this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]);
}
for (var i in this.bFort){
this.bFort[i] = gameState.applyCiv(this.bFort[i]);
}
this.getEconomicTargets = function(gameState, militaryManager){
return militaryManager.getEnemyBuildings(gameState, "Economic");
};
// TODO: figure out how to make this generic
for (var i in this.attackManagers){
this.availableAttacks[i] = new this.attackManagers[i](gameState, this);
}
var enemies = gameState.getEnemyEntities();
var filter = Filters.byClassesOr(["CitizenSoldier", "Champion", "Hero", "Siege"]);
this.enemySoldiers = enemies.filter(filter); // TODO: cope with diplomacy changes
this.enemySoldiers.registerUpdates();
// each enemy watchers keeps a list of entity collections about the enemy it watches
// It also keeps track of enemy armies, merging/splitting as needed
this.enemyWatchers = {};
for (var i = 1; i <= 8; i++)
if (gameState.player != i && gameState.isPlayerEnemy(i)) {
this.enemyWatchers[i] = new enemyWatcher(gameState, i);
}
};
/**
* @param (GameState) gameState
* @param (string) soldierTypes
* @returns array of soldiers for which training buildings exist
*/
MilitaryAttackManager.prototype.findTrainableUnits = function(gameState, soldierType){
var allTrainable = [];
gameState.getOwnEntities().forEach(function(ent) {
var trainable = ent.trainableEntities();
for (var i in trainable){
if (allTrainable.indexOf(trainable[i]) === -1){
allTrainable.push(trainable[i]);
}
}
});
var ret = [];
for (var i in allTrainable){
var template = gameState.getTemplate(allTrainable[i]);
if (soldierType == this.getSoldierType(template)){
ret.push(allTrainable[i]);
}
}
return ret;
};
// Returns the type of a soldier, either citizenSoldier, advanced or siege
MilitaryAttackManager.prototype.getSoldierType = function(ent){
if (ent.hasClass("Hero")){
return undefined;
}
if (ent.hasClass("CitizenSoldier") && !ent.hasClass("Cavalry")){
return "citizenSoldier";
}else if (ent.hasClass("Champion") || ent.hasClass("CitizenSoldier")){
return "advanced";
}else if (ent.hasClass("Siege")){
return "siege";
}else{
return undefined;
}
};
/**
* Returns the unit type we should begin training. (Currently this is whatever
* we have least of.)
*/
MilitaryAttackManager.prototype.findBestNewUnit = function(gameState, queue, soldierType) {
var units = this.findTrainableUnits(gameState, soldierType);
// Count each type
var types = [];
for ( var tKey in units) {
var t = units[tKey];
types.push([t, gameState.countEntitiesAndQueuedByType(gameState.applyCiv(t))
+ queue.countAllByType(gameState.applyCiv(t)) ]);
}
// Sort by increasing count
types.sort(function(a, b) {
return a[1] - b[1];
});
if (types.length === 0){
return false;
}
return types[0][0];
};
// picks the best template based on parameters and classes
MilitaryAttackManager.prototype.findBestTrainableUnit = function(gameState, classes, parameters) {
var units = gameState.findTrainableUnits(classes);
+
if (units.length === 0)
return undefined;
+
units.sort(function(a, b) { //}) {
var aDivParam = 0, bDivParam = 0;
var aTopParam = 0, bTopParam = 0;
for (i in parameters) {
var param = parameters[i];
if (param[0] == "base") {
aTopParam = param[1];
bTopParam = param[1];
}
if (param[0] == "strength") {
aTopParam += a[1].getMaxStrength() * param[1];
bTopParam += b[1].getMaxStrength() * param[1];
}
if (param[0] == "speed") {
aTopParam += a[1].walkSpeed() * param[1];
bTopParam += b[1].walkSpeed() * param[1];
}
if (param[0] == "cost") {
aDivParam += a[1].costSum() * param[1];
bDivParam += b[1].costSum() * param[1];
}
}
return -(aTopParam/(aDivParam+1)) + (bTopParam/(bDivParam+1));
});
return units[0][0];
};
MilitaryAttackManager.prototype.registerSoldiers = function(gameState) {
var soldiers = gameState.getOwnEntitiesByRole("soldier");
var self = this;
soldiers.forEach(function(ent) {
ent.setMetadata("role", "military");
ent.setMetadata("military", "unassigned");
});
};
// return count of enemy buildings for a given building class
MilitaryAttackManager.prototype.getEnemyBuildings = function(gameState,cls) {
var targets = gameState.entities.filter(function(ent) {
return (gameState.isEntityEnemy(ent) && ent.hasClass("Structure") && ent.hasClass(cls) && ent.owner() !== 0 && ent.position());
});
return targets;
};
// return n available units and makes these units unavailable
MilitaryAttackManager.prototype.getAvailableUnits = function(n, filter) {
var ret = [];
var count = 0;
var units = undefined;
if (filter){
units = this.getUnassignedUnits().filter(filter);
}else{
units = this.getUnassignedUnits();
}
units.forEach(function(ent){
ret.push(ent.id());
ent.setMetadata("military", "assigned");
ent.setMetadata("role", "military");
count++;
if (count >= n) {
return;
}
});
return ret;
};
// Takes a single unit id, and marks it unassigned
MilitaryAttackManager.prototype.unassignUnit = function(unit){
this.entity(unit).setMetadata("military", "unassigned");
};
// Takes an array of unit id's and marks all of them unassigned
MilitaryAttackManager.prototype.unassignUnits = function(units){
for (var i in units){
this.unassignUnit(units[i]);
}
};
MilitaryAttackManager.prototype.getUnassignedUnits = function(){
return this.gameState.getOwnEntitiesByMetadata("military", "unassigned");
};
MilitaryAttackManager.prototype.countAvailableUnits = function(filter){
var count = 0;
if (filter){
return this.getUnassignedUnits().filter(filter).length;
}else{
return this.getUnassignedUnits().length;
}
};
// Takes an entity id and returns an entity object or undefined if there is no entity with that id
// Also sends a debug message warning if the id has no entity
MilitaryAttackManager.prototype.entity = function(id) {
return this.gameState.getEntityById(id);
};
// Returns the military strength of unit
MilitaryAttackManager.prototype.getUnitStrength = function(ent){
var strength = 0.0;
var attackTypes = ent.attackTypes();
var armourStrength = ent.armourStrengths();
var hp = 2 * ent.hitpoints() / (160 + 1*ent.maxHitpoints()); //100 = typical number of hitpoints
for (var typeKey in attackTypes) {
var type = attackTypes[typeKey];
var attackStrength = ent.attackStrengths(type);
var attackRange = ent.attackRange(type);
var attackTimes = ent.attackTimes(type);
for (var str in attackStrength) {
var val = parseFloat(attackStrength[str]);
switch (str) {
case "crush":
strength += (val * 0.085) / 3;
break;
case "hack":
strength += (val * 0.075) / 3;
break;
case "pierce":
strength += (val * 0.065) / 3;
break;
}
}
if (attackRange){
strength += (attackRange.max * 0.0125) ;
}
for (var str in attackTimes) {
var val = parseFloat(attackTimes[str]);
switch (str){
case "repeat":
strength += (val / 100000);
break;
case "prepare":
strength -= (val / 100000);
break;
}
}
}
for (var str in armourStrength) {
var val = parseFloat(armourStrength[str]);
switch (str) {
case "crush":
strength += (val * 0.085) / 3;
break;
case "hack":
strength += (val * 0.075) / 3;
break;
case "pierce":
strength += (val * 0.065) / 3;
break;
}
}
return strength * hp;
};
// Returns the strength of the available units of ai army
MilitaryAttackManager.prototype.measureAvailableStrength = function(){
var strength = 0.0;
var self = this;
this.getUnassignedUnits(this.gameState).forEach(function(ent){
strength += self.getUnitStrength(ent);
});
return strength;
};
MilitaryAttackManager.prototype.getEnemySoldiers = function(){
return this.enemySoldiers;
};
// Returns the number of units in the largest enemy army
MilitaryAttackManager.prototype.measureEnemyCount = function(gameState){
// Measure enemy units
var isEnemy = gameState.playerData.isEnemy;
var enemyCount = [];
var maxCount = 0;
for ( var i = 1; i < isEnemy.length; i++) {
enemyCount[i] = 0;
}
// Loop through the enemy soldiers and add one to the count for that soldiers player's count
this.enemySoldiers.forEach(function(ent) {
enemyCount[ent.owner()]++;
if (enemyCount[ent.owner()] > maxCount) {
maxCount = enemyCount[ent.owner()];
}
});
return maxCount;
};
// Returns the strength of the largest enemy army
MilitaryAttackManager.prototype.measureEnemyStrength = function(gameState){
// Measure enemy strength
var isEnemy = gameState.playerData.isEnemy;
var enemyStrength = [];
var maxStrength = 0;
var self = this;
for ( var i = 1; i < isEnemy.length; i++) {
enemyStrength[i] = 0;
}
// Loop through the enemy soldiers and add the strength to that soldiers player's total strength
this.enemySoldiers.forEach(function(ent) {
enemyStrength[ent.owner()] += self.getUnitStrength(ent);
if (enemyStrength[ent.owner()] > maxStrength) {
maxStrength = enemyStrength[ent.owner()];
}
});
return maxStrength;
};
// Adds towers to the defenceBuilding queue
MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower'))
+ queues.defenceBuilding.totalLength() < gameState.getBuildLimits()["DefenseTower"]) {
gameState.getOwnEntities().forEach(function(dropsiteEnt) {
if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.getMetadata("defenseTower") !== true){
var position = dropsiteEnt.position();
if (position){
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, 'structures/{civ}_defense_tower', position));
}
dropsiteEnt.setMetadata("defenseTower", true);
}
});
}
var numFortresses = 0;
for (var i in this.bFort){
numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i]));
}
if (numFortresses + queues.defenceBuilding.totalLength() < 1){ //gameState.getBuildLimits()["Fortress"]) {
if (gameState.getTimeElapsed() > 720 * 1000 + numFortresses * 300 * 1000){
if (gameState.ai.pathsToMe && gameState.ai.pathsToMe.length > 0){
var position = gameState.ai.pathsToMe.shift();
// TODO: pick a fort randomly from the list.
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0], position));
}else{
queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0]));
}
}
}
};
MilitaryAttackManager.prototype.constructTrainingBuildings = function(gameState, queues) {
// Build more military buildings
// TODO: make military building better
Engine.ProfileStart("Build buildings");
- if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > 35) {
+ if (gameState.countEntitiesByType(gameState.applyCiv("units/{civ}_support_female_citizen")) > 25) {
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0]))
+ queues.militaryBuilding.totalLength() < 1) {
queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0]));
}
}
//build advanced military buildings
if (gameState.getTimeElapsed() > 720*1000){
if (queues.militaryBuilding.totalLength() === 0){
for (var i in this.bAdvanced){
if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){
queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i]));
}
}
}
}
Engine.ProfileStop();
};
MilitaryAttackManager.prototype.trainMilitaryUnits = function(gameState, queues){
Engine.ProfileStart("Train Units");
// Continually try training new units, in batches of 5
if (queues.citizenSoldier.length() < 6) {
var newUnit = this.findBestNewUnit(gameState, queues.citizenSoldier, "citizenSoldier");
if (newUnit){
queues.citizenSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
"role" : "soldier"
}, 5));
}
}
if (queues.advancedSoldier.length() < 2) {
var newUnit = this.findBestNewUnit(gameState, queues.advancedSoldier, "advanced");
if (newUnit){
queues.advancedSoldier.addItem(new UnitTrainingPlan(gameState, newUnit, {
"role" : "soldier"
}, 5));
}
}
if (queues.siege.length() < 4) {
var newUnit = this.findBestNewUnit(gameState, queues.siege, "siege");
if (newUnit){
queues.siege.addItem(new UnitTrainingPlan(gameState, newUnit, {
"role" : "soldier"
}, 2));
}
}
Engine.ProfileStop();
};
MilitaryAttackManager.prototype.pausePlan = function(gameState, planName) {
for (attackType in this.upcomingAttacks) {
for (i in this.upcomingAttacks[attackType]) {
var attack = this.upcomingAttacks[attackType][i];
if (attack.getName() == planName)
attack.setPaused(gameState, true);
}
}
}
MilitaryAttackManager.prototype.unpausePlan = function(gameState, planName) {
for (attackType in this.upcomingAttacks) {
for (i in this.upcomingAttacks[attackType]) {
var attack = this.upcomingAttacks[attackType][i];
if (attack.getName() == planName)
attack.setPaused(gameState, false);
}
}
}
MilitaryAttackManager.prototype.pauseAllPlans = function(gameState) {
for (attackType in this.upcomingAttacks) {
for (i in this.upcomingAttacks[attackType]) {
var attack = this.upcomingAttacks[attackType][i];
attack.setPaused(gameState, true);
}
}
}
MilitaryAttackManager.prototype.unpauseAllPlans = function(gameState) {
for (attackType in this.upcomingAttacks) {
for (i in this.upcomingAttacks[attackType]) {
var attack = this.upcomingAttacks[attackType][i];
attack.setPaused(gameState, false);
}
}
}
MilitaryAttackManager.prototype.update = function(gameState, queues, events) {
var self = this;
Engine.ProfileStart("military update");
this.gameState = gameState;
//this.registerSoldiers(gameState);
//this.trainMilitaryUnits(gameState, queues);
this.constructTrainingBuildings(gameState, queues);
if(gameState.getTimeElapsed() > 300*1000)
this.buildDefences(gameState, queues);
for (watcher in this.enemyWatchers)
this.enemyWatchers[watcher].detectArmies(gameState,this);
this.defenceManager.update(gameState, events, this);
/*Engine.ProfileStart("Plan new attacks");
// Look for attack plans which can be executed, only do this once every minute
for (var i = 0; i < this.availableAttacks.length; i++){
if (this.availableAttacks[i].canExecute(gameState, this)){
this.availableAttacks[i].execute(gameState, this);
this.currentAttacks.push(this.availableAttacks[i]);
//debug("Attacking!");
}
this.availableAttacks.splice(i, 1, new this.attackManagers[i](gameState, this));
}
Engine.ProfileStop();
Engine.ProfileStart("Update attacks");
// Keep current attacks updated
for (var i in this.currentAttacks){
this.currentAttacks[i].update(gameState, this, events);
}
Engine.ProfileStop();*/
Engine.ProfileStart("Looping through attack plans");
// create plans if I'm at peace. I'm not starting plans if there is a sizable force in my realm (hence defcon 4+)
//if (gameState.defcon() >= 4 && this.canStartAttacks === true) {
//if ((this.preparingNormal) == 0 && this.BuildingInfoManager.getNumberBuiltByRole("Barracks") > 0) {
// this will updats plans. Plans can be updated up to defcon 2, where they'll be paused (TODO)
//if (0 == 1) // remove to activate attacks
//if (gameState.defcon() >= 3) {
if (1) {
for (attackType in this.upcomingAttacks) {
for (i in this.upcomingAttacks[attackType]) {
var attack = this.upcomingAttacks[attackType][i];
- if (!attack.isPaused()) {
- // okay so we'll get the support plan
- if (!attack.isStarted()) {
- if (1) { //gameState.ai.status["underAttack"] == false) {
- var updateStep = attack.updatePreparation(gameState, this,events);
-
- // now we're gonna check if the preparation time is over
- if (updateStep === 1) {
- // just chillin'
- } else if (updateStep === 0) {
- debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" aborted.");
- attack.Abort(gameState, this);
-
- //this.abortedAttacks.push(attack);
-
- this.upcomingAttacks[attackType].splice(i,1);
- i--;
- } else if (updateStep === 2) {
- debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName());
- attack.StartAttack(gameState,this);
- this.startedAttacks[attackType].push(attack);
- this.upcomingAttacks[attackType].splice(i,1);
- i--;
- }
+ // okay so we'll get the support plan
+ if (!attack.isStarted()) {
+ var updateStep = attack.updatePreparation(gameState, this,events);
+
+ // now we're gonna check if the preparation time is over
+ if (updateStep === 1 || attack.isPaused() ) {
+ // just chillin'
+ } else if (updateStep === 0 || updateStep === 3) {
+ debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" aborted.");
+ if (updateStep === 3) {
+ this.attackPlansEncounteredWater = true;
+ debug("I dare not wet my feet");
}
- } else {
+ attack.Abort(gameState, this);
+ //this.abortedAttacks.push(attack);
+
+ i--;
+ this.upcomingAttacks[attackType].splice(i,1);
+ } else if (updateStep === 2) {
debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName());
+ attack.StartAttack(gameState,this);
this.startedAttacks[attackType].push(attack);
- this.upcomingAttacks[attackType].splice(i,1);
i--;
+ this.upcomingAttacks[attackType].splice(i-1,1);
}
+ } else {
+ debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName());
+ this.startedAttacks[attackType].push(attack);
+ i--;
+ this.upcomingAttacks[attackType].splice(i-1,1);
}
}
}
//if (this.abortedAttacks.length !== 0)
// this.abortedAttacks[gameState.ai.mainCounter % this.abortedAttacks.length].releaseAnyUnit(gameState);
}
for (attackType in this.startedAttacks) {
for (i in this.startedAttacks[attackType]) {
var attack = this.startedAttacks[attackType][i];
// okay so then we'll update the raid.
var remaining = attack.update(gameState,this,events);
if (remaining == 0 || remaining == undefined) {
debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" is now finished.");
attack.Abort(gameState);
//this.abortedAttacks.push(attack);
this.startedAttacks[attackType].splice(i,1);
i--;
}
}
}
// creating plans after updating because an aborted plan might be reused in that case.
- if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0])) >= 1) {
- if (this.upcomingAttacks["CityAttack"].length == 0) {
+ if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0])) >= 1 && !this.attackPlansEncounteredWater) {
+ if (this.upcomingAttacks["CityAttack"].length == 0 && gameState.getTimeElapsed() < 25*60000) {
var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1);
debug ("Military Manager: Creating the plan " +this.TotalAttackNumber);
this.TotalAttackNumber++;
this.upcomingAttacks["CityAttack"].push(Lalala);
+ } else if (this.upcomingAttacks["CityAttack"].length == 0) {
+ var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1, "superSized");
+ debug ("Military Manager: Creating the super sized plan " +this.TotalAttackNumber);
+ this.TotalAttackNumber++;
+ this.upcomingAttacks["CityAttack"].push(Lalala);
}
}
/*
if (this.HarassRaiding && this.preparingRaidNumber + this.startedRaidNumber < 1 && gameState.getTimeElapsed() < 780000) {
var Lalala = new CityAttack(gameState, this,this.totalStartedAttackNumber, -1, "harass_raid");
if (!Lalala.createSupportPlans(gameState, this, queues.advancedSoldier)) {
debug ("Military Manager: harrassing plan not a valid option");
this.HarassRaiding = false;
} else {
debug ("Military Manager: Creating the harass raid plan " +this.totalStartedAttackNumber);
this.totalStartedAttackNumber++;
this.preparingRaidNumber++;
this.currentAttacks.push(Lalala);
}
}
*/
Engine.ProfileStop();
/*Engine.ProfileStart("Use idle military as workers");
// Set unassigned to be workers TODO: fix this so it doesn't scan all units every time
this.getUnassignedUnits(gameState).forEach(function(ent){
if (self.getSoldierType(ent) === "citizenSoldier"){
ent.setMetadata("role", "worker");
}
});
Engine.ProfileStop();*/
Engine.ProfileStop();
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/filters-extend.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/filters-extend.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/filters-extend.js (revision 12343)
@@ -1,33 +1,42 @@
// Some new filters I use in entity Collections
Filters["byID"] =
function(id){
return {"func": function(ent){
return (ent.id() == id);
},
"dynamicProperties": ['id']};
};
Filters["byTargetedEntity"] =
function(targetID){
return {"func": function(ent){
return (ent.unitAIOrderData() && ent.unitAIOrderData()["target"] && ent.unitAIOrderData()["target"] == targetID);
},
"dynamicProperties": ['unitAIOrderData']};
};
Filters["byHasMetadata"] =
function(key){
return {"func" : function(ent){
return (ent.getMetadata(key) != undefined);
},
"dynamicProperties": ['metadata.' + key]};
};
Filters["byTerritory"] = function(Map, territoryIndex){
return {"func": function(ent){
if (Map.point(ent.position()) == territoryIndex) {
return true;
} else {
return false;
}
},
"dynamicProperties": ['position']};
-};
\ No newline at end of file
+};
+Filters["isDropsite"] = function(resourceType){
+ return {"func": function(ent){
+ return (ent.resourceDropsiteTypes() && ent.resourceDropsiteTypes().indexOf(resourceType) !== -1
+ && ent.foundationProgress() === undefined);
+ },
+ "dynamicProperties": []};
+};
+
+
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/map-module.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/map-module.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/map-module.js (revision 12343)
@@ -1,263 +1,377 @@
const TERRITORY_PLAYER_MASK = 0x3F;
//TODO: Make this cope with negative cell values
-function Map(gameState, originalMap){
+function Map(gameState, originalMap, actualCopy){
// get the map to find out the correct dimensions
var gameMap = gameState.getMap();
this.width = gameMap.width;
this.height = gameMap.height;
this.length = gameMap.data.length;
- if (originalMap){
+
+ if (originalMap && actualCopy){
+ this.map = new Uint16Array(this.length);
+ for (var i = 0; i < originalMap.length; ++i)
+ this.map[i] = originalMap[i];
+ } else if (originalMap) {
this.map = originalMap;
- }else{
+ } else {
this.map = new Uint16Array(this.length);
}
this.cellSize = gameState.cellSize;
}
Map.prototype.gamePosToMapPos = function(p){
return [Math.round(p[0]/this.cellSize), Math.round(p[1]/this.cellSize)];
};
Map.prototype.point = function(p){
var q = this.gamePosToMapPos(p);
return this.map[q[0] + this.width * q[1]];
};
Map.createObstructionMap = function(gameState, template){
var passabilityMap = gameState.getMap();
var territoryMap = gameState.ai.territoryMap;
// default values
var placementType = "land";
var buildOwn = true;
var buildAlly = true;
var buildNeutral = true;
var buildEnemy = false;
// If there is a template then replace the defaults
if (template){
placementType = template.buildPlacementType();
buildOwn = template.hasBuildTerritory("own");
buildAlly = template.hasBuildTerritory("ally");
buildNeutral = template.hasBuildTerritory("neutral");
buildEnemy = template.hasBuildTerritory("enemy");
}
var obstructionMask = gameState.getPassabilityClassMask("foundationObstruction");
// Only accept valid land tiles (we don't handle docks yet)
switch(placementType){
case "shore":
obstructionMask |= gameState.getPassabilityClassMask("building-shore");
break;
case "land":
default:
obstructionMask |= gameState.getPassabilityClassMask("building-land");
break;
}
var playerID = gameState.getPlayerID();
var obstructionTiles = new Uint16Array(passabilityMap.data.length);
for (var i = 0; i < passabilityMap.data.length; ++i)
{
var tilePlayer = (territoryMap.data[i] & TERRITORY_PLAYER_MASK);
var invalidTerritory = (
(!buildOwn && tilePlayer == playerID) ||
(!buildAlly && gameState.isPlayerAlly(tilePlayer) && tilePlayer != playerID) ||
(!buildNeutral && tilePlayer == 0) ||
(!buildEnemy && gameState.isPlayerEnemy(tilePlayer) && tilePlayer != 0)
);
var tileAccessible = (gameState.ai.accessibility.map[i] == 1);
obstructionTiles[i] = (!tileAccessible || invalidTerritory || (passabilityMap.data[i] & obstructionMask)) ? 0 : 65535;
}
var map = new Map(gameState, obstructionTiles);
if (template && template.buildDistance()){
var minDist = template.buildDistance().MinDistance;
var category = template.buildDistance().FromCategory;
if (minDist !== undefined && category !== undefined){
gameState.getOwnEntities().forEach(function(ent) {
if (ent.buildCategory() === category && ent.position()){
var pos = ent.position();
var x = Math.round(pos[0] / gameState.cellSize);
var z = Math.round(pos[1] / gameState.cellSize);
map.addInfluence(x, z, minDist/gameState.cellSize, -65535, 'constant');
}
});
}
}
return map;
};
Map.createTerritoryMap = function(gameState) {
var map = gameState.ai.territoryMap;
var ret = new Map(gameState, map.data);
ret.getOwner = function(p) {
return this.point(p) & TERRITORY_PLAYER_MASK;
}
-
+ ret.getOwnerIndex = function(p) {
+ return this.map[p] & TERRITORY_PLAYER_MASK;
+ }
return ret;
};
-
+Map.prototype.drawDistance = function(gameState, elements) {
+ for ( var y = 0; y < this.height; ++y) {
+ for ( var x = 0; x < this.width; ++x) {
+ var minDist = 500000;
+ for (i in elements) {
+ var px = elements[i].position()[0]/gameState.cellSize;
+ var py = elements[i].position()[1]/gameState.cellSize;
+ var dist = VectorDistance([px,py], [x,y]);
+ if (dist < minDist)
+ minDist = dist;
+ }
+ this.map[x + y*this.width] = Math.max(1,this.width - minDist);
+ }
+ }
+};
Map.prototype.addInfluence = function(cx, cy, maxDist, strength, type) {
- strength = strength ? strength : maxDist;
+ strength = strength ? +strength : +maxDist;
type = type ? type : 'linear';
var x0 = Math.max(0, cx - maxDist);
var y0 = Math.max(0, cy - maxDist);
var x1 = Math.min(this.width, cx + maxDist);
var y1 = Math.min(this.height, cy + maxDist);
var maxDist2 = maxDist * maxDist;
- var str = 0;
+ var str = 0.0;
switch (type){
case 'linear':
- str = strength / maxDist;
+ str = +strength / +maxDist;
break;
case 'quadratic':
- str = strength / maxDist2;
+ str = +strength / +maxDist2;
break;
case 'constant':
- str = strength;
+ str = +strength;
break;
}
-
for ( var y = y0; y < y1; ++y) {
for ( var x = x0; x < x1; ++x) {
var dx = x - cx;
var dy = y - cy;
var r2 = dx*dx + dy*dy;
if (r2 < maxDist2){
var quant = 0;
switch (type){
case 'linear':
var r = Math.sqrt(r2);
quant = str * (maxDist - r);
break;
case 'quadratic':
quant = str * (maxDist2 - r2);
break;
case 'constant':
quant = str;
break;
}
-
if (-1 * quant > this.map[x + y * this.width]){
this.map[x + y * this.width] = 0; //set anything which would have gone negative to 0
}else{
this.map[x + y * this.width] += quant;
}
}
}
}
};
+Map.prototype.multiplyInfluence = function(cx, cy, maxDist, strength, type) {
+ strength = strength ? +strength : +maxDist;
+ type = type ? type : 'constant';
+
+ var x0 = Math.max(0, cx - maxDist);
+ var y0 = Math.max(0, cy - maxDist);
+ var x1 = Math.min(this.width, cx + maxDist);
+ var y1 = Math.min(this.height, cy + maxDist);
+ var maxDist2 = maxDist * maxDist;
+
+ var str = 0.0;
+ switch (type){
+ case 'linear':
+ str = strength / maxDist;
+ break;
+ case 'quadratic':
+ str = strength / maxDist2;
+ break;
+ case 'constant':
+ str = strength;
+ break;
+ }
+
+ for ( var y = y0; y < y1; ++y) {
+ for ( var x = x0; x < x1; ++x) {
+ var dx = x - cx;
+ var dy = y - cy;
+ var r2 = dx*dx + dy*dy;
+ if (r2 < maxDist2){
+ var quant = 0;
+ switch (type){
+ case 'linear':
+ var r = Math.sqrt(r2);
+ quant = str * (maxDist - r);
+ break;
+ case 'quadratic':
+ quant = str * (maxDist2 - r2);
+ break;
+ case 'constant':
+ quant = str;
+ break;
+ }
+ var machin = this.map[x + y * this.width] * quant;
+ if (machin <= 0){
+ this.map[x + y * this.width] = 0; //set anything which would have gone negative to 0
+ }else{
+ this.map[x + y * this.width] = machin;
+ }
+ }
+ }
+ }
+};
+Map.prototype.setInfluence = function(cx, cy, maxDist, value) {
+ value = value ? value : 0;
+
+ var x0 = Math.max(0, cx - maxDist);
+ var y0 = Math.max(0, cy - maxDist);
+ var x1 = Math.min(this.width, cx + maxDist);
+ var y1 = Math.min(this.height, cy + maxDist);
+ var maxDist2 = maxDist * maxDist;
+
+ for ( var y = y0; y < y1; ++y) {
+ for ( var x = x0; x < x1; ++x) {
+ var dx = x - cx;
+ var dy = y - cy;
+ var r2 = dx*dx + dy*dy;
+ if (r2 < maxDist2){
+ this.map[x + y * this.width] = value;
+ }
+ }
+ }
+};
+
Map.prototype.sumInfluence = function(cx, cy, radius){
var x0 = Math.max(0, cx - radius);
var y0 = Math.max(0, cy - radius);
var x1 = Math.min(this.width, cx + radius);
var y1 = Math.min(this.height, cy + radius);
var radius2 = radius * radius;
var sum = 0;
for ( var y = y0; y < y1; ++y) {
for ( var x = x0; x < x1; ++x) {
var dx = x - cx;
var dy = y - cy;
var r2 = dx*dx + dy*dy;
if (r2 < radius2){
sum += this.map[x + y * this.width];
}
}
}
return sum;
};
/**
* Make each cell's 16-bit value at least one greater than each of its
* neighbours' values. (If the grid is initialised with 0s and 65535s, the
* result of each cell is its Manhattan distance to the nearest 0.)
*
* TODO: maybe this should be 8-bit (and clamp at 255)?
*/
Map.prototype.expandInfluences = function() {
var w = this.width;
var h = this.height;
var grid = this.map;
for ( var y = 0; y < h; ++y) {
var min = 65535;
for ( var x = 0; x < w; ++x) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
}
for ( var x = w - 2; x >= 0; --x) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
}
}
for ( var x = 0; x < w; ++x) {
var min = 65535;
for ( var y = 0; y < h; ++y) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
}
for ( var y = h - 2; y >= 0; --y) {
var g = grid[x + y * w];
if (g > min)
grid[x + y * w] = min;
else if (g < min)
min = g;
++min;
}
}
};
Map.prototype.findBestTile = function(radius, obstructionTiles){
// Find the best non-obstructed tile
var bestIdx = 0;
var bestVal = -1;
for ( var i = 0; i < this.length; ++i) {
if (obstructionTiles.map[i] > radius) {
var v = this.map[i];
if (v > bestVal) {
bestVal = v;
bestIdx = i;
}
}
}
return [bestIdx, bestVal];
};
-// Multiplies current map by the parameter map pixelwise
-Map.prototype.multiply = function(map){
- for (var i = 0; i < this.length; i++){
- this.map[i] *= map.map[i];
+// Multiplies current map by 3 if in my territory
+Map.prototype.multiplyTerritory = function(gameState,map){
+ for (var i = 0; i < this.length; ++i){
+ if (map.getOwnerIndex(i) === gameState.player)
+ this.map[i] *= 2.5;
+ }
+};
+// Multiplies current map by the parameter map pixelwise
+Map.prototype.multiply = function(map, onlyBetter,divider,maxMultiplier){
+ for (var i = 0; i < this.length; ++i){
+ if (map.map[i]/divider > 1)
+ this.map[i] = Math.min(maxMultiplier*this.map[i], this.map[i] * (map.map[i]/divider));
+ }
+};
+// add to current map by the parameter map pixelwise
+Map.prototype.add = function(map){
+ for (var i = 0; i < this.length; ++i){
+ this.map[i] += +map.map[i];
+ }
+};
+// add to current map by the parameter map pixelwise
+Map.prototype.subtract = function(map){
+ for (var i = 0; i < this.length; ++i){
+ this.map[i] += map.map[i];
+ if (this.map[i] <= 0)
+ this.map[i] = 0;
}
};
Map.prototype.dumpIm = function(name, threshold){
name = name ? name : "default.png";
- threshold = threshold ? threshold : 256;
+ threshold = threshold ? threshold : 65500;
Engine.DumpImage(name, this.map, this.width, this.height, threshold);
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/entity-extend.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/entity-extend.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/entity-extend.js (revision 12343)
@@ -1,160 +1,185 @@
EntityTemplate.prototype.genericName = function() {
if (!this._template.Identity || !this._template.Identity.GenericName)
return undefined;
return this._template.Identity.GenericName;
};
EntityTemplate.prototype.walkSpeed = function() {
if (!this._template.UnitMotion || !this._template.UnitMotion.WalkSpeed)
return undefined;
return this._template.UnitMotion.WalkSpeed;
};
EntityTemplate.prototype.buildTime = function() {
if (!this._template.Cost || !this._template.Cost.buildTime)
return undefined;
return this._template.Cost.buildTime;
};
EntityTemplate.prototype.getPopulationBonus = function() {
if (!this._template.Cost || !this._template.Cost.PopulationBonus)
return undefined;
return this._template.Cost.PopulationBonus;
};
// will return either "food", "wood", "stone", "metal" and not treasure.
EntityTemplate.prototype.getResourceType = function() {
if (!this._template.ResourceSupply)
return undefined;
var [type, subtype] = this._template.ResourceSupply.Type.split('.');
if (type == "treasure")
return subtype;
return type;
};
EntityTemplate.prototype.garrisonMax = function() {
if (!this._template.GarrisonHolder)
return undefined;
return this._template.GarrisonHolder.Max;
};
EntityTemplate.prototype.hasClasses = function(array) {
var classes = this.classes();
if (!classes)
return false;
for (i in array)
if (classes.indexOf(array[i]) === -1)
return false;
return true;
};
+// returns the classes this counters:
+// each countered class is an array specifying what is required (even if only one) and the Multiplier [ ["whatever","other whatever"] , 0 ].
+EntityTemplate.prototype.getCounteredClasses = function() {
+ if (!this._template.Attack)
+ return undefined;
+
+ var Classes = [];
+ for (i in this._template.Attack) {
+ if (!this._template.Attack[i].Bonuses)
+ continue;
+ for (o in this._template.Attack[i].Bonuses) {
+ Classes.push([this._template.Attack[i].Bonuses[o].Classes.split(" "), +this._template.Attack[i].Bonuses[o].Multiplier]);
+ }
+ }
+ return Classes;
+};
+
EntityTemplate.prototype.getMaxStrength = function()
{
var strength = 0.0;
var attackTypes = this.attackTypes();
var armourStrength = this.armourStrengths();
var hp = this.maxHitpoints() / 100.0; // some normalization
for (var typeKey in attackTypes) {
var type = attackTypes[typeKey];
var attackStrength = this.attackStrengths(type);
var attackRange = this.attackRange(type);
var attackTimes = this.attackTimes(type);
for (var str in attackStrength) {
var val = parseFloat(attackStrength[str]);
switch (str) {
case "crush":
strength += (val * 0.085) / 3;
break;
case "hack":
strength += (val * 0.075) / 3;
break;
case "pierce":
strength += (val * 0.065) / 3;
break;
}
}
if (attackRange){
strength += (attackRange.max * 0.0125) ;
}
for (var str in attackTimes) {
var val = parseFloat(attackTimes[str]);
switch (str){
case "repeat":
strength += (val / 100000);
break;
case "prepare":
strength -= (val / 100000);
break;
}
}
}
for (var str in armourStrength) {
var val = parseFloat(armourStrength[str]);
switch (str) {
case "crush":
strength += (val * 0.085) / 3;
break;
case "hack":
strength += (val * 0.075) / 3;
break;
case "pierce":
strength += (val * 0.065) / 3;
break;
}
}
return strength * hp;
};
EntityTemplate.prototype.costSum = function() {
if (!this._template.Cost)
return undefined;
var ret = 0;
for (var type in this._template.Cost.Resources)
ret += +this._template.Cost.Resources[type];
return ret;
};
-
Entity.prototype.deleteMetadata = function(id) {
delete this._ai._entityMetadata[this.id()];
};
+Entity.prototype.healthLevel = function() {
+ return (this.hitpoints() / this.maxHitpoints());
+};
+
+Entity.prototype.visibility = function(player) {
+ return this._entity.visibility[player-1];
+};
+
Entity.prototype.unload = function(id) {
if (!this._template.GarrisonHolder)
return undefined;
Engine.PostCommand({"type": "unload", "garrisonHolder": this.id(), "entity": id});
return this;
};
Entity.prototype.unloadAll = function() {
if (!this._template.GarrisonHolder)
return undefined;
Engine.PostCommand({"type": "unload-all", "garrisonHolder": this.id()});
return this;
};
Entity.prototype.garrison = function(target) {
Engine.PostCommand({"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": false});
return this;
};
Entity.prototype.stopMoving = function() {
if (this.position() !== undefined)
Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": this.position()[0], "z": this.position()[1], "queued": false});
};
// from from a unit in the opposite direction.
Entity.prototype.flee = function(unitToFleeFrom) {
if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) {
var FleeDirection = [unitToFleeFrom.position()[0] - this.position()[0],unitToFleeFrom.position()[1] - this.position()[1]];
var dist = VectorDistance(unitToFleeFrom.position(), this.position() );
FleeDirection[0] = (FleeDirection[0]/dist) * 5;
FleeDirection[1] = (FleeDirection[1]/dist) * 5;
Engine.PostCommand({"type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0]*5, "z": this.position()[1] + FleeDirection[1]*5, "queued": false});
}
return this;
};
Entity.prototype.barter = function(buyType, sellType, amount) {
Engine.PostCommand({"type": "barter", "sell" : sellType, "buy" : buyType, "amount" : amount });
return this;
};
+
Index: ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/plan-training.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/plan-training.js (revision 12342)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/qbot-wc/plan-training.js (revision 12343)
@@ -1,57 +1,67 @@
var UnitTrainingPlan = function(gameState, type, metadata, number) {
this.type = gameState.applyCiv(type);
this.metadata = metadata;
this.template = gameState.getTemplate(this.type);
if (!this.template) {
this.invalidTemplate = true;
this.template = undefined;
return;
}
this.category= "unit";
this.cost = new Resources(this.template.cost(), this.template._template.Cost.Population);
if (!number){
this.number = 1;
}else{
this.number = number;
}
};
UnitTrainingPlan.prototype.canExecute = function(gameState) {
if (this.invalidTemplate)
return false;
// TODO: we should probably check pop caps
var trainers = gameState.findTrainers(this.type);
return (trainers.length != 0);
};
UnitTrainingPlan.prototype.execute = function(gameState) {
//warn("Executing UnitTrainingPlan " + uneval(this));
-
+ var self = this;
var trainers = gameState.findTrainers(this.type).toEntityArray();
// Prefer training buildings with short queues
// (TODO: this should also account for units added to the queue by
// plans that have already been executed this turn)
if (trainers.length > 0){
trainers.sort(function(a, b) {
+
+ if (self.metadata["plan"] !== undefined) {
+ var aa = a.trainingQueueTime();
+ var bb = b.trainingQueueTime();
+ if (a.hasClass("Civic"))
+ aa += 20;
+ if (b.hasClass("Civic"))
+ bb += 20;
+ return (a.trainingQueueTime() - b.trainingQueueTime());
+ }
return a.trainingQueueTime() - b.trainingQueueTime();
});
trainers[0].train(this.type, this.number, this.metadata);
}
};
UnitTrainingPlan.prototype.getCost = function(){
var multCost = new Resources();
multCost.add(this.cost);
multCost.multiply(this.number);
return multCost;
};
UnitTrainingPlan.prototype.addItem = function(){
this.number += 1;
};
\ No newline at end of file