Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions luarules/gadgets/api_missions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,16 @@ function gadget:Initialize()
--local scriptPath = 'mission-api-tests/validation_test.lua'
--local scriptPath = 'mission-api-tests/test_mission.lua'
--local scriptPath = 'mission-api-tests/markers_test.lua'
local scriptPath = 'mission-api-tests/sound_test.lua'
--local scriptPath = 'mission-api-tests/sound_test.lua'
--local scriptPath = 'mission-api-tests/issue_orders_test.lua'
--local scriptPath = 'mission-api-tests/unit_triggers_test.lua'
--local scriptPath = 'mission-api-tests/feature_triggers_test.lua'
--local scriptPath = 'mission-api-tests/statistics_triggers_test.lua'
--local scriptPath = 'mission-api-tests/resource_test.lua'
--local scriptPath = 'mission-api-tests/loadout_test.lua'
--local scriptPath = 'mission-api-tests/stages_and_objectives_test.lua'

local scriptPath = 'mission-api-tests/unit_mover_test.lua'

if not scriptPath then
gadgetHandler:RemoveGadget()
return
Expand Down
32 changes: 32 additions & 0 deletions luarules/mission_api/actions/move_units.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
local ParameterTypes = GG['MissionAPI'].Modules.ParameterTypes.Types

local function moveUnits(unitName, position, direction, randomRadius)
local tracking = GG['MissionAPI'].Modules.Tracking
if tracking.IsUnitNameUntracked(unitName) then return end

local trackedUnitIDs = GG['MissionAPI'].trackedUnitIDs[unitName]

for unitID in pairs(trackedUnitIDs) do
if Spring.GetUnitIsDead(unitID) == false then
Spring.SetUnitPosition(unitID, position.x, position.y, position.z)
if direction then
local testposx, _, testposz = Spring.GetUnitPosition(unitID)
if math.abs(direction.x-testposx) > 0.001 or math.abs(direction.z-testposz) > 0.001 then
Spring.SetUnitDirection(unitID, direction.x-testposx, direction.y, direction.z-testposz)
end
end
end
end
end

return {
{
type = 'MoveUnits',
parameters = {
{ name = 'unitName', required = true, type = ParameterTypes.UnitName },
{ name = 'position', required = true, type = ParameterTypes.Position },
{ name = 'direction', required = false, type = ParameterTypes.Position }, -- Point on the map towards which the unit rotates
},
actionFunction = moveUnits,
},
}
32 changes: 32 additions & 0 deletions luarules/mission_api/actions/rotate_units.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
local ParameterTypes = GG['MissionAPI'].Modules.ParameterTypes.Types



local function rotateUnits(unitName, direction)
local tracking = GG['MissionAPI'].Modules.Tracking
if tracking.IsUnitNameUntracked(unitName) then return end

local trackedUnitIDs = GG['MissionAPI'].trackedUnitIDs[unitName]

for unitID in pairs(trackedUnitIDs) do
if Spring.GetUnitIsDead(unitID) == false then
if direction then
local testposx, _, testposz = Spring.GetUnitPosition(unitID)
Comment thread
Damgam marked this conversation as resolved.
if math.abs(direction.x-testposx) >= 0.001 or math.abs(direction.z-testposz) >= 0.001 then
Spring.SetUnitDirection(unitID, direction.x-testposx, direction.y, direction.z-testposz)
end
end
end
end
end

