Index: ps/trunk/binaries/data/mods/public/simulation/components/Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 20133) +++ ps/trunk/binaries/data/mods/public/simulation/components/Attack.js (revision 20134) @@ -1,613 +1,612 @@ function Attack() {} var g_AttackTypes = ["Melee", "Ranged", "Capture"]; Attack.prototype.bonusesSchema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; Attack.prototype.preferredClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; Attack.prototype.restrictedClassesSchema = "" + "" + "" + "tokens" + "" + "" + "" + ""; Attack.prototype.Schema = "Controls the attack abilities and strengths of the unit." + "" + "" + "10.0" + "0.0" + "5.0" + "4.0" + "1000" + "" + "" + "pers" + "Infantry" + "1.5" + "" + "" + "Cavalry Melee" + "1.5" + "" + "" + "Champion" + "Cavalry Infantry" + "" + "" + "0.0" + "10.0" + "0.0" + "44.0" + "20.0" + "15.0" + "800" + "1600" + "50.0" + "2.5" + "" + "" + "Cavalry" + "2" + "" + "" + "Champion" + "" + "Circular" + "20" + "false" + "0.0" + "10.0" + "0.0" + "" + "" + "" + "1000.0" + "0.0" + "0.0" + "4.0" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""+ "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + Attack.prototype.bonusesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: it shouldn't be stretched "" + "" + Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + // TODO: how do these work? Attack.prototype.bonusesSchema + Attack.prototype.preferredClassesSchema + Attack.prototype.restrictedClassesSchema + "" + "" + ""; Attack.prototype.Init = function() { }; Attack.prototype.Serialize = null; // we have no dynamic state to save Attack.prototype.GetAttackTypes = function(wantedTypes) { let types = g_AttackTypes.filter(type => !!this.template[type]); if (!wantedTypes) return types; let wantedTypesReal = wantedTypes.filter(wtype => wtype.indexOf("!") != 0); return types.filter(type => wantedTypes.indexOf("!" + type) == -1 && (!wantedTypesReal || !wantedTypesReal.length || wantedTypesReal.indexOf(type) != -1)); }; Attack.prototype.GetPreferredClasses = function(type) { if (this.template[type] && this.template[type].PreferredClasses && this.template[type].PreferredClasses._string) return this.template[type].PreferredClasses._string.split(/\s+/); return []; }; Attack.prototype.GetRestrictedClasses = function(type) { if (this.template[type] && this.template[type].RestrictedClasses && this.template[type].RestrictedClasses._string) return this.template[type].RestrictedClasses._string.split(/\s+/); return []; }; Attack.prototype.CanAttack = function(target, wantedTypes) { let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) return true; let cmpThisPosition = Engine.QueryInterface(this.entity, IID_Position); let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpThisPosition || !cmpTargetPosition || !cmpThisPosition.IsInWorld() || !cmpTargetPosition.IsInWorld()) return false; let cmpIdentity = QueryMiragedInterface(target, IID_Identity); if (!cmpIdentity) return false; let targetClasses = cmpIdentity.GetClassesList(); if (targetClasses.indexOf("Domestic") != -1 && this.template.Slaughter && (!wantedTypes || !wantedTypes.filter(wType => wType.indexOf("!") != 0).length)) return true; let cmpEntityPlayer = QueryOwnerInterface(this.entity); let cmpTargetPlayer = QueryOwnerInterface(target); if (!cmpTargetPlayer || !cmpEntityPlayer) return false; let types = this.GetAttackTypes(wantedTypes); let entityOwner = cmpEntityPlayer.GetPlayerID(); let targetOwner = cmpTargetPlayer.GetPlayerID(); let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); // Check if the relative height difference is larger than the attack range // If the relative height is bigger, it means they will never be able to // reach each other, no matter how close they come. let heightDiff = Math.abs(cmpThisPosition.GetHeightOffset() - cmpTargetPosition.GetHeightOffset()); for (let type of types) { if (type != "Capture" && !cmpEntityPlayer.IsEnemy(targetOwner)) continue; if (type == "Capture" && (!cmpCapturable || !cmpCapturable.CanCapture(entityOwner))) continue; if (heightDiff > this.GetRange(type).max) continue; let restrictedClasses = this.GetRestrictedClasses(type); if (!restrictedClasses.length) return true; if (!MatchesClassList(targetClasses, restrictedClasses)) return true; } return false; }; /** * Returns null if we have no preference or the lowest index of a preferred class. */ Attack.prototype.GetPreference = function(target) { let cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; let targetClasses = cmpIdentity.GetClassesList(); let minPref = null; for (let type of this.GetAttackTypes()) { let preferredClasses = this.GetPreferredClasses(type); for (let targetClass of targetClasses) { let pref = preferredClasses.indexOf(targetClass); if (pref === 0) return pref; if (pref != -1 && (minPref === null || minPref > pref)) minPref = pref; } } return minPref; }; /** * Get the full range of attack using all available attack types. */ Attack.prototype.GetFullAttackRange = function() { let ret = { "min": Infinity, "max": 0 }; for (let type of this.GetAttackTypes()) { let range = this.GetRange(type); ret.min = Math.min(ret.min, range.min); ret.max = Math.max(ret.max, range.max); } return ret; }; Attack.prototype.GetBestAttackAgainst = function(target, allowCapture) { let cmpFormation = Engine.QueryInterface(target, IID_Formation); if (cmpFormation) { // TODO: Formation against formation needs review let types = this.GetAttackTypes(); return g_AttackTypes.find(attack => types.indexOf(attack) != -1); } let cmpIdentity = Engine.QueryInterface(target, IID_Identity); if (!cmpIdentity) return undefined; let targetClasses = cmpIdentity.GetClassesList(); let isTargetClass = className => targetClasses.indexOf(className) != -1; // Always slaughter domestic animals instead of using a normal attack if (isTargetClass("Domestic") && this.template.Slaughter) return "Slaughter"; let types = this.GetAttackTypes().filter(type => !this.GetRestrictedClasses(type).some(isTargetClass)); // check if the target is capturable let captureIndex = types.indexOf("Capture"); if (captureIndex != -1) { let cmpCapturable = QueryMiragedInterface(target, IID_Capturable); let cmpPlayer = QueryOwnerInterface(this.entity); if (allowCapture && cmpPlayer && cmpCapturable && cmpCapturable.CanCapture(cmpPlayer.GetPlayerID())) return "Capture"; // not capturable, so remove this attack types.splice(captureIndex, 1); } let isPreferred = className => this.GetPreferredClasses(className).some(isTargetClass); return types.sort((a, b) => (types.indexOf(a) + (isPreferred(a) ? types.length : 0)) - (types.indexOf(b) + (isPreferred(b) ? types.length : 0))).pop(); }; Attack.prototype.CompareEntitiesByPreference = function(a, b) { let aPreference = this.GetPreference(a); let bPreference = this.GetPreference(b); if (aPreference === null && bPreference === null) return 0; if (aPreference === null) return 1; if (bPreference === null) return -1; return aPreference - bPreference; }; Attack.prototype.GetTimers = function(type) { let prepare = +(this.template[type].PrepareTime || 0); prepare = ApplyValueModificationsToEntity("Attack/" + type + "/PrepareTime", prepare, this.entity); let repeat = +(this.template[type].RepeatTime || 1000); repeat = ApplyValueModificationsToEntity("Attack/" + type + "/RepeatTime", repeat, this.entity); return { "prepare": prepare, "repeat": repeat }; }; Attack.prototype.GetAttackStrengths = function(type) { // Work out the attack values with technology effects let template = this.template[type]; let splash = ""; if (!template) { template = this.template[type.split(".")[0]].Splash; splash = "/Splash"; } let applyMods = damageType => ApplyValueModificationsToEntity("Attack/" + type + splash + "/" + damageType, +(template[damageType] || 0), this.entity); if (type == "Capture") return { "value": applyMods("Value") }; return { "hack": applyMods("Hack"), "pierce": applyMods("Pierce"), "crush": applyMods("Crush") }; }; Attack.prototype.GetSplashDamage = function(type) { if (!this.template[type].Splash) return false; let splash = this.GetAttackStrengths(type + ".Splash"); splash.friendlyFire = this.template[type].Splash.FriendlyFire != "false"; splash.shape = this.template[type].Splash.Shape; return splash; }; Attack.prototype.GetRange = function(type) { let max = +this.template[type].MaxRange; max = ApplyValueModificationsToEntity("Attack/" + type + "/MaxRange", max, this.entity); let min = +(this.template[type].MinRange || 0); min = ApplyValueModificationsToEntity("Attack/" + type + "/MinRange", min, this.entity); let elevationBonus = +(this.template[type].ElevationBonus || 0); elevationBonus = ApplyValueModificationsToEntity("Attack/" + type + "/ElevationBonus", elevationBonus, this.entity); return { "max": max, "min": min, "elevationBonus": elevationBonus }; }; Attack.prototype.GetBonusTemplate = function(type) { let template = this.template[type]; if (!template) template = this.template[type.split(".")[0]].Splash; - if (template.Bonuses) - return clone(template.Bonuses); - return null; + + return template.Bonuses || null; }; /** * Attack the target entity. This should only be called after a successful range check, * and should only be called after GetTimers().repeat msec has passed since the last * call to PerformAttack. */ Attack.prototype.PerformAttack = function(type, target) { let attackerOwner = Engine.QueryInterface(this.entity, IID_Ownership).GetOwner(); let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); // If this is a ranged attack, then launch a projectile if (type == "Ranged") { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let turnLength = cmpTimer.GetLatestTurnLength()/1000; // In the future this could be extended: // * Obstacles like trees could reduce the probability of the target being hit // * Obstacles like walls should block projectiles entirely let horizSpeed = +this.template[type].ProjectileSpeed; let gravity = +this.template[type].Gravity; //horizSpeed /= 2; gravity /= 2; // slow it down for testing let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; let selfPosition = cmpPosition.GetPosition(); let cmpTargetPosition = Engine.QueryInterface(target, IID_Position); if (!cmpTargetPosition || !cmpTargetPosition.IsInWorld()) return; let targetPosition = cmpTargetPosition.GetPosition(); let previousTargetPosition = Engine.QueryInterface(target, IID_Position).GetPreviousPosition(); let targetVelocity = Vector3D.sub(targetPosition, previousTargetPosition).div(turnLength); let timeToTarget = this.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); let predictedPosition = (timeToTarget !== false) ? Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition) : targetPosition; // Add inaccuracy based on spread. let distanceModifiedSpread = ApplyValueModificationsToEntity("Attack/Ranged/Spread", +this.template.Ranged.Spread, this.entity) * predictedPosition.horizDistanceTo(selfPosition) / 100; let randNorm = randomNormal2D(); let offsetX = randNorm[0] * distanceModifiedSpread; let offsetZ = randNorm[1] * distanceModifiedSpread; let realTargetPosition = new Vector3D(predictedPosition.x + offsetX, targetPosition.y, predictedPosition.z + offsetZ); // Recalculate when the missile will hit the target position. let realHorizDistance = realTargetPosition.horizDistanceTo(selfPosition); timeToTarget = realHorizDistance / horizSpeed; let missileDirection = Vector3D.sub(realTargetPosition, selfPosition).div(realHorizDistance); // Launch the graphical projectile. let cmpProjectileManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ProjectileManager); let id = cmpProjectileManager.LaunchProjectileAtPoint(this.entity, realTargetPosition, horizSpeed, gravity); cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let data = { "type": type, "attacker": this.entity, "target": target, "strengths": this.GetAttackStrengths(type), "position": realTargetPosition, "direction": missileDirection, "projectileId": id, "bonus": this.GetBonusTemplate(type), "isSplash": false, "attackerOwner": attackerOwner }; if (this.template.Ranged.Splash) { data.friendlyFire = this.template.Ranged.Splash.FriendlyFire != "false"; data.radius = +this.template.Ranged.Splash.Range; data.shape = this.template.Ranged.Splash.Shape; data.isSplash = true; data.splashStrengths = this.GetAttackStrengths(type + ".Splash"); data.splashBonus = this.GetBonusTemplate(type + ".Splash"); } cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Damage, "MissileHit", timeToTarget * 1000, data); } else if (type == "Capture") { if (attackerOwner == -1) return; let multiplier = GetDamageBonus(target, this.GetBonusTemplate(type)); let cmpHealth = Engine.QueryInterface(target, IID_Health); if (!cmpHealth || cmpHealth.GetHitpoints() == 0) return; multiplier *= cmpHealth.GetMaxHitpoints() / (0.1 * cmpHealth.GetMaxHitpoints() + 0.9 * cmpHealth.GetHitpoints()); let cmpCapturable = Engine.QueryInterface(target, IID_Capturable); if (!cmpCapturable || !cmpCapturable.CanCapture(attackerOwner)) return; let strength = this.GetAttackStrengths("Capture").value * multiplier; if (cmpCapturable.Reduce(strength, attackerOwner) && IsOwnedByEnemyOfPlayer(attackerOwner, target)) Engine.PostMessage(target, MT_Attacked, { "attacker": this.entity, "target": target, "type": type, "damage": strength, "attackerOwner": attackerOwner }); } else { // Melee attack - hurt the target immediately cmpDamage.CauseDamage({ "strengths": this.GetAttackStrengths(type), "target": target, "attacker": this.entity, "multiplier": GetDamageBonus(target, this.GetBonusTemplate(type)), "type": type, "attackerOwner": attackerOwner }); } }; /** * Get the predicted time of collision between a projectile (or a chaser) * and its target, assuming they both move in straight line at a constant speed. * Vertical component of movement is ignored. * @param {Vector3D} selfPosition - the 3D position of the projectile (or chaser). * @param {number} horizSpeed - the horizontal speed of the projectile (or chaser). * @param {Vector3D} targetPosition - the 3D position of the target. * @param {Vector3D} targetVelocity - the 3D velocity vector of the target. * @return {Vector3D|boolean} - the 3D predicted position or false if the collision will not happen. */ Attack.prototype.PredictTimeToTarget = function(selfPosition, horizSpeed, targetPosition, targetVelocity) { let relativePosition = new Vector3D.sub(targetPosition, selfPosition); let a = targetVelocity.x * targetVelocity.x + targetVelocity.z * targetVelocity.z - horizSpeed * horizSpeed; let b = relativePosition.x * targetVelocity.x + relativePosition.z * targetVelocity.z; let c = relativePosition.x * relativePosition.x + relativePosition.z * relativePosition.z; // The predicted time to reach the target is the smallest non negative solution // (when it exists) of the equation a t^2 + 2 b t + c = 0. // Using c>=0, we can straightly compute the right solution. if (c == 0) return 0; let disc = b * b - a * c; if (a < 0 || b < 0 && disc >= 0) return c / (Math.sqrt(disc) - b); return false; }; Attack.prototype.OnValueModification = function(msg) { if (msg.component != "Attack") return; let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); if (!cmpUnitAI) return; if (this.GetAttackTypes().some(type => msg.valueNames.indexOf("Attack/" + type + "/MaxRange") != -1)) cmpUnitAI.UpdateRangeQueries(); }; Engine.RegisterComponentType(IID_Attack, "Attack", Attack); Index: ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js (revision 20133) +++ ps/trunk/binaries/data/mods/public/simulation/components/DeathDamage.js (revision 20134) @@ -1,95 +1,93 @@ function DeathDamage() {} DeathDamage.prototype.bonusesSchema = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; DeathDamage.prototype.Schema = "When a unit or building is destroyed, it inflicts damage to nearby units." + "" + "Circular" + "20" + "false" + "0.0" + "10.0" + "50.0" + "" + "" + "" + "" + "" + "" + "" + DeathDamage.prototype.bonusesSchema; DeathDamage.prototype.Init = function() { }; DeathDamage.prototype.Serialize = null; // we have no dynamic state to save DeathDamage.prototype.GetDeathDamageStrengths = function() { // Work out the damage values with technology effects let applyMods = damageType => ApplyValueModificationsToEntity("DeathDamage/" + damageType, +(this.template[damageType] || 0), this.entity); return { "hack": applyMods("Hack"), "pierce": applyMods("Pierce"), "crush": applyMods("Crush") }; }; DeathDamage.prototype.GetBonusTemplate = function() { - if (this.template.Bonuses) - return clone(this.template.Bonuses); - return null; + return this.template.Bonuses || null; }; DeathDamage.prototype.CauseDeathDamage = function() { let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return; let pos = cmpPosition.GetPosition2D(); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); let owner = cmpOwnership.GetOwner(); if (owner == -1) warn("Unit causing death damage does not have any owner."); let cmpDamage = Engine.QueryInterface(SYSTEM_ENTITY, IID_Damage); let playersToDamage = cmpDamage.GetPlayersToDamage(owner, this.template.FriendlyFire); let radius = ApplyValueModificationsToEntity("DeathDamage/Range", +this.template.Range, this.entity); cmpDamage.CauseSplashDamage({ "attacker": this.entity, "origin": pos, "radius": radius, "shape": this.template.Shape, "strengths": this.GetDeathDamageStrengths(), "splashBonus": this.GetBonusTemplate(), "playersToDamage": playersToDamage, "type": "Death", "attackerOwner": owner }); }; Engine.RegisterComponentType(IID_DeathDamage, "DeathDamage", DeathDamage); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/setup.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/setup.js (revision 20133) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/setup.js (revision 20134) @@ -1,98 +1,112 @@ var g_NewIID = 1000; // some arbitrary not-yet-used number var g_NewMTID = 1000; // some arbitrary not-yet-used number var g_ComponentTypes = {}; var g_Components = {}; // Emulate some engine functions: Engine.RegisterComponentType = function(iid, name, ctor) { TS_ASSERT(!g_ComponentTypes[name]); - g_ComponentTypes[name] = { iid: iid, ctor: ctor }; + g_ComponentTypes[name] = { "iid": iid, "ctor": ctor }; }; Engine.RegisterSystemComponentType = function(iid, name, ctor) { TS_ASSERT(!g_ComponentTypes[name]); - g_ComponentTypes[name] = { iid: iid, ctor: ctor }; + g_ComponentTypes[name] = { "iid": iid, "ctor": ctor }; }; Engine.RegisterInterface = function(name) { - global["IID_"+name] = g_NewIID++; + global["IID_" + name] = g_NewIID++; }; Engine.RegisterMessageType = function(name) { - global["MT_"+name] = g_NewMTID++; + global["MT_" + name] = g_NewMTID++; }; Engine.QueryInterface = function(ent, iid) { if (g_Components[ent] && g_Components[ent][iid]) return g_Components[ent][iid]; return null; }; Engine.RegisterGlobal = function(name, value) { global[name] = value; }; Engine.DestroyEntity = function(ent) { - for (var cid in g_Components[ent]) + for (let cid in g_Components[ent]) { - var cmp = g_Components[ent][cid]; + let cmp = g_Components[ent][cid]; if (cmp && cmp.Deinit) cmp.Deinit(); } delete g_Components[ent]; // TODO: should send Destroy message }; Engine.PostMessage = function(ent, iid, message) { // TODO: make this send a message if necessary }; Engine.BroadcastMessage = function(iid, message) { // TODO: make this send a message if necessary }; global.ResetState = function() { g_Components = {}; }; global.AddMock = function(ent, iid, mock) { if (!g_Components[ent]) g_Components[ent] = {}; g_Components[ent][iid] = mock; }; global.DeleteMock = function(ent, iid) { if (!g_Components[ent]) g_Components[ent] = {}; delete g_Components[ent][iid]; }; global.ConstructComponent = function(ent, name, template) { - var cmp = new g_ComponentTypes[name].ctor(); - cmp.entity = ent; - cmp.template = template; + let cmp = new g_ComponentTypes[name].ctor(); + + Object.defineProperties(cmp, { + "entity": { + "value": ent, + "configurable": false, + "enumerable": false, + "writable": false + }, + "template": { + "value": template && deepfreeze(clone(template)), + "configurable": false, + "enumerable": false, + "writable": false + } + }); + cmp.Init(); if (!g_Components[ent]) g_Components[ent] = {}; g_Components[ent][g_ComponentTypes[name].iid] = cmp; return cmp; }; Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/setup_test.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/setup_test.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/setup_test.js (revision 20134) @@ -0,0 +1,24 @@ +Engine.RegisterInterface("TestSetup"); + +function TestSetup() {}; +TestSetup.prototype.Init = function() {}; + +Engine.RegisterSystemComponentType(IID_TestSetup, "TestSetup", TestSetup); +let cmpTestSetup = ConstructComponent(SYSTEM_ENTITY, "TestSetup", { "property": "value" }); + +function expectException(func) +{ + try { + func(); + Engine.TS_FAIL("Missed exception at " + new Error().stack); + } catch (e) {} +} + +expectException(() => { cmpTestSetup.template = "replacement forbidden"; }); +expectException(() => { cmpTestSetup.template.property = "modification forbidden"; }); +expectException(() => { cmpTestSetup.template.other_property = "insertion forbidden"; }); +expectException(() => { delete cmpTestSetup.entity; }); +expectException(() => { delete cmpTestSetup.template; }); +expectException(() => { delete cmpTestSetup.template.property; }); + +TS_ASSERT_UNEVAL_EQUALS(cmpTestSetup.template, { "property": "value" }); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/tests/setup_test.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 20133) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Attack.js (revision 20134) @@ -1,292 +1,288 @@ Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("Attack.js"); let entityID = 903; function attackComponentTest(defenderClass, isEnemy, test_function) { ResetState(); { let playerEnt1 = 5; AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": () => playerEnt1 }); AddMock(playerEnt1, IID_Player, { "GetPlayerID": () => 1, "IsEnemy": () => isEnemy }); } let attacker = entityID; AddMock(attacker, IID_Position, { "IsInWorld": () => true, "GetHeightOffset": () => 5, "GetPosition2D": () => new Vector2D(1, 2) }); AddMock(attacker, IID_Ownership, { "GetOwner": () => 1 }); let cmpAttack = ConstructComponent(attacker, "Attack", { "Melee" : { "Hack": 11, "Pierce": 5, "Crush": 0, "MinRange": 3, "MaxRange": 5, "PreferredClasses": { "_string": "FemaleCitizen" }, "RestrictedClasses": { "_string": "Elephant Archer" }, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 2 } } }, "Ranged" : { "Hack": 0, "Pierce": 10, "Crush": 0, "MinRange": 10, "MaxRange": 80, "PrepareTime": 300, "RepeatTime": 500, "ProjectileSpeed": 50, "Gravity": 9.81, "Spread": 2.5, "PreferredClasses": { "_string": "Archer" }, "RestrictedClasses": { "_string": "Elephant" }, "Splash" : { "Shape": "Circular", "Range": 10, "FriendlyFire": "false", "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0, "Bonuses": { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } } } }, "Capture" : { "Value": 8, "MaxRange": 10, }, "Slaughter": {} }); let defender = ++entityID; AddMock(defender, IID_Identity, { "GetClassesList": () => [defenderClass], "HasClass": className => className == defenderClass }); AddMock(defender, IID_Ownership, { "GetOwner": () => 1 }); AddMock(defender, IID_Position, { "IsInWorld": () => true, "GetHeightOffset": () => 0 }); test_function(attacker, cmpAttack, defender); } // Validate template getter functions attackComponentTest(undefined, true ,(attacker, cmpAttack, defender) => { TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes([]), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged", "Capture"]), ["Melee", "Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "Ranged"]), ["Melee", "Ranged"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Melee", "!Melee"]), []); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee"]), ["Ranged", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["!Melee", "!Ranged"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "!Ranged"]), ["Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackTypes(["Capture", "Melee", "!Ranged"]), ["Melee", "Capture"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetPreferredClasses("Melee"), ["FemaleCitizen"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetRestrictedClasses("Melee"), ["Elephant", "Archer"]); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetFullAttackRange(), { "min": 0, "max": 80 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Capture"), { "value": 8 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Ranged"), { "hack": 0, "pierce": 10, "crush": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetAttackStrengths("Ranged.Splash"), { "hack": 0.0, "pierce": 15.0, "crush": 35.0 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Ranged"), { "prepare": 300, "repeat": 500 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetTimers("Capture"), { "prepare": 0, "repeat": 1000 }); TS_ASSERT_UNEVAL_EQUALS(cmpAttack.GetSplashDamage("Ranged"), { "hack": 0, "pierce": 15, "crush": 35, "friendlyFire": false, "shape": "Circular" }); }); for (let className of ["Infantry", "Cavalry"]) attackComponentTest(className, true, (attacker, cmpAttack, defender) => { TS_ASSERT_EQUALS(cmpAttack.GetBonusTemplate("Melee").BonusCav.Multiplier, 2); - // Check that we don't leak data - let bonus = cmpAttack.GetBonusTemplate("Melee"); - bonus.BonusCav.Multiplier = 2.7; - TS_ASSERT_EQUALS(cmpAttack.GetBonusTemplate("Melee").BonusCav.Multiplier, 2); TS_ASSERT(cmpAttack.GetBonusTemplate("Capture") === null); let getAttackBonus = (t, e) => GetDamageBonus(e, cmpAttack.GetBonusTemplate(t)); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Melee", defender), className == "Cavalry" ? 2 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Ranged", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Ranged.Splash", defender), className == "Cavalry" ? 3 : 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Capture", defender), 1); TS_ASSERT_UNEVAL_EQUALS(getAttackBonus("Slaughter", defender), 1); }); // CanAttack rejects elephant attack due to RestrictedClasses attackComponentTest("Elephant", true, (attacker, cmpAttack, defender) => { TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), false); }); function testGetBestAttackAgainst(defenderClass, bestAttack, isBuilding = false) { attackComponentTest(defenderClass, true, (attacker, cmpAttack, defender) => { if (isBuilding) AddMock(defender, IID_Capturable, { "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; } }); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), defenderClass != "Archer"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), true); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); let allowCapturing = [true]; if (!isBuilding) allowCapturing.push(false); for (let ac of allowCapturing) TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack); }); attackComponentTest(defenderClass, false, (attacker, cmpAttack, defender) => { if (isBuilding) AddMock(defender, IID_Capturable, { "CanCapture": playerID => { TS_ASSERT_EQUALS(playerID, 1); return true; } }); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, []), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged"]), false); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Ranged", "Capture"]), isBuilding); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["!Ranged", "!Melee"]), isBuilding || defenderClass == "Domestic"); TS_ASSERT_EQUALS(cmpAttack.CanAttack(defender, ["Melee", "!Melee"]), false); let allowCapturing = [true]; if (!isBuilding) allowCapturing.push(false); let attack; if (defenderClass == "Domestic") attack = "Slaughter"; else if (defenderClass == "Structure") attack = "Capture"; for (let ac of allowCapturing) TS_ASSERT_EQUALS(cmpAttack.GetBestAttackAgainst(defender, ac), bestAttack); }); } testGetBestAttackAgainst("FemaleCitizen", "Melee"); testGetBestAttackAgainst("Archer", "Ranged"); testGetBestAttackAgainst("Domestic", "Slaughter"); testGetBestAttackAgainst("Structure", "Capture", true); function testPredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity) { ResetState(); let cmpAttack = ConstructComponent(1, "Attack", {}); let timeToTarget = cmpAttack.PredictTimeToTarget(selfPosition, horizSpeed, targetPosition, targetVelocity); if (timeToTarget === false) return; // Position of the target after that time. let targetPos = Vector3D.mult(targetVelocity, timeToTarget).add(targetPosition); // Time that the projectile need to reach it. let time = targetPos.horizDistanceTo(selfPosition) / horizSpeed; TS_ASSERT_EQUALS(timeToTarget.toFixed(1), time.toFixed(1)); } testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(0, 0, 0), new Vector3D(0, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(16, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-16, 0, 0)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 4)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(0, 0, 16)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(1, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(2, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(8, 0, 8)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-1, 0, 1)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-2, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-8, 0, 8)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(4, 0, 2)); testPredictTimeToTarget(new Vector3D(0, 0, 0), 4, new Vector3D(20, 0, 0), new Vector3D(-4, 0, 2)); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js (revision 20133) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js (revision 20134) @@ -1,72 +1,74 @@ Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Damage.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("DeathDamage.js"); let deadEnt = 60; let player = 1; ApplyValueModificationsToEntity = function(value, stat, ent) { if (value == "DeathDamage/Pierce" && ent == deadEnt) return stat + 200; return stat; }; let template = { "Shape": "Circular", "Range": 10.7, "FriendlyFire": "false", "Hack": 0.0, "Pierce": 15.0, "Crush": 35.0 }; let modifiedDamage = { "hack": 0.0, "pierce": 215.0, "crush": 35.0 }; let cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template); let playersToDamage = [2, 3, 7]; let pos = new Vector2D(3, 4.2); let result = { "attacker": deadEnt, "origin": pos, "radius": template.Range, "shape": template.Shape, "strengths": modifiedDamage, "splashBonus": null, "playersToDamage": playersToDamage, "type": "Death", "attackerOwner": player }; AddMock(SYSTEM_ENTITY, IID_Damage, { "CauseSplashDamage": data => TS_ASSERT_UNEVAL_EQUALS(data, result), "GetPlayersToDamage": (owner, friendlyFire) => playersToDamage }); AddMock(deadEnt, IID_Position, { "GetPosition2D": () => pos, "IsInWorld": () => true }); AddMock(deadEnt, IID_Ownership, { "GetOwner": () => player }); TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage); cmpDeathDamage.CauseDeathDamage(); +// Test splash damage bonus let splashBonus = { "BonusCav": { "Classes": "Cavalry", "Multiplier": 3 } }; -cmpDeathDamage.template.Bonuses = splashBonus; +template.Bonuses = splashBonus; +cmpDeathDamage = ConstructComponent(deadEnt, "DeathDamage", template); result.splashBonus = splashBonus; TS_ASSERT_UNEVAL_EQUALS(cmpDeathDamage.GetDeathDamageStrengths(), modifiedDamage); cmpDeathDamage.CauseDeathDamage(); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js (revision 20133) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js (revision 20134) @@ -1,174 +1,172 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Engine.LoadComponentScript("interfaces/RallyPoint.js"); const garrisonedEntitiesList = [25, 26, 27, 28, 29, 30, 31, 32, 33]; const garrisonHolderId = 15; const unitToGarrisonId = 24; const enemyUnitId = 34; const player = 1; const friendlyPlayer = 2; const enemyPlayer = 3; AddMock(garrisonHolderId, IID_Footprint, { "PickSpawnPointBothPass": entity => new Vector3D(4, 3, 30), "PickSpawnPoint": entity => new Vector3D(4, 3, 30) }); AddMock(garrisonHolderId, IID_Ownership, { "GetOwner": () => player }); AddMock(garrisonHolderId, IID_Health, { "GetHitpoints": () => 50, "GetMaxHitpoints": () => 600 }); AddMock(player, IID_Player, { "IsAlly": id => true, "IsMutualAlly": id => true, "GetPlayerID": () => player }); AddMock(friendlyPlayer, IID_Player, { "IsAlly": id => true, "IsMutualAlly": id => true, "GetPlayerID": () => friendlyPlayer }); AddMock(SYSTEM_ENTITY, IID_Timer, { "SetTimeout": (ent, iid, funcname, time, data) => 1 }); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => id }); for (let i = 24; i <= 34; ++i) { AddMock(i, IID_Identity, { "GetClassesList": () => "Infantry+Cavalry", "GetSelectionGroupName": () => "mace_infantry_archer_a" }); if (i < 28) AddMock(i, IID_Ownership, { "GetOwner": () => player }); else if (i == 34) AddMock(i, IID_Ownership, { "GetOwner": () => enemyPlayer }); else AddMock(i, IID_Ownership, { "GetOwner": () => friendlyPlayer }); AddMock(i, IID_Garrisonable, {}); AddMock(i, IID_Position, { "GetHeightOffset": () => 0, "GetPosition": () => new Vector3D(4, 3, 25), "GetRotation": () => new Vector3D(4, 0, 6), "GetTurretParent": () => INVALID_ENTITY, "IsInWorld": () => true, "JumpTo": (posX, posZ) => {}, "MoveOutOfWorld": () => {}, "SetTurretParent": (entity, offset) => {}, "SetHeightOffset": height => {} }); } AddMock(33, IID_Identity, { "GetClassesList": () => "Infantry+Cavalry", "GetSelectionGroupName": () => "spart_infantry_archer_a" }); let cmpGarrisonHolder = ConstructComponent(garrisonHolderId, "GarrisonHolder", { "Max": 10, "List": { "_string": "Infantry+Cavalry" }, "EjectHealth": 0.1, "EjectClassesOnDestroy": { "_string": "Infantry" }, "BuffHeal": 1, "LoadingRange": 2.1, "Pickup": false, "VisibleGarrisonPoints": { "archer1": { "X": 12, "Y": 5, "Z": 6 }, "archer2": { "X": 15, "Y": 5, "Z": 6 } } }); cmpGarrisonHolder.AllowGarrisoning(true, "callerID1"); cmpGarrisonHolder.AllowGarrisoning(false, 5); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsGarrisoningAllowed(), false); cmpGarrisonHolder.AllowGarrisoning(true, 5); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsGarrisoningAllowed(), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetLoadingRange(), { "max": 2.1, "min": 0 }); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetHealRate(), 1); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetAllowedClasses(), "Infantry+Cavalry"); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetCapacity(), 10); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 0); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(unitToGarrisonId), false); -cmpGarrisonHolder.template.Pickup = true; -TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsAllowedToGarrison(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.HasEnoughHealth(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.PerformGarrison(unitToGarrisonId), false); AddMock(garrisonHolderId, IID_Health, { "GetHitpoints": () => 600, "GetMaxHitpoints": () => 600 }); TS_ASSERT_EQUALS(cmpGarrisonHolder.HasEnoughHealth(), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(enemyUnitId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Eject(unitToGarrisonId), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(unitToGarrisonId), true); for (let entity of garrisonedEntitiesList) TS_ASSERT_EQUALS(cmpGarrisonHolder.Garrison(entity), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.CanPickup(unitToGarrisonId), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadTemplate("spart_infantry_archer_a", 2, false, false), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 25, 26, 27, 28, 29, 30, 31, 32]); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAllByOwner(friendlyPlayer, false), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 25, 26, 27]); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 4); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsEjectable(25), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.Unload(25), true); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsEjectable(25), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.PerformEject([25], false), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.PerformEject([], false), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), [24, 26, 27]); TS_ASSERT_EQUALS(cmpGarrisonHolder.GetGarrisonedEntitiesCount(), 3); TS_ASSERT_EQUALS(cmpGarrisonHolder.IsFull(), false); TS_ASSERT_EQUALS(cmpGarrisonHolder.UnloadAll(), true); TS_ASSERT_UNEVAL_EQUALS(cmpGarrisonHolder.GetEntities(), []); Index: ps/trunk/source/simulation2/components/tests/test_scripts.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 20133) +++ ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 20134) @@ -1,85 +1,86 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "simulation2/system/ComponentTest.h" #include "ps/Filesystem.h" class TestComponentScripts : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(20 * MiB); g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST); g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); } static void load_script(const ScriptInterface& scriptInterface, const VfsPath& pathname) { CVFSFile file; TS_ASSERT_EQUALS(file.Load(g_VFS, pathname), PSRETURN_OK); CStr content = file.DecodeUTF8(); // assume it's UTF-8 TSM_ASSERT(L"Running script "+pathname.string(), scriptInterface.LoadScript(pathname, content)); } static void Script_LoadComponentScript(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/components") / pathname)); } static void Script_LoadHelperScript(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } void test_scripts() { if (!VfsFileExists(L"simulation/components/tests/setup.js")) { debug_printf("WARNING: Skipping component scripts tests (can't find binaries/data/mods/public/simulation/components/tests/setup.js)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths)); + paths.push_back(VfsPath(L"simulation/components/tests/setup_test.js")); for (const VfsPath& path : paths) { CSimContext context; CComponentManager componentManager(context, g_ScriptRuntime, true); ScriptTestSetup(componentManager.GetScriptInterface()); componentManager.GetScriptInterface().RegisterFunction ("LoadComponentScript"); componentManager.GetScriptInterface().RegisterFunction ("LoadHelperScript"); componentManager.LoadComponentTypes(); load_script(componentManager.GetScriptInterface(), L"simulation/components/tests/setup.js"); load_script(componentManager.GetScriptInterface(), path); } } };