Skip to content

Instantly share code, notes, and snippets.

@leberechtreinhold
Last active April 15, 2021 01:36
Show Gist options
  • Save leberechtreinhold/d74fbc42dc3ac1d6b1d954c47347c861 to your computer and use it in GitHub Desktop.
Save leberechtreinhold/d74fbc42dc3ac1d6b1d954c47347c861 to your computer and use it in GitHub Desktop.
Script for DBA in Tabletop Simulator. WIP.
---------------------------------------------------------------------------------------------------------
--
-- LOGGING
--
---------------------------------------------------------------------------------------------------------
-- Colors to avoid having to calculate each time on a function
white = { r = 1, g = 1, b = 1}
grey = { r = 0.8, g = 0.8, b = 0.8}
red = { r = 1, g = 0.2, b = 0.1}
-- Prints ONLY to the host, should never be in release builds
function print_debug(msg)
print('[DEBUG] ' .. msg)
end
-- Messages that can be seen by everyone on the textbox
function print_info(msg)
printToAll('[INFO] ' .. msg, grey)
end
-- Messages that can be seen by everyone on the textbox and indicate
-- that the user may be doing something wrong
function print_error(msg)
printToAll('[ERROR] ' .. msg, red)
end
-- Message that will appear TO EVERYONE ON THE SCREEN.
function print_important(msg)
broadcastToAll('[IMPORTANT] ' .. msg, grey)
end
---------------------------------------------------------------------------------------------------------
--
-- UTILITIES
--
---------------------------------------------------------------------------------------------------------
-- Given two tables with x,y,z numerical components, computes the dot product of them
function vec_dot_product(vec1, vec2)
return { x = vec1['x'] * vec2['x'], y = vec1['y'] * vec2['y'], z = vec1['z'] * vec2['z'] }
end
-- Given two tables with x,y,z numerical components, computes the sum of both
function vec_add(vec1, vec2)
return { x = vec1['x'] + vec2['x'], y = vec1['y'] + vec2['y'], z = vec1['z'] + vec2['z'] }
end
-- Given two tables with x,y,z numerical components, computes the vec1-vec2
function vec_sub(vec1, vec2)
return { x = vec1['x'] - vec2['x'], y = vec1['y'] - vec2['y'], z = vec1['z'] - vec2['z'] }
end
-- Given a table with x,y,z numerical components, and a escalar number, returns a vector with each component multiplied
function vec_mul_escalar(vec, num)
return { x = vec['x'] * num, y = vec['y'] * num, z = vec['z'] * num }
end
-- Given a table with x,y,z numerical components, and a escalar number, returns a vector with each component divided
function vec_div_escalar(vec, num)
return { x = vec['x'] / num, y = vec['y'] / num, z = vec['z'] / num }
end
-- Given a table with x,y,z numerical components representing inches, return the same vector with each component being in mm
function vec_in_to_mm(vec)
return { x = from_in_to_mm(vec['x']), y = from_in_to_mm(vec['y']), z = from_in_to_mm(vec['z']) }
end
-- Given a tables with x,y,z numerical components, returns a [x,y,z] string with two decimals of precision
function vec_to_str(vec)
return '[' .. string.format('%.2f',vec['x']) .. ', ' .. string.format('%.2f',vec['y']) .. ', ' .. string.format('%.2f',vec['z']) .. ']'
end
-- As insane as it sounds, tables in lua don't have a well-defined way of getting the number of entries
-- If the table is anything but a contiguous array, the # operator is useless. This computes that.
-- Beware that this iterates the whole table and is therefore, perf intensive.
function tlen(table)
local n = 0
for _ in pairs(table) do n = n + 1 end
return n
end
-- Given a tables with x,y,z each with a degree number [0-360], returns a table with x,y,z converted to radians (o, 2pi)
function from_degrees_to_rad(vec)
return { x = math.rad(vec['x']), y = math.rad(vec['y']), z = math.rad(vec['z']) }
end
function from_in_to_mm(inches)
return inches * 25.4
end
-- Given two tables with x,y,z representing world coords, calculates the distance between them in x,z, SQUARED
-- This is because we don't need the square root in most cases
function distance_points_flat_sq(point1, point2)
return point1['x'] - point2['x'] + point1['z'] - point2['z']
end
-- Given a number of radians (0, 2pi), returns a table with x,y,z components,
-- where y 0 is always 0 and x,z is the rotation value corresponding to those
-- radians, x being updown and z leftright
function rad_to_vector(radians)
return { x = math.sin(radians), y = 0, z = math.cos(radians) }
end
-- Angles in TTS can be pretty funny: the y axis defines the rotation from +x to
-- -y to -x to +y, which is pretty unintuitive, but I guess they wanted the
-- degrees to be clockwise instead of counterclockwise...
-- This makes them counterclockwise, x goes to -y to -x
function normalize_angle(angle)
return 2*math.pi - angle
end
-- Given a point with xyz coordinates, where xz form a plane, rotates using
-- an angle theta, respective to a coordinate system with xyz coordinates,
-- on that same plane (leaving y untouched)
function rotate_point(point, center_coordinates, theta)
theta = normalize_angle(theta)
return { x = point['x'] * math.cos(theta) - point['z'] * math.sin(theta) + center_coordinates['x'],
y = point['y'] + center_coordinates['y'],
z = point['x'] * math.sin(theta) + point['z'] * math.cos(theta) + center_coordinates['z']}
end
-- Given a table with four corners top/bot left/right, each with a xyz
-- vector representing coordinates in inches, returns the same table but
-- each coords is in mm
function corners_in_to_mm(corner)
return {
topright = vec_in_to_mm(corner['topright']),
botright = vec_in_to_mm(corner['botright']),
topleft = vec_in_to_mm(corner['topleft']),
botleft = vec_in_to_mm(corner['botleft'])
}
end
-- Given a table with four corners top/bot left/right, each with a xyz
-- vector representing coordinates, returns a str version of it
-- { corner = [coords], corner = [coords], ...}
function corners_to_str(corner)
return '{' ..
'topright=' .. vec_to_str(corner['topright']) .. ', ' ..
'botright=' .. vec_to_str(corner['botright']) .. ', ' ..
'topleft=' .. vec_to_str(corner['topleft']) .. ', ' ..
'botleft=' .. vec_to_str(corner['botleft']) .. '}'
end
-- Rounds a number to the power of ten given
-- For example, round_to_power(123, 1) => 120, round_to_power(123, 2) => 100
function round_to_power(number, power)
return math.floor(number/(10^power) + 0.5) * 10^power
end
---------------------------------------------------------------------------------------------------------
--
-- BASE FUNCTIONALITY
--
---------------------------------------------------------------------------------------------------------
-- Given a base object, computes the 4 bounds points, returned in a table,
-- each with a vector xyz of world pos coords
--
-- topleft rotation topright
-- +-------------^--------------+
-- | | |
-- | * center | z axis
-- | |
-- +----------------------------+
-- botleft x axis botright
function compute_corners_base(base_obj)
local bounds = base_obj.getBounds()
local rotation = from_degrees_to_rad(base_obj.getRotation())['y']
print_debug(base_obj.getName() .. ' rotation is ' .. rotation)
local size = bounds['size']
local pos = base_obj.getPosition()
print_debug(base_obj.getName() .. ' pos is ' .. vec_to_str(vec_in_to_mm(pos)))
local xhalf = size['x'] / 2
local zhalf = size['z'] / 2
return {
topright = rotate_point({x = xhalf, y = 0, z = zhalf}, pos, rotation),
botright = rotate_point({x = xhalf, y = 0, z =-zhalf}, pos, rotation),
topleft = rotate_point({x =-xhalf, y = 0, z = zhalf}, pos, rotation),
botleft = rotate_point({x =-xhalf, y = 0, z =-zhalf}, pos, rotation)
}
end
-- Given two base objects, aligns them so they share on side, the closest
-- base2 will align to base1, and it's assumed that base1 is to the left of
-- base2 (that is, the X component is smaller)
-- TODO this only aligns with the right side atm
function align_two_bases(player, base1, base2)
if (distance_points_flat_sq(base1.getPosition(), base2.getPosition()) > 9) then
print(player.steam_name .. ' is trying to align but the bases are too far apart, more than 3inch between centers!')
return
end
print(player.steam_name .. ' is aligning ' .. base1.getName() .. ' with ' .. base2.getName())
base2.setRotation(base1.getRotation())
local corners1 = compute_corners_base(base1)
print_debug('CORNERS ' .. base1.getName() .. corners_to_str(corners_in_to_mm(corners1)))
local corners2 = compute_corners_base(base2)
print_debug('CORNERS ' .. base2.getName() .. corners_to_str(corners_in_to_mm(corners2)))
local translation = vec_sub(corners1['topright'], corners2['topleft'])
print_debug('Translation is ' .. vec_to_str(vec_in_to_mm(translation)))
base2.setPosition(vec_add(base2.getPosition(), translation)) -- false, false)
print_info(base2.getName() .. ' has aligned to ' .. base1.getName())
end
-- Checks if the given str starts with substr
function str_starts_with(str, substr)
return string.find(str, '^' .. substr) ~= nil
end
-- Given a list of objects in a table, returns another table with ONLY
-- those who start with "base", ignoring the keys
function filter_bases(list)
local filtered = {}
for _,obj in ipairs(list) do
if str_starts_with(obj.getName(), 'base') then
table.insert(filtered, obj)
end
end
return filtered
end
---------------------------------------------------------------------------------------------------------
--
-- UI EVENTS
--
---------------------------------------------------------------------------------------------------------
-- Global number of paces moved
g_paces_movement = 100
-- Updates the global that manages the number of paces moved by the other functions, and updates the UI
function slider_paces_changed(player, value, id)
g_paces_movement = round_to_power(value, 1)
-- It's undocumented, but changing the value of the button does not update the button_move_forward
-- Instead we have to change the undocumented text attribute. However, we still, need to
UI.setAttribute('button_move_forward', 'text', 'Move ' .. g_paces_movement .. ' paces')
UI.setValue('button_move_forward', 'text', 'Move ' .. g_paces_movement .. ' paces')
end
-- Moves one or more DBA bases 100 paces (1 inch) forward
-- ASSUMES all bases are in a flat board!!!!
function move_bases(player, value, id)
local objs = filter_bases(player.getSelectedObjects())
if tlen(objs) < 1 then
print_error(player.steam_name ..' is trying to move 100 paces, but (s)he has no object selected, ignoring')
return
end
for k,obj in ipairs(objs) do
local current_world_pos = obj.getPosition()
local current_rotation = from_degrees_to_rad(obj.getRotation())
local displacement_vector = rad_to_vector(current_rotation['y'])
local magnitude = g_paces_movement / 100
local destination = current_world_pos + vec_mul_escalar(displacement_vector, magnitude)
-- print_debug(player.steam_name .. 'Moving ' .. obj.getName() ..
-- ' from ' .. vec_to_str(current_world_pos) ..
-- ' with rotation ' .. vec_to_str(current_rotation) ..
-- ' to ' .. vec_to_str(destination))
print_info(player.steam_name .. ' is moving ' .. obj.getName() .. ' ' .. magnitude .. 'in forward')
obj.setPosition(destination)
-- TODO: COLISION
end
end
function align_bases(player, value, id)
local objs = filter_bases(player.getSelectedObjects())
local n_objs = tlen(objs)
if n_objs < 2 then
print_error(player.steam_name ..' is trying to align ' .. n_objs .. ' bases, which is not supported')
return
end
table.sort(objs, function(l, r)
return l.getPosition()['x'] < r.getPosition()['x']
end)
for i=1,n_objs-1 do
align_two_bases(player, objs[i], objs[i + 1])
end
end
-- TODO This only summons a empire soldier..., should create menus and stuff
function create_army_player_red()
local obj = spawnObject({
type = 'Custom_Model',
position = { x = 1, y = 1.36, z = 0},
rotation = { x = 0, y = 0, z = 0},
scale = { x = 1, y = 1, z = 1},
sound = false,
snap_to_grid = false
})
obj.setCustomObject({
mesh = 'http://cloud-3.steamusercontent.com/ugc/1022822649627337225/1437C085D4FFF74C2FBF2286FAFA4C555FA9AAAA/',
diffuse = 'http://cloud-3.steamusercontent.com/ugc/1022822649627341435/6849CB89095D43654125ED506222AF73172C09C1/',
material = 1
})
end
function create_army_player_blue()
local obj = spawnObject({
type = 'Custom_Model',
position = { x = 0, y = 1.36, z = 0},
rotation = { x = 0, y = 0, z = 0},
scale = { x = 1, y = 1, z = 1},
sound = false,
snap_to_grid = false
})
obj.setCustomObject({
mesh = 'http://cloud-3.steamusercontent.com/ugc/1022822649627337225/1437C085D4FFF74C2FBF2286FAFA4C555FA9AAAA/',
diffuse = 'http://cloud-3.steamusercontent.com/ugc/1022822649627338507/BBA5455E7C52A92432746B828602DFAEB68142D2/',
material = 1
})
end
function onload()
print('-----------------------------------')
end
<Panel id="dba_panel" class="WindowBorder" width="200" height="400" color="#FFFFFF" outline="#000" outlineSize="2" rectAlignment="MiddleRight" >
<VerticalLayout>
<Slider id="slider_paces" minValue="10" maxValue="500" wholeNumbers="true" value="100" colors="#00000000|#00000000|#00000000|#00000000" backgroundcolor="#FFFFFFF" onValueChanged="slider_paces_changed"></Slider>
<Button id="button_move_forward" onClick="move_bases">Move 100 paces</Button>
<Button onClick="align_bases">Align bases</Button>
<Button onClick="create_army_player_blue">Spawn army player 1</Button>
<Button onClick="create_army_player_red">Spawn army player 2</Button>
</VerticalLayout>
</Panel>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment