Changeset View
Standalone View
binaries/data/mods/public/maps/scripts/CaptureTheRelic.js
Property | Old Value | New Value |
---|---|---|
svn:eol-style | null | native \ No newline at end of property |
let g_CatafalqueTemplate = "other/special_catafalque"; | |||||
elexis: All of the functions look like they could easily be extended if we intend to implement the… | |||||
Not Done Inline ActionsThis file leaves a clean impression, good work! elexis: This file leaves a clean impression, good work! | |||||
Trigger.prototype.InitCaptureTheRelic = function() | |||||
{ | |||||
// Attempt to spawn relics using gaia entities in neutral territory | |||||
// If there are none, try to spawn using gaia entities in non-gaia territory | |||||
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); | |||||
let cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); | |||||
let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); | |||||
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); | |||||
let potentialGaiaSpawnPoints = []; | |||||
let potentialSpawnPoints = cmpRangeManager.GetEntitiesByPlayer(0).filter(entity => { | |||||
let cmpPosition = Engine.QueryInterface(entity, IID_Position); | |||||
if (!cmpPosition || !cmpPosition.IsInWorld()) | |||||
return false; | |||||
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); | |||||
if (!cmpIdentity) | |||||
return false; | |||||
let templateName = cmpTemplateManager.GetCurrentTemplateName(entity); | |||||
if (!templateName) | |||||
return false; | |||||
let template = cmpTemplateManager.GetTemplate(templateName); | |||||
if (!template || template.UnitMotionFlying) | |||||
return false; | |||||
Not Done Inline ActionsNot triggered by hawks as they don't have Identity. Tested with acropolis bay with gaia planes and test works. elexis: Not triggered by hawks as they don't have Identity. Tested with acropolis bay with gaia planes… | |||||
let pos = cmpPosition.GetPosition(); | |||||
if (pos.y <= cmpWaterManager.GetWaterLevel(pos.x, pos.z)) | |||||
return false; | |||||
Not Done Inline ActionsTested on cycladic archipelago. Some semi submerged berries and stone mines were detected too. elexis: Tested on cycladic archipelago. Some semi submerged berries and stone mines were detected too. | |||||
if (cmpTerritoryManager.GetOwner(pos.x, pos.z) == 0) | |||||
potentialGaiaSpawnPoints.push(entity); | |||||
return true; | |||||
}); | |||||
if (potentialGaiaSpawnPoints.length) | |||||
potentialSpawnPoints = potentialGaiaSpawnPoints; | |||||
Not Done Inline ActionsEven if it's only one, that one will be used N times, so it works for all maps elexis: Even if it's only one, that one will be used N times, so it works for all maps | |||||
// Try to spread the spawned relics fairly evenly across the map | |||||
let pickSpawnPoint = spawnPoints => { | |||||
Done Inline ActionsWhen testing I encountered a mapgen where 2 relics were spawned right beside each other near one players base. It were preferable if the distance between the avilable spawn points is maximized. Doesn't have to be the actual maximum possible (as that might require brute forcing all solutions, NP-hard issue iirc), a simple algorithm should be sufficient. Maybe picking one location, then picking the next location that has the greatest distance to all other locations? elexis: When testing I encountered a mapgen where 2 relics were spawned right beside each other near… | |||||
Not Done Inline ActionsMaximizing the distance between spawn points causes the entities to be spawned in extreme corners of the map, in an extremely predictable pattern, so this is not preferable, but yes, using something other than purely random selection is a good idea. The updated diff takes into account the position of already-placed relics when spawning them, and also uses some randomness. Sandarac: Maximizing the distance between spawn points causes the entities to be spawned in extreme… | |||||
let chosenSpawnPoint; | |||||
let maxDist = 0; | |||||
for (let potentialSpawnPoint of potentialSpawnPoints) | |||||
{ | |||||
Not Done Inline ActionsIn case it actually doesn't pick the max anymore, a shuffleArray would make things independent of entity ID order elexis: In case it actually doesn't pick the max anymore, a shuffleArray would make things independent… | |||||
let totalDist = 0; | |||||
Not Done Inline ActionsD230 if you wan't to add the foundation for generic AI compatibility elexis: D230 if you wan't to add the foundation for generic AI compatibility | |||||
Not Done Inline ActionsAs D230 is not complete, cmpDamageReceiver.SetInvulnerability(true); can be done for now. Sandarac: As D230 is not complete, `cmpDamageReceiver.SetInvulnerability(true);` can be done for now. | |||||
let potentialSpawnPointPos = Engine.QueryInterface(potentialSpawnPoint, IID_Position).GetPosition(); | |||||
// Fudge the exact numbers slightly so relics aren't spawned in the same positions every time | |||||
for (let point of spawnPoints) | |||||
totalDist += DistanceBetweenEntities(potentialSpawnPoint, point) / 2 * randFloat(0.2, 0.5); | |||||
if (totalDist < maxDist) | |||||
continue; | |||||
maxDist = totalDist; | |||||
chosenSpawnPoint = potentialSpawnPoint; | |||||
} | |||||
return chosenSpawnPoint; | |||||
} | |||||
let numSpawnedRelics = Math.ceil(TriggerHelper.GetNumberOfPlayers() / 2); | |||||
this.playerRelicsCount = new Array(TriggerHelper.GetNumberOfPlayers()).fill(0, 1); | |||||
Not Done Inline ActionsThat , 1 not really needed, might be easier to read without, doesn't matter though elexis: That , 1 not really needed, might be easier to read without, doesn't matter though | |||||
Not Done Inline ActionsHope to see someone accomplishing the impossible and triggering that case with some trick elexis: Hope to see someone accomplishing the impossible and triggering that case with some trick | |||||
this.playerRelicsCount[0] = numSpawnedRelics; | |||||
let spawnPoints = []; | |||||
for (let i = 0; i < numSpawnedRelics; ++i) | |||||
{ | |||||
let spawnEntity = i == 0 ? pickRandom(potentialSpawnPoints) : pickSpawnPoint(spawnPoints); | |||||
this.relics[i] = TriggerHelper.SpawnUnits(spawnEntity, g_CatafalqueTemplate, 1, 0)[0]; | |||||
let cmpPosition = Engine.QueryInterface(spawnEntity, IID_Position); | |||||
spawnPoints.push(spawnEntity); | |||||
let cmpDamageReceiver = Engine.QueryInterface(this.relics[i], IID_DamageReceiver); | |||||
cmpDamageReceiver.SetInvulnerability(true); | |||||
let cmpPositionRelic = Engine.QueryInterface(this.relics[i], IID_Position); | |||||
cmpPositionRelic.SetYRotation(randFloat(0, 2 * Math.PI)); | |||||
} | |||||
}; | |||||
Trigger.prototype.CheckCaptureTheRelicVictory = function(data) | |||||
{ | |||||
let cmpIdentity = Engine.QueryInterface(data.entity, IID_Identity); | |||||
if (!cmpIdentity || !cmpIdentity.HasClass("Relic") || data.from == -1) | |||||
return; | |||||
if (data.to == -1) | |||||
{ | |||||
error("Relic entity " + data.entity + " has been destroyed"); | |||||
return; | |||||
} | |||||
Not Done Inline ActionsNext time move that { to the next line elexis: Next time move that { to the next line | |||||
--this.playerRelicsCount[data.from]; | |||||
++this.playerRelicsCount[data.to]; | |||||
this.CheckCaptureTheRelicCountdown(); | |||||
}; | |||||
/** | |||||
* Check if an individual player or team has acquired all relics. | |||||
* Also check if the countdown needs to be stopped if a player/team no longer has all relics. | |||||
*/ | |||||
Trigger.prototype.CheckCaptureTheRelicCountdown = function() | |||||
{ | |||||
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); | |||||
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID) | |||||
{ | |||||
let playerAndAllies = cmpEndGameManager.GetAlliedVictory() ? | |||||
QueryPlayerIDInterface(playerID).GetMutualAllies() : [playerID]; | |||||
let teamRelicsOwned = 0; | |||||
for (let ally of playerAndAllies) | |||||
Not Done Inline ActionsSince this function isn't called from an ownership change / diplomacy change event but called manually from this prototype, there is no point to hide the actual two arguments in the data variable. isTeam is derived from data.to, so shouldn't be passed as an additional argument. Remotely refs https://en.wikipedia.org/wiki/Single_source_of_truth The entity doesn't need to be passed at all, since it isn't relevant which player is shown on the screen (with this diff it shows the one that captured the lowest entity ID entity). If it were to show the last one that captured something, that would have to be saved in a prototype grade variable to also work with diplomacy changes. So we end up with only passing playersAndAllies. elexis: Since this function isn't called from an ownership change / diplomacy change event but called… | |||||
teamRelicsOwned += this.playerRelicsCount[ally]; | |||||
Not Done Inline Actionsmessages var can be nuked elexis: messages var can be nuked | |||||
if (teamRelicsOwned == this.relics.length) | |||||
{ | |||||
this.StartCaptureTheRelicCountdown({ | |||||
"entity": this.relics[0], "to": playerAndAllies }, | |||||
playerAndAllies.length > 1); | |||||
return; | |||||
} | |||||
} | |||||
this.DeleteCaptureTheRelicVictoryMessages(); | |||||
}; | |||||
Not Done Inline Actionsplayers contains all players that are not in playersAndAllies and is being used for the otherMessage, so should be called others elexis: players contains all players that are not in playersAndAllies and is being used for the… | |||||
Trigger.prototype.DeleteCaptureTheRelicVictoryMessages = function() | |||||
{ | |||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | |||||
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); | |||||
for (let ent in this.relicsVictoryMessages) | |||||
{ | |||||
cmpGuiInterface.DeleteTimeNotification(this.relicsVictoryMessages[ent].ownMessage); | |||||
cmpGuiInterface.DeleteTimeNotification(this.relicsVictoryMessages[ent].otherMessage); | |||||
cmpTimer.CancelTimer(this.relicsVictoryTimers[ent]); | |||||
} | |||||
}; | |||||
Trigger.prototype.StartCaptureTheRelicCountdown = function(data, isTeam) | |||||
{ | |||||
let messages = this.relicsVictoryMessages[data.entity] || {}; | |||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | |||||
let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); | |||||
if (this.relicsVictoryTimers[data.entity]) | |||||
{ | |||||
cmpTimer.CancelTimer(this.relicsVictoryTimers[data.entity]); | |||||
cmpGuiInterface.DeleteTimeNotification(messages.ownMessage); | |||||
cmpGuiInterface.DeleteTimeNotification(messages.otherMessage); | |||||
} | |||||
let players = [-1]; | |||||
Not Done Inline Actions"and will have won" seems more natural. elexis: "and will have won" seems more natural.
Considered but rejected periods.
Also Gallaecio prefers… | |||||
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID) | |||||
{ | |||||
let cmpPlayer = QueryPlayerIDInterface(playerID); | |||||
if (cmpPlayer.GetState() == "won") | |||||
return; | |||||
if (data.to.indexOf(playerID) == -1) | |||||
players.push(playerID); | |||||
} | |||||
let cmpPlayer = QueryOwnerInterface(data.entity, IID_Player); | |||||
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); | |||||
let captureTheRelicDuration = cmpEndGameManager.GetGameTypeSettings().victoryDuration || 0; | |||||
messages.otherMessage = cmpGuiInterface.AddTimeNotification({ | |||||
"message": isTeam ? | |||||
Not Done Inline ActionsSince there can be only one countdown and one message per player at a time, those things shouldn't be arrays / objects, just a single atomic number / undefined variable elexis: Since there can be only one countdown and one message per player at a time, those things… | |||||
markForTranslation("%(player)s's team has captured all relics and will have won in %(time)s") : | |||||
markForTranslation("%(player)s has captured all relics and will have won in %(time)s"), | |||||
"players": players, | |||||
"parameters": { | |||||
"player": cmpPlayer.GetName() | |||||
}, | |||||
"translateMessage": true, | |||||
"translateParameters": [] | |||||
}, captureTheRelicDuration); | |||||
messages.ownMessage = cmpGuiInterface.AddTimeNotification({ | |||||
"message": isTeam ? | |||||
markForTranslation("Your team has captured all relics. You will have won in %(time)s") : | |||||
markForTranslation("You have captured all relics. You will have won in %(time)s"), | |||||
"players": data.to, | |||||
"translateMessage": true | |||||
}, captureTheRelicDuration); | |||||
this.relicsVictoryTimers[data.entity] = cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_EndGameManager, | |||||
"MarkPlayerAsWon", captureTheRelicDuration, data.to[0]); | |||||
this.relicsVictoryMessages[data.entity] = messages; | |||||
}; | |||||
{ | |||||
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); | |||||
cmpTrigger.relics = []; | |||||
cmpTrigger.playerRelicsCount = []; | |||||
cmpTrigger.relicsVictoryTimers = {}; | |||||
cmpTrigger.relicsVictoryMessages = {}; | |||||
cmpTrigger.DoAfterDelay(0, "InitCaptureTheRelic", {}); | |||||
cmpTrigger.RegisterTrigger("OnDiplomacyChanged", "CheckCaptureTheRelicCountdown", { "enabled": true }); | |||||
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckCaptureTheRelicVictory", { "enabled": true }); | |||||
cmpTrigger.RegisterTrigger("OnPlayerWon", "DeleteCaptureTheRelicVictoryMessages", { "enabled": true }); | |||||
} |
All of the functions look like they could easily be extended if we intend to implement the artifact pickup variant, so naming them Relic is in line with that.
g_CatafalqueTemplate though and special_relic -> special_catafalque