return {
{
type = 'RotateUnits',
parameters = {
{ name = 'unitName', required = true, type = ParameterTypes.UnitName },
{ name = 'direction', required = true, type = ParameterTypes.Position }, -- Point on the map towards which the unit rotates

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using such an indirect method of specifying rotation? Shouldn't this just be 0°-360°?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I missed hitting the submit review button, I thought this had posted.

@Damgam Damgam Aug 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that's the only way Spring/Recoil provides as far as I can tell. The other "rotation" thing seem to be for aircrafts only from my tests.

But also if you think about it, it's actually very user friendly. "You sit here, look at that position." instead of thinking how many degrees it's gonna be.

@sprunk sprunk Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WatchTheFort you have to be more explicit, damgam doesn't intuitively know that an angle is equivalent to a class of vectors so you'll have to give him a hint on how to derive a vector (or alternatively give him a link to the docs for the other Spring function which does accept an angle).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think this is better than explicitly giving it degrees, but whatever.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My 2c:

  • you can have both "rotate towards point" and "rotate angle" functions
  • Damgam is correct that "rotate towards point" is useful, and IME becomes more so the more interesting your triggers are
  • reconsider bundling either of these with the "move units" action, it's trivial to call one's preferred variant separately after the move

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can overload the parameter here, if a single value is passed, that is an angle, if a coordinate pair is passed, that's a direction vector.

@sprunk sprunk Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if you come up with more variants later?

  • rotate to a world-relative angle (e.g. 90' means towards North)
  • rotate to a unit-relative angle (e.g. 90' means turn left)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can overload the parameter here, if a single value is passed, that is an angle, if a coordinate pair is passed, that's a direction vector.

How do you feel about the face-toward-point parameter, then?

@efrec efrec Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* you can have both "rotate towards point" and "rotate angle" functions

Sounds good

* Damgam is correct that "rotate towards point" is useful, and IME becomes more so the more interesting your triggers are

I agree

* reconsider bundling either of these with the "move units" action, it's trivial to call one's preferred variant separately after the move

Is there a case where a rectangular def might need both to move and rotate at once?

edit: or rotate-then-move, e.g. if a building will flatten terrain

},
actionFunction = rotateUnits,
},
}
110 changes: 110 additions & 0 deletions singleplayer/mission-api-tests/unit_mover_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
local triggerTypes = GG['MissionAPI'].TriggerDefinitions.Types
local actionTypes = GG['MissionAPI'].ActionDefinitions.Types

local triggers = {
spawnTanks = {
type = triggerTypes.TimeElapsed,
parameters = {
seconds = 3,
},
actions = { 'spawnTanks' },
},

teleportTanksWithoutSetDirection = {
type = triggerTypes.TimeElapsed,
parameters = {
seconds = 5,
},
actions = { 'teleportTanksWithoutSetDirection1' },
},

teleportTanksWithSetDirection1 = {
type = triggerTypes.TimeElapsed,
parameters = {
seconds = 10,
},
actions = { 'teleportTanksWithSetDirection1' },
},

teleportTanksWithSetDirection2 = {
type = triggerTypes.TimeElapsed,
parameters = {
seconds = 15,
},
actions = { 'teleportTanksWithSetDirection2' },
},

teleportTanksWithoutSetDirection2 = {
type = triggerTypes.TimeElapsed,
parameters = {
seconds = 25,
},
actions = { 'teleportTanksWithoutSetDirection1' },
},
}

local actions = {

spawnTanks = {
type = actionTypes.SpawnUnits,
parameters = {
unitLoadout = {
{
unitDefName = 'armstump',
x = 2300,
z = 1900,
team = 0,
unitName = 'tanks',
quantity = 10
},
},
},
},

teleportTanksWithoutSetDirection1 = {
type = actionTypes.MoveUnits,
parameters = {
unitName = "tanks",
position = {
x = 2500,
z = 2000,
},
},
},

teleportTanksWithSetDirection1 = {
type = actionTypes.MoveUnits,
parameters = {
unitName = "tanks",
position = {
x = 2500,
z = 2000,
},
direction = {
x = 3000,
z = 3000,
},
},
},

teleportTanksWithSetDirection2 = {
type = actionTypes.MoveUnits,
parameters = {
unitName = "tanks",
position = {
x = 2500,
z = 2000,
},
direction = {
x = 1000,
z = 1000,
},
},
},

}

return {
Triggers = triggers,
Actions = actions,
}
Loading