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