Last active
April 15, 2021 01:36
-
-
Save leberechtreinhold/d74fbc42dc3ac1d6b1d954c47347c861 to your computer and use it in GitHub Desktop.
Script for DBA in Tabletop Simulator. WIP.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--------------------------------------------------------------------------------------------------------- | |
-- | |
-- 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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