Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -138,6 +138,10 @@ // ignore spurious movement messages }, + "MoveObstructed": function() { + // ignore spurious movement messages + }, + "ConstructionFinished": function(msg) { // ignore uninteresting construction messages }, @@ -1860,6 +1864,14 @@ } }, + "MoveObstructed": function(msg) { + // If our path was blocked by an entity, try and attack it unless we're forced to target something else + if (msg.data.entity != INVALID_ENTITY && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force))) + { + this.RespondToTargetedEntities([msg.data.entity]); + } + }, + "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 @@ -4018,6 +4030,11 @@ this.UnitFsm.ProcessMessage(this, {"type": "MoveCompleted", "data": msg}); }; +UnitAI.prototype.OnMotionObstructed = function(msg) +{ + this.UnitFsm.ProcessMessage(this, {"type": "MoveObstructed", "data": msg}); +}; + UnitAI.prototype.OnGlobalConstructionFinished = function(msg) { // TODO: This is a bit inefficient since every unit listens to every @@ -4771,7 +4788,7 @@ // Stop if it's left our vision range, unless we're especially persistent if (!this.GetStance().respondChaseBeyondVision) { - if (!this.CheckTargetIsInVisionRange(target)) + if (!this.CheckTargetVisible(target)) return true; } @@ -5127,7 +5144,7 @@ this.WalkToTarget(target, queued); return; } - this.AddOrder("Attack", { "target": target, "force": true, "allowCapture": allowCapture}, queued); + this.AddOrder("Attack", { "target": target, "force": false, "allowCapture": allowCapture}, queued); }; /** @@ -6009,7 +6026,7 @@ cmpMotion.SetFacePointAfterMove(val); }; -UnitAI.prototype.AttackEntitiesByPreference = function(ents) +UnitAI.prototype.AttackEntitiesByPreference = function(ents, checkAll) { if (!ents.length) return false; @@ -6029,6 +6046,9 @@ let entsByPreferences = {}; let preferences = []; let entsWithoutPref = []; + + let valid = 0; + for (let ent of ents) { if (!attackfilter(ent)) @@ -6043,6 +6063,9 @@ } else entsByPreferences[pref].push(ent); + // stop after 8, we'll retry with all of them if we failed to see them as valid. + if (!checkAll && valid++ > 8) + break; } if (preferences.length) @@ -6053,7 +6076,11 @@ return true; } - return this.RespondToTargetedEntities(entsWithoutPref); + let couldAttack = this.RespondToTargetedEntities(entsWithoutPref); + if (!couldAttack && !checkAll) + return this.AttackEntitiesByPreference(ents, true) + else + return couldAttack; }; /** Index: source/simulation2/MessageTypes.h =================================================================== --- source/simulation2/MessageTypes.h +++ source/simulation2/MessageTypes.h @@ -335,6 +335,23 @@ }; /** + * Sent by CCmpUnitMotion during Update, whenever the motion was obstructed + * and it was not obstructed the turn before. + */ +class CMessageMotionObstructed : public CMessage +{ +public: + DEFAULT_MESSAGE_IMPL(MotionObstructed) + + CMessageMotionObstructed(entity_id_t ent) : + entity(ent) + { + } + + entity_id_t entity; // ID of entity we collided with, or INVALID_ENTITY if something else. +}; + +/** * Sent when water height has been changed. */ class CMessageWaterChanged : public CMessage Index: source/simulation2/TypeList.h =================================================================== --- source/simulation2/TypeList.h +++ source/simulation2/TypeList.h @@ -46,6 +46,7 @@ MESSAGE(InterpolatedPositionChanged) MESSAGE(TerritoryPositionChanged) MESSAGE(MotionChanged) +MESSAGE(MotionObstructed) MESSAGE(RangeUpdate) MESSAGE(TerrainChanged) MESSAGE(VisibilityChanged) Index: source/simulation2/components/CCmpObstructionManager.cpp =================================================================== --- source/simulation2/components/CCmpObstructionManager.cpp +++ source/simulation2/components/CCmpObstructionManager.cpp @@ -465,7 +465,7 @@ } } - virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits = false); + virtual std::pair TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits = false); virtual bool TestStaticShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t a, entity_pos_t w, entity_pos_t h, std::vector* out); virtual bool TestUnitShape(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, std::vector* out); @@ -658,13 +658,13 @@ REGISTER_COMPONENT_TYPE(ObstructionManager) -bool CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits) +std::pair CCmpObstructionManager::TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits) { PROFILE("TestLine"); // Check that both end points are within the world (which means the whole line must be) if (!IsInWorld(x0, z0, r) || !IsInWorld(x1, z1, r)) - return true; + return { true, INVALID_ENTITY }; CFixedVector2D posMin (std::min(x0, x1) - r, std::min(z0, z1) - r); CFixedVector2D posMax (std::max(x0, x1) + r, std::max(z0, z1) + r); @@ -687,7 +687,7 @@ CFixedVector2D center(it->second.x, it->second.z); CFixedVector2D halfSize(it->second.clearance + unitUnitRadius, it->second.clearance + unitUnitRadius); if (Geometry::TestRayAASquare(CFixedVector2D(x0, z0) - center, CFixedVector2D(x1, z1) - center, halfSize)) - return true; + return { true, it->second.entity }; } std::vector staticShapes; @@ -703,10 +703,9 @@ CFixedVector2D center(it->second.x, it->second.z); CFixedVector2D halfSize(it->second.hw + r, it->second.hh + r); if (Geometry::TestRaySquare(CFixedVector2D(x0, z0) - center, CFixedVector2D(x1, z1) - center, it->second.u, it->second.v, halfSize)) - return true; + return { true, it->second.entity }; } - - return false; + return { false, INVALID_ENTITY }; } bool CCmpObstructionManager::TestStaticShape(const IObstructionTestFilter& filter, Index: source/simulation2/components/CCmpPathfinder.cpp =================================================================== --- source/simulation2/components/CCmpPathfinder.cpp +++ source/simulation2/components/CCmpPathfinder.cpp @@ -791,7 +791,7 @@ ////////////////////////////////////////////////////////// -bool CCmpPathfinder::CheckMovement(const IObstructionTestFilter& filter, +std::pair CCmpPathfinder::CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass) { @@ -801,12 +801,17 @@ // Use more permissive version of TestLine to allow unit-unit collisions to overlap slightly. // This makes movement smoother and more natural for units, overall. CmpPtr cmpObstructionManager(GetSystemEntity()); - if (!cmpObstructionManager || cmpObstructionManager->TestLine(filter, x0, z0, x1, z1, r, true)) - return false; + if (!cmpObstructionManager) + return { false, INVALID_ENTITY }; + + std::pair ret = cmpObstructionManager->TestLine(filter, x0, z0, x1, z1, r, true); + ret.first = !ret.first; + if (!ret.first) + return ret; // Then test against the terrain grid. This should not be necessary // But in case we allow terrain to change it will become so. - return Pathfinding::CheckLineMovement(x0, z0, x1, z1, passClass, *m_TerrainOnlyGrid); + return { Pathfinding::CheckLineMovement(x0, z0, x1, z1, passClass, *m_TerrainOnlyGrid), INVALID_ENTITY }; } ICmpObstruction::EFoundationCheck CCmpPathfinder::CheckUnitPlacement(const IObstructionTestFilter& filter, Index: source/simulation2/components/CCmpPathfinder_Common.h =================================================================== --- source/simulation2/components/CCmpPathfinder_Common.h +++ source/simulation2/components/CCmpPathfinder_Common.h @@ -271,7 +271,7 @@ virtual void SetAtlasOverlay(bool enable, pass_class_t passClass = 0); - virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass); + virtual std::pair CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass); virtual ICmpObstruction::EFoundationCheck CheckUnitPlacement(const IObstructionTestFilter& filter, entity_pos_t x, entity_pos_t z, entity_pos_t r, pass_class_t passClass, bool onlyCenterPoint); Index: source/simulation2/components/CCmpRallyPointRenderer.cpp =================================================================== --- source/simulation2/components/CCmpRallyPointRenderer.cpp +++ source/simulation2/components/CCmpRallyPointRenderer.cpp @@ -1098,7 +1098,7 @@ curNodeY = std::max(curNodeY, cmpWaterManager->GetExactWaterLevel(coords[curNodeIdx].X, coords[curNodeIdx].Y)); // find out whether curNode is visible from baseNode (careful; this is in 2D only; terrain height differences are ignored!) - bool curNodeVisible = cmpPathFinder->CheckMovement(obstructionFilter, baseNodeX, baseNodeZ, curNodeX, curNodeZ, lineRadius, passabilityClass); + bool curNodeVisible = cmpPathFinder->CheckMovement(obstructionFilter, baseNodeX, baseNodeZ, curNodeX, curNodeZ, lineRadius, passabilityClass).first; // since height differences are ignored by CheckMovement, let's call two points visible from one another only if they're at // roughly the same terrain elevation Index: source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- source/simulation2/components/CCmpUnitMotion.cpp +++ source/simulation2/components/CCmpUnitMotion.cpp @@ -242,6 +242,8 @@ // Motion planning u8 m_Tries; // how many tries we've done to get to our current Final Goal. + bool m_Obstructed; // whether we ran into an obstruction last turn + PathGoal m_FinalGoal; static std::string GetSchema() @@ -281,6 +283,8 @@ m_Moving = false; m_FacePointAfterMove = true; + m_Obstructed = false; + m_WalkSpeed = m_OriginalWalkSpeed = paramNode.GetChild("WalkSpeed").ToFixed(); m_Speed = m_WalkSpeed; m_CurSpeed = fixed::Zero(); @@ -346,6 +350,8 @@ serialize.NumberU8("tries", m_Tries, 0, 255); + serialize.Bool("obstructed", m_Obstructed); + SerializeVector()(serialize, "long path", m_LongPath.m_Waypoints); SerializeVector()(serialize, "short path", m_ShortPath.m_Waypoints); @@ -870,7 +876,7 @@ fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); - bool wasObstructed = false; + std::pair passageClear = { false, INVALID_ENTITY }; // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint @@ -894,11 +900,13 @@ // Work out how far we can travel in timeLeft fixed maxdist = maxSpeed.Multiply(timeLeft); + passageClear = cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass); + // If the target is close, we can move there directly fixed offsetLength = offset.Length(); if (offsetLength <= maxdist) { - if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) + if (passageClear.first) { pos = target; @@ -913,11 +921,7 @@ continue; } else - { - // Error - path was obstructed - wasObstructed = true; break; - } } else { @@ -925,10 +929,8 @@ offset.Normalize(maxdist); target = pos + offset; - if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) + if (passageClear.first) pos = target; - else - wasObstructed = true; // Error - path was obstructed break; } @@ -947,15 +949,24 @@ m_CurSpeed = cmpPosition->GetDistanceTravelled() / dt; } - if (wasObstructed) + if (!passageClear.first) { - // Oops, we hit something (very likely another unit). + // Oops, we hit something. // This is when we might easily get stuck wrongly. // check if we've arrived. if (ShouldConsiderOurselvesAtDestination(pos)) return; + if (!m_Obstructed) + { + m_Obstructed = true; + + // send a message saying we obstructed, in case we may want to do something else entirely. + CMessageMotionObstructed msg(passageClear.second); + GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); + } + // If we still have long waypoints, try and compute a short path // This will get us around units, amongst others. // However in some cases a long waypoint will be in located in the obstruction of @@ -1005,6 +1016,8 @@ // potential TODO: We could switch the short-range pathfinder for something else entirely. return; } + else + m_Obstructed = false; // We successfully moved along our path, until running out of // waypoints or time. @@ -1103,7 +1116,7 @@ return false; // Check if there's any collisions on that route - if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) + if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass).first) return false; // That route is okay, so update our path @@ -1139,7 +1152,7 @@ CFixedVector2D goalPos = goal.NearestPointOnGoal(from); // Check if there's any collisions on that route - if (!cmpPathfinder->CheckMovement(GetObstructionFilter(true), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) + if (!cmpPathfinder->CheckMovement(GetObstructionFilter(true), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass).first) return false; // That route is okay, so update our path Index: source/simulation2/components/ICmpObstructionManager.h =================================================================== --- source/simulation2/components/ICmpObstructionManager.h +++ source/simulation2/components/ICmpObstructionManager.h @@ -169,9 +169,11 @@ * @param z1 Z coordinate of line's second point * @param r radius (half width) of line * @param relaxClearanceForUnits whether unit-unit collisions should be more permissive. - * @return true if there is a collision + * @return if there is a collision with an entity + * @return if we collided for another reason + * @return if we did not collide */ - virtual bool TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits) = 0; + virtual std::pair TestLine(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, bool relaxClearanceForUnits) = 0; /** * Collision test a static square shape against the current set of shapes. Index: source/simulation2/components/ICmpPathfinder.h =================================================================== --- source/simulation2/components/ICmpPathfinder.h +++ source/simulation2/components/ICmpPathfinder.h @@ -127,9 +127,11 @@ /** * Check whether the given movement line is valid and doesn't hit any obstructions * or impassable terrain. - * Returns true if the movement is okay. + * @return if we where obstructed by bumping against entity ID + * @return if we collided for another reason + * @return if the movement is clear */ - virtual bool CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass) = 0; + virtual std::pair CheckMovement(const IObstructionTestFilter& filter, entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, entity_pos_t r, pass_class_t passClass) = 0; /** * Check whether a unit placed here is valid and doesn't hit any obstructions Index: source/simulation2/scripting/MessageTypeConversions.cpp =================================================================== --- source/simulation2/scripting/MessageTypeConversions.cpp +++ source/simulation2/scripting/MessageTypeConversions.cpp @@ -285,6 +285,22 @@ //////////////////////////////// +JS::Value CMessageMotionObstructed::ToJSVal(ScriptInterface& scriptInterface) const +{ + TOJSVAL_SETUP(); + SET_MSG_PROPERTY(entity); + return JS::ObjectValue(*obj); +} + +CMessage* CMessageMotionObstructed::FromJSVal(ScriptInterface& scriptInterface, JS::HandleValue val) +{ + FROMJSVAL_SETUP(); + GET_MSG_PROPERTY(entity_id_t, entity); + return new CMessageMotionObstructed(entity); +} + +//////////////////////////////// + JS::Value CMessageTerrainChanged::ToJSVal(ScriptInterface& scriptInterface) const { TOJSVAL_SETUP();