From 4ffab163030d92ddc00751e9e3bd2251a30d600b Mon Sep 17 00:00:00 2001 From: Perttu Ahola Date: Sat, 1 Apr 2023 18:17:00 +0300 Subject: [PATCH] Initial commit --- .gitignore | 3 + init.lua | 443 ++++++++++++++++++++++++++++ media/bulldozer_bulldozer.obj | 141 +++++++++ media/bulldozer_bulldozer_blade.png | Bin 0 -> 5088 bytes media/bulldozer_bulldozer_body.png | Bin 0 -> 5089 bytes media/bulldozer_bulldozer_track.png | Bin 0 -> 5702 bytes media/bulldozer_engine.ogg | Bin 0 -> 6517 bytes 7 files changed, 587 insertions(+) create mode 100644 .gitignore create mode 100644 init.lua create mode 100644 media/bulldozer_bulldozer.obj create mode 100644 media/bulldozer_bulldozer_blade.png create mode 100644 media/bulldozer_bulldozer_body.png create mode 100644 media/bulldozer_bulldozer_track.png create mode 100644 media/bulldozer_engine.ogg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..151e299 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.* +!.gitignore +*~ diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..f412f3d --- /dev/null +++ b/init.lua @@ -0,0 +1,443 @@ +-- Function to calculate the positions of nodes inside the box +-- box: A table containing yaw angle (in radians), dimensions, and the origin position +function get_nodes_in_box(box) + local yaw, dimensions, origin = box.yaw, box.dimensions, box.origin + local longitudinal, transverse, height = dimensions[1], dimensions[2], dimensions[3] + local nodes = {} + + local sin_yaw = math.sin(yaw) + local cos_yaw = math.cos(yaw) + + for x = -math.floor(longitudinal / 2), math.floor(longitudinal / 2) do + for y = 0, height - 1 do + for z = -math.floor(transverse / 2), math.floor(transverse / 2) do + local rotated_x = origin.x + (x * cos_yaw - z * sin_yaw) + local rotated_y = origin.y + y + local rotated_z = origin.z + (x * sin_yaw + z * cos_yaw) + + local node_pos = {x = math.floor(rotated_x), y = math.floor(rotated_y), z = math.floor(rotated_z)} + table.insert(nodes, node_pos) + end + end + end + + return nodes +end + +-- Function to get non-walkable nodes in the box +-- box: A table containing yaw angle (in radians), dimensions, and the origin position +function get_non_walkable_nodes_in_box(box) + local all_nodes = get_nodes_in_box(box) + local non_walkable_nodes = {} + + for _, node_pos in ipairs(all_nodes) do + local node = minetest.get_node(node_pos) + local node_def = minetest.registered_nodes[node.name] + if node_def and not node_def.walkable or node_def.drawtype ~= "normal" then + table.insert(non_walkable_nodes, node_pos) + end + end + + return non_walkable_nodes +end + +function get_walkable_nodes_in_box(box) + local all_nodes = get_nodes_in_box(box) + local walkable_nodes = {} + + for _, node_pos in ipairs(all_nodes) do + local node = minetest.get_node(node_pos) + local node_def = minetest.registered_nodes[node.name] + if node_def and node_def.walkable then + table.insert(walkable_nodes, node_pos) + end + end + + return walkable_nodes +end + +-- Function to create a virtual wall in front of the player and move nodes in front of the wall +function create_virtual_wall(player_pos_float, player_yaw, wall_dimensions, move_distance, + placement_height, placement_depth) + local player_pos = { + x = player_pos_float.x + 0.5, + y = player_pos_float.y + 0.5, + z = player_pos_float.z + 0.5, + } + + local wall_pos = { + x = player_pos.x + math.cos(player_yaw) * 0, + y = player_pos.y, + z = player_pos.z + math.sin(player_yaw) * 0, + } + + local wall_box = { + yaw = player_yaw, + dimensions = wall_dimensions, + origin = { + x = wall_pos.x + math.cos(player_yaw), + y = wall_pos.y + 1.5 - 1, + z = wall_pos.z + math.sin(player_yaw), + }, + } + + local nodes_in_wall = get_nodes_in_box(wall_box) + + -- Main placement box in front + + local placement_box = { + yaw = player_yaw, + dimensions = {move_distance, wall_dimensions[2], placement_height - placement_depth}, + origin = { + x = player_pos.x + math.cos(player_yaw) * 5, + y = player_pos.y + 1.5 + placement_depth, + z = player_pos.z + math.sin(player_yaw) * 5, + }, + } + + local non_walkable_nodes_in_placement_box = get_non_walkable_nodes_in_box(placement_box) + + table.sort(non_walkable_nodes_in_placement_box, function(a, b) return a.y < b.y end) + + -- Side placement boxes + + local side_box_width = wall_dimensions[2] + + local left_box = { + yaw = player_yaw, + dimensions = {move_distance, side_box_width, wall_dimensions[3] + 2}, + origin = { + x = player_pos.x + math.cos(player_yaw+math.pi/2) * side_box_width, + y = player_pos.y + 0.5 * wall_dimensions[3] - 3, + z = player_pos.z + math.sin(player_yaw+math.pi/2) * side_box_width, + }, + } + + local nwn_in_left_box = get_non_walkable_nodes_in_box(left_box) + + local right_box = { + yaw = player_yaw, + dimensions = {move_distance, side_box_width, wall_dimensions[3] + 2}, + origin = { + x = player_pos.x + math.cos(player_yaw-math.pi/2) * side_box_width, + y = player_pos.y + 0.5 * wall_dimensions[3] - 3, + z = player_pos.z + math.sin(player_yaw-math.pi/2) * side_box_width, + }, + } + + local nwn_in_right_box = get_non_walkable_nodes_in_box(right_box) + + local nwn_left_right_combined = {} + + for i, node_pos in ipairs(nwn_in_left_box) do + table.insert(nwn_left_right_combined, node_pos) + end + for i, node_pos in ipairs(nwn_in_right_box) do + table.insert(nwn_left_right_combined, node_pos) + end + + table.sort(nwn_left_right_combined, function(a, b) return a.y < b.y end) + + for i, node_pos in ipairs(nwn_left_right_combined) do + table.insert(non_walkable_nodes_in_placement_box, node_pos) + end + + local num_sounds_played = 0 + + for i, node_pos in ipairs(nodes_in_wall) do + local node = minetest.get_node(node_pos) + if node.name ~= "air" then + if #non_walkable_nodes_in_placement_box > 0 then + minetest.set_node(non_walkable_nodes_in_placement_box[1], node) + minetest.remove_node(node_pos) + table.remove(non_walkable_nodes_in_placement_box, 1) + + if num_sounds_played < 1 then + local node_def = minetest.registered_nodes[node.name] + if node_def then + local sounds = node_def.sounds + if sounds and sounds.dig then + num_sounds_played = num_sounds_played + 1 + minetest.sound_play(sounds.dig, {pos = pos, gain = 0.5}) + end + end + end + end + end + end +end + +function target_value(current, target, rate) + if current < target - rate then + return current + rate + elseif current > target + rate then + return current - rate + else + return target + end +end + +local BULLDOZER_SIZE = 3 +local BULLDOZER_HEIGHT = 1 +local CLEAR_HEIGHT = 10 +local PLACEMENT_HEIGHT = 3 +local PLACEMENT_DEPTH = -5 + +-- Register the bulldozer entity +minetest.register_entity("bulldozer:bulldozer", { + initial_properties = { + physical = true, + collisionbox = {-1.4, -0.5, -1.4, 1.4, 0.8, 1.4}, + visual = "mesh", + mesh = "bulldozer_bulldozer.obj", + textures = { + "bulldozer_bulldozer_blade.png", + "bulldozer_bulldozer_track.png", + "bulldozer_bulldozer_track.png", + "bulldozer_bulldozer_body.png", + "bulldozer_bulldozer_body.png", + "bulldozer_bulldozer_body.png", + }, + }, + driver = nil, + wanted_sound_pitch = 0.7, + played_sound_pitch = 0.0, + wanted_sound_gain = 0.2, + played_sound_gain = 0.0, + + on_rightclick = function(self, clicker) + if not clicker or not clicker:is_player() then + return + end + + local player_name = clicker:get_player_name() + + if self.driver and player_name == self.driver:get_player_name() then + -- Detach the player + self.driver:set_detach() + self.driver:set_eye_offset() + self.driver = nil + self.object:set_properties({ + physical = true, + }) + if self.sound_handle then minetest.sound_stop(self.sound_handle) end + elseif not self.driver then + -- Attach the player + self.driver = clicker + self.driver:set_attach(self.object, "", {x = 0, y = 0, z = 0}, {x = 0, y = -90, z = 0}) + self.driver:set_eye_offset({x = 0, y = 2, z = 0}) + self.object:set_properties({ + physical = false, -- We want to go through nodes + }) + self.wanted_sound_pitch = 0.7 + self.wanted_sound_gain = 0.2 + end + end, + + on_step = function(self, dtime) + if not self.driver then + self:update_sound() + return + end + + -- Get player control inputs + local ctrl = self.driver:get_player_control() + --local yaw = self.driver:get_look_horizontal() + local yaw = self.object:get_yaw() + + local object_pos = self.object:get_pos() + + local y_off = -0.5 + + local box = { + yaw = yaw, + dimensions = {(BULLDOZER_SIZE+1), (BULLDOZER_SIZE+1), 1}, + origin = { + x = object_pos.x, + y = object_pos.y - y_off + 0.9, + z = object_pos.z, + }, + } + local nwn_tracks = get_walkable_nodes_in_box(box) + + local box = { + yaw = yaw, + dimensions = {(BULLDOZER_SIZE+1), (BULLDOZER_SIZE+1), 1}, + origin = { + x = object_pos.x, + y = object_pos.y - y_off - 0.5, + z = object_pos.z, + }, + } + local nwn_support = get_walkable_nodes_in_box(box) + + local box = { + yaw = yaw, + dimensions = {(BULLDOZER_SIZE+1), (BULLDOZER_SIZE+1), 1}, + origin = { + x = object_pos.x, + y = object_pos.y - y_off - 0.15, + z = object_pos.z, + }, + } + local nwn_close_support = get_walkable_nodes_in_box(box) + + local box = { + yaw = yaw, + dimensions = {(BULLDOZER_SIZE+1), (BULLDOZER_SIZE+1), 1}, + origin = { + x = object_pos.x, + y = object_pos.y - y_off - 0.05, + z = object_pos.z, + }, + } + local nwn_very_close_support = get_walkable_nodes_in_box(box) + + if ctrl.up then + local speed = 2.0 + + -- Move the bulldozer forward + self.object:set_velocity(vector.new( + math.cos(yaw+math.pi) * speed, + 0, + math.sin(yaw+math.pi) * speed + )) + + local object_pos2 = self.object:get_pos() + if ctrl.jump then + object_pos2.y = object_pos2.y - y_off + 1.1 + elseif ctrl.sneak then + object_pos2.y = object_pos2.y - y_off - 0.9 + else + object_pos2.y = object_pos2.y - y_off - 0.5 + end + local object_yaw = self.object:get_yaw()+math.pi + + local wall_dimensions = {2, (BULLDOZER_SIZE+1), CLEAR_HEIGHT} + local move_distance = 5 + create_virtual_wall(object_pos2, object_yaw, wall_dimensions, move_distance, PLACEMENT_HEIGHT, PLACEMENT_DEPTH) + elseif ctrl.down then + local speed = 1.5 + -- Move the bulldozer backward + self.object:set_velocity(vector.new( + math.cos(yaw) * speed, + 0, + math.sin(yaw) * speed + )) + else + self.object:set_velocity({x = 0, y = 0, z = 0}) + end + + if ctrl.left then + self.object:set_yaw(self.object:get_yaw() + 0.020) + elseif ctrl.right then + self.object:set_yaw(self.object:get_yaw() - 0.020) + end + + if (ctrl.jump and ctrl.up and (#nwn_tracks >= 1 or #nwn_very_close_support >= (BULLDOZER_SIZE*BULLDOZER_SIZE/2))) or + (ctrl.down and #nwn_tracks >= 1 and not ctrl.sneak) then + local rate = 0.01 + if #nwn_very_close_support >= 9 then + rate = 0.03 + elseif #nwn_very_close_support >= 5 then + rate = 0.02 + end + self.object:set_pos({ + x = self.object:get_pos().x, + y = self.object:get_pos().y + rate, + z = self.object:get_pos().z + }) + elseif (ctrl.sneak and ctrl.up) then + local rate = 0.01 + if #nwn_support <= 7 then + rate = 0.02 + end + self.object:set_pos({ + x = self.object:get_pos().x, + y = self.object:get_pos().y - rate, + z = self.object:get_pos().z + }) + else + if #nwn_close_support >= (BULLDOZER_SIZE*BULLDOZER_SIZE*0.8) and not ctrl.sneak then + local off = -y_off + local new_y = math.floor(self.object:get_pos().y + off + 0.5) - off + if math.abs(new_y - self.object:get_pos().y) >= 0.2 then + self.object:set_pos({ + x = self.object:get_pos().x, + y = new_y, + z = self.object:get_pos().z + }) + end + end + end + + if not ctrl.sneak then + if #nwn_support <= (BULLDOZER_SIZE*BULLDOZER_SIZE/3) then + self.object:set_pos({ + x = self.object:get_pos().x, + --y = math.floor(self.object:get_pos().y + 0.5 - 1.0), + y = self.object:get_pos().y - 0.1, + z = self.object:get_pos().z + }) + elseif #nwn_close_support <= (BULLDOZER_SIZE*BULLDOZER_SIZE/3) then + self.object:set_pos({ + x = self.object:get_pos().x, + --y = math.floor(self.object:get_pos().y + 0.5 - 1.0), + y = self.object:get_pos().y - 0.02, + z = self.object:get_pos().z + }) + end + end + + if ctrl.up then + self.wanted_sound_pitch = target_value(self.wanted_sound_pitch, 1.3, 0.02) + self.wanted_sound_gain = target_value(self.wanted_sound_gain, 0.30, 0.02) + elseif ctrl.down or ctrl.left or ctrl.right then + self.wanted_sound_pitch = target_value(self.wanted_sound_pitch, 1.1, 0.02) + self.wanted_sound_gain = target_value(self.wanted_sound_gain, 0.25, 0.02) + else + self.wanted_sound_pitch = target_value(self.wanted_sound_pitch, 0.9, 0.02) + self.wanted_sound_gain = target_value(self.wanted_sound_gain, 0.20, 0.02) + end + self:update_sound() + end, + + update_sound = function(self) + if not self.driver then + if self.sound_handle then minetest.sound_stop(self.sound_handle) end + return + end + if math.abs(self.wanted_sound_pitch - self.played_sound_pitch) < 0.06 and + math.abs(self.wanted_sound_gain - self.played_sound_gain) < 0.03 then + return + end + if self.sound_handle then minetest.sound_stop(self.sound_handle) end + if self.object then + self.played_sound_pitch = self.wanted_sound_pitch + self.played_sound_gain = self.wanted_sound_gain + self.sound_handle = minetest.sound_play({name = "bulldozer_engine"}, { + object = self.object, gain = self.wanted_sound_gain, + pitch = self.wanted_sound_pitch, + max_hear_distance = 45, + loop = true, + }) + end + end, +}) + +-- Register the bulldozer item for spawning the entity +minetest.register_craftitem("bulldozer:bulldozer_item", { +description = "Bulldozer", +inventory_image = "bulldozer_bulldozer_item.png", +on_place = function(itemstack, placer, pointed_thing) +if pointed_thing.type ~= "node" then +return +end + local ent = minetest.add_entity(pointed_thing.above, "bulldozer:bulldozer") + ent:set_yaw(placer:get_look_horizontal()) + + itemstack:take_item() + return itemstack +end, +}) + diff --git a/media/bulldozer_bulldozer.obj b/media/bulldozer_bulldozer.obj new file mode 100644 index 0000000..ec761e7 --- /dev/null +++ b/media/bulldozer_bulldozer.obj @@ -0,0 +1,141 @@ +# Axis-aligned boxes for Minetest + +mtllib boxes.mtl + +# Blade +o Box1 +g Group1 +usemtl Texture1 +v 18.5 0 -15.0 +v 20.5 0 -15.0 +v 20.5 0 15.0 +v 18.5 0 15.0 +v 18.5 8 -15.0 +v 20.5 8 -15.0 +v 20.5 8 15.0 +v 18.5 8 15.0 +vt 0 0 +vt 1 0 +vt 1 1 +vt 0 1 +f 1/1 5/4 6/3 2/2 +f 2/1 6/4 7/3 3/2 +f 3/1 7/4 8/3 4/2 +f 4/1 8/4 5/3 1/2 +f 1/1 2/2 3/3 4/4 +f 5/1 8/2 7/3 6/4 + +# Left track +o Box2 +g Group2 +usemtl Texture2 +v -10.0 0 -14 +v 17.0 0 -14 +v 17.0 0 -8 +v -10.0 0 -8 +v -10.0 6 -14 +v 17.0 6 -14 +v 17.0 6 -8 +v -10.0 6 -8 +vt 0 0 +vt 1 0 +vt 1 1 +vt 0 1 +f 9/1 13/4 14/3 10/2 +f 10/1 14/4 15/3 11/2 +f 11/1 15/4 16/3 12/2 +f 12/1 16/4 13/3 9/2 +f 9/1 10/2 11/3 12/4 +f 13/1 16/2 15/3 14/4 + +# Right track +o Box3 +g Group3 +usemtl Texture3 +v -10.0 0 8 +v 17.0 0 8 +v 17.0 0 14 +v -10.0 0 14 +v -10.0 6 8 +v 17.0 6 8 +v 17.0 6 14 +v -10.0 6 14 +vt 0 0 +vt 1 0 +vt 1 1 +vt 0 1 +f 17/1 21/4 22/3 18/2 +f 18/1 22/4 23/3 19/2 +f 19/1 23/4 24/3 20/2 +f 20/1 24/4 21/3 17/2 +f 17/1 18/2 19/3 20/4 +f 21/1 24/2 23/3 22/4 + +# Body +o Box4 +g Group4 +usemtl Texture4 +v -10.0 1 -7.0 +v 17.0 1 -7.0 +v 17.0 1 7.0 +v -10.0 1 7.0 +v -10.0 10 -7.0 +v 17.0 10 -7.0 +v 17.0 10 7.0 +v -10.0 10 7.0 +vt 0 0 +vt 1 0 +vt 1 1 +vt 0 1 +f 25/1 29/4 30/3 26/2 +f 26/1 30/4 31/3 27/2 +f 27/1 31/4 32/3 28/2 +f 28/1 32/4 29/3 25/2 +f 25/1 26/2 27/3 28/4 +f 29/1 32/2 31/3 30/4 + +# Left track cover +o Box5 +g Group5 +usemtl Texture5 +v -9.0 1 -15 +v 16.0 1 -15 +v 16.0 1 -7 +v -9.0 1 -7 +v -9.0 5 -15 +v 16.0 5 -15 +v 16.0 5 -7 +v -9.0 5 -7 +vt 0 0 +vt 1 0 +vt 1 1 +vt 0 1 +f 33/1 37/4 38/3 34/2 +f 34/1 38/4 39/3 35/2 +f 35/1 39/4 40/3 36/2 +f 36/1 40/4 37/3 33/2 +f 33/1 34/2 35/3 36/4 +f 37/1 40/2 39/3 38/4 + +# Right track cover +o Box6 +g Group6 +usemtl Texture6 +v -9.0 1 7 +v 16.0 1 7 +v 16.0 1 15 +v -9.0 1 15 +v -9.0 5 7 +v 16.0 5 7 +v 16.0 5 15 +v -9.0 5 15 +vt 0 0 +vt 1 0 +vt 1 1 +vt 0 1 +f 41/1 45/4 46/3 42/2 +f 42/1 46/4 47/3 43/2 +f 43/1 47/4 48/3 44/2 +f 44/1 48/4 45/3 41/2 +f 41/1 42/2 43/3 44/4 +f 45/1 48/2 47/3 46/4 diff --git a/media/bulldozer_bulldozer_blade.png b/media/bulldozer_bulldozer_blade.png new file mode 100644 index 0000000000000000000000000000000000000000..50d3ccea56e8b088911b47503f004adcc68ab0c2 GIT binary patch literal 5088 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 F{s)6(pfvyh literal 0 HcmV?d00001 diff --git a/media/bulldozer_bulldozer_body.png b/media/bulldozer_bulldozer_body.png new file mode 100644 index 0000000000000000000000000000000000000000..fa8d0a10c5ca4212f4aa44a5728ebee781d27a0f GIT binary patch literal 5089 zcmeHKc~Dd577r?*fItP1j6Kwl(0xV1WAIFDuo1#Lm@m_i9)Cb zlMo1+J0%gzc0P76d3V!6GR?30l1h`S?lHMqoWo1$ zuBWfnEDbnK8ru9sInS)`(qHlm-#u=Wi4WafJd_wKSpLPmayQo+R^Q7ewojs7*v7t~ zT}4e_4q0Yc7w#aC>ZIv-rB{Tp{%~}1j^CtmG$EKbf=_Z@fBIGzwUpUd zE;Dui!gEy`VywS!_Oh0>0*lNhD8wen051O=NW2}qz++3)cI|C|2VgzHqEbad#(=+w;me^GBK)J zU!PPP<9ot^oAY*u+~QsU>yIUz*;n_5^#`A^ZM*Y^9(G6=YiSyiXpEl(C2fq5!BDaUNPp~=N)Z7gT>Z5OJ226r)^OjtHl;Im# zxoThkjp6%Osyq$9l}@7{Es8Knb$_UDzqjFKhF6Mj$DyhAYj*jW*?ylNpB3MlxAL_&<>ccjfrB0=W8zP(+qdjxZGLybJ2p2F zHzu5a*l@mMjb8Jah3s$C+oK5ty$sCDD~Rvq^?usHjOTy9)!FZ2ki~|PVoRT;b9_HG6=V0+Ut`vH5_3M);^ z;oba}nZB9zLPN)SP5#A;cs_GOnS1{-;iUY38#KkHQI6N<^RS3A(sP*a;spKmteR1l)bG!cSx-mxlu9ndh)9dRf3zM zD*|j~4Fzyju|kNFH8LgG&IAI_O`}9au_z7+Q7I;OA^vvmJQ2dgF2o3? z02U~{&=|}wMTLf@1cZrFVnrM=(aqI}r{Mws8Hyv2MiwVmb2TnR9WEDKYsC~Iq*K9T zU5Jr_AjnIhLLmm3L54{_8Z41UbTxu_DzStc;=O1L0^GR}V{lx_rBITRlE_JPvO*=L zP&ph91*TDGG!oDtsgvb6q9MuEwpxf$3~y8|QejFQQ^+ALCL&ZM;4VZWIEUWHCsPUp zgRu1XU{%R3g+T5tZY%6CuQ+ zaeHNgDo&SA0_LE{!$3Z$4!sj~uPf24_Hk`H8k6r1)$m(IjM zfcZG?N80am*C_)nfq?6+5G82C<9oXhwf?zcg$NUKb(g4^j#7nGDv2%;p(KWwO(O}# zA|Xl4X3`l{HXA{i@B}EnT#X}g5vqj(;A9NoNF*F57-iB(P8^s?V$dZt5}SjFNFp|e z4vQHqmWaii01>FdKvyDh6Qj~Xi2)P`W;-EHBA5gtFo;Hr!l4VR$Cx&tlYT#q2W3z0@14O}M)ibHS-FmNF*#^ec_52|693=PE* zEt^y)HcV$Sm@GDpLuaxlST8|UYS4*VOe##K({$$AvT#8%fLKJ^rvN}F2f1*)R49Tg zRACB5oC{GK0Hn1XKNf&-5+gX`jo>H%g=usy%;3V*Fsc(5=5XmOM;J&Z=qtpSB>8`7 zYljEojh5UGQv?6WI??Dvg`)AJccZs)m~NCHNH-K*L^PU$8c9UOx;O#W=#eM}kxNmq zJ;vJgy&U_SR-g%)5-}A8Rbw-qNDPsfLqcE)N@Bq*2}{DD)0ni;tj5vR3JIQss8A0n z;1O^I+Ed3Bv`|+l`;TKuib1ti0ECfX2I&J~>@mV9V+m8VJL7%EJj#D@!qX{CSTewF zR0ft8*oBnwWjMwei1(j-jn(3xbOAx1oqQ6%pXvHc*C#RXNy?wQ>oZ-S#K0#hf9|gT z8(l^p9;Z+__!pD}9+q4i>z{!Kt!YZXC^dm#JX8DDBkaky20}xeFYqyZHbrmxv;}Kx z`)&bID&N~9Y+=i(AP>_zXzKGBZiQt7-L=KMi}TvIJhW;@vZzeYLjBD9NoJWvNk2f+ zAEMopn~$68Wgf^5mOAf6Z81(=~bfge~RUpd`DT^qEG{Yw|WG3P~Um0^cXVyVNr}{Xf{y Bt2F=s literal 0 HcmV?d00001 diff --git a/media/bulldozer_bulldozer_track.png b/media/bulldozer_bulldozer_track.png new file mode 100644 index 0000000000000000000000000000000000000000..79e3dab98d9ca88fc56c3c9fd322afd641cb32a5 GIT binary patch literal 5702 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

0TgJ=exgrp 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 literal 0 HcmV?d00001