├── examples ├── raw │ └── pointy_dot.png ├── assets │ └── atlas.atlas ├── example_rotation │ ├── example_rotation_dot.script │ ├── example_rotation_dot.go │ ├── example_rotation_main.collection │ └── example_rotation_main.script ├── example_with_defgraph │ ├── example_with_defgraph_dot.script │ ├── example_with_defgraph_dot.go │ ├── example_with_defgraph_main.collection │ └── example_with_defgraph_main.script └── main.input_binding ├── .gitignore ├── game.project ├── LICENSE ├── README.md └── defarmy └── defarmy.lua /examples/raw/pointy_dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev-masih/defarmy/HEAD/examples/raw/pointy_dot.png -------------------------------------------------------------------------------- /examples/assets/atlas.atlas: -------------------------------------------------------------------------------- 1 | images { 2 | image: "/examples/raw/pointy_dot.png" 3 | } 4 | margin: 0 5 | extrude_borders: 2 6 | inner_padding: 0 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.internal 2 | /build 3 | .externalToolBuilders 4 | .DS_Store 5 | Thumbs.db 6 | .lock-wscript 7 | *.pyc 8 | .project 9 | .cproject 10 | builtins -------------------------------------------------------------------------------- /game.project: -------------------------------------------------------------------------------- 1 | [project] 2 | title = DefArmy 3 | version = 1.0 4 | dependencies = https://github.com/dev-masih/defgraph/archive/master.zip 5 | 6 | [library] 7 | include_dirs = defarmy 8 | 9 | [input] 10 | game_binding = /examples/main.input_bindingc 11 | 12 | [bootstrap] 13 | main_collection = /examples/example_rotation/example_rotation_main.collectionc 14 | 15 | [script] 16 | shared_state = 1 17 | 18 | -------------------------------------------------------------------------------- /examples/example_rotation/example_rotation_dot.script: -------------------------------------------------------------------------------- 1 | go.property("army_id", 1) 2 | go.property("debug", false) 3 | 4 | -- require defarmy 5 | local defarmy = require("defarmy.defarmy") 6 | 7 | function init(self) 8 | -- get army id from main 9 | self.soldier_id = defarmy.soldier_create(go.get_position(), vmath.vector3(0, 1, 0), self.army_id) 10 | 11 | if self.debug then 12 | defarmy.soldier_debug_on(self.soldier_id, vmath.vector4(1, 0, 0, 1)) 13 | end 14 | end 15 | 16 | function update(self, dt) 17 | local soldier_position, soldier_rotation = defarmy.soldier_move(self.soldier_id, go.get_position(), 80 * dt) 18 | go.set_position(soldier_position) 19 | go.set_rotation(soldier_rotation) 20 | end -------------------------------------------------------------------------------- /examples/example_with_defgraph/example_with_defgraph_dot.script: -------------------------------------------------------------------------------- 1 | go.property("army_id", 1) 2 | go.property("tint", vmath.vector4(0, 1, 0, 1)) 3 | 4 | -- require defarmy 5 | local defarmy = require("defarmy.defarmy") 6 | 7 | function init(self) 8 | sprite.set_constant("#sprite", "tint", self.tint) 9 | 10 | -- Create a new soldier in defarmy 11 | self.soldier_id = defarmy.soldier_create(go.get_position(), vmath.vector3(0, 1, 0), self.army_id) 12 | end 13 | 14 | function update(self, dt) 15 | -- fetch each step position and rotation from defarmy 16 | local soldier_position, soldier_rotation = defarmy.soldier_move(self.soldier_id, go.get_position(), 80 * dt) 17 | go.set_position(soldier_position) 18 | go.set_rotation(soldier_rotation) 19 | end -------------------------------------------------------------------------------- /examples/example_rotation/example_rotation_dot.go: -------------------------------------------------------------------------------- 1 | components { 2 | id: "dot" 3 | component: "/examples/example_rotation/example_rotation_dot.script" 4 | position { 5 | x: 0.0 6 | y: 0.0 7 | z: 0.0 8 | } 9 | rotation { 10 | x: 0.0 11 | y: 0.0 12 | z: 0.0 13 | w: 1.0 14 | } 15 | } 16 | embedded_components { 17 | id: "sprite" 18 | type: "sprite" 19 | data: "tile_set: \"/examples/assets/atlas.atlas\"\n" 20 | "default_animation: \"pointy_dot\"\n" 21 | "material: \"/builtins/materials/sprite.material\"\n" 22 | "blend_mode: BLEND_MODE_ALPHA\n" 23 | "" 24 | position { 25 | x: 0.0 26 | y: 0.0 27 | z: 0.0 28 | } 29 | rotation { 30 | x: 0.0 31 | y: 0.0 32 | z: 0.0 33 | w: 1.0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/example_with_defgraph/example_with_defgraph_dot.go: -------------------------------------------------------------------------------- 1 | components { 2 | id: "dot" 3 | component: "/examples/example_with_defgraph/example_with_defgraph_dot.script" 4 | position { 5 | x: 0.0 6 | y: 0.0 7 | z: 0.0 8 | } 9 | rotation { 10 | x: 0.0 11 | y: 0.0 12 | z: 0.0 13 | w: 1.0 14 | } 15 | } 16 | embedded_components { 17 | id: "sprite" 18 | type: "sprite" 19 | data: "tile_set: \"/examples/assets/atlas.atlas\"\n" 20 | "default_animation: \"pointy_dot\"\n" 21 | "material: \"/builtins/materials/sprite.material\"\n" 22 | "blend_mode: BLEND_MODE_ALPHA\n" 23 | "" 24 | position { 25 | x: 0.0 26 | y: 0.0 27 | z: 0.0 28 | } 29 | rotation { 30 | x: 0.0 31 | y: 0.0 32 | z: 0.0 33 | w: 1.0 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/main.input_binding: -------------------------------------------------------------------------------- 1 | key_trigger { 2 | input: KEY_A 3 | action: "key_a" 4 | } 5 | key_trigger { 6 | input: KEY_S 7 | action: "key_s" 8 | } 9 | key_trigger { 10 | input: KEY_D 11 | action: "key_d" 12 | } 13 | key_trigger { 14 | input: KEY_1 15 | action: "key_1" 16 | } 17 | key_trigger { 18 | input: KEY_2 19 | action: "key_2" 20 | } 21 | key_trigger { 22 | input: KEY_3 23 | action: "key_3" 24 | } 25 | key_trigger { 26 | input: KEY_4 27 | action: "key_4" 28 | } 29 | key_trigger { 30 | input: KEY_5 31 | action: "key_5" 32 | } 33 | key_trigger { 34 | input: KEY_6 35 | action: "key_6" 36 | } 37 | mouse_trigger { 38 | input: MOUSE_BUTTON_1 39 | action: "left_click" 40 | } 41 | mouse_trigger { 42 | input: MOUSE_BUTTON_2 43 | action: "right_click" 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 dev-masih 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/example_rotation/example_rotation_main.collection: -------------------------------------------------------------------------------- 1 | name: "default" 2 | scale_along_z: 0 3 | embedded_instances { 4 | id: "go" 5 | data: "components {\n" 6 | " id: \"script\"\n" 7 | " component: \"/examples/example_rotation/example_rotation_main.script\"\n" 8 | " position {\n" 9 | " x: 0.0\n" 10 | " y: 0.0\n" 11 | " z: 0.0\n" 12 | " }\n" 13 | " rotation {\n" 14 | " x: 0.0\n" 15 | " y: 0.0\n" 16 | " z: 0.0\n" 17 | " w: 1.0\n" 18 | " }\n" 19 | "}\n" 20 | "embedded_components {\n" 21 | " id: \"factory\"\n" 22 | " type: \"factory\"\n" 23 | " data: \"prototype: \\\"/examples/example_rotation/example_rotation_dot.go\\\"\\n" 24 | "load_dynamically: false\\n" 25 | "\"\n" 26 | " position {\n" 27 | " x: 0.0\n" 28 | " y: 0.0\n" 29 | " z: 0.0\n" 30 | " }\n" 31 | " rotation {\n" 32 | " x: 0.0\n" 33 | " y: 0.0\n" 34 | " z: 0.0\n" 35 | " w: 1.0\n" 36 | " }\n" 37 | "}\n" 38 | "" 39 | position { 40 | x: 300.0 41 | y: 300.0 42 | z: 0.0 43 | } 44 | rotation { 45 | x: 0.0 46 | y: 0.0 47 | z: 0.0 48 | w: 1.0 49 | } 50 | scale3 { 51 | x: 1.0 52 | y: 1.0 53 | z: 1.0 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/example_with_defgraph/example_with_defgraph_main.collection: -------------------------------------------------------------------------------- 1 | name: "default" 2 | scale_along_z: 0 3 | embedded_instances { 4 | id: "go" 5 | data: "components {\n" 6 | " id: \"script\"\n" 7 | " component: \"/examples/example_with_defgraph/example_with_defgraph_main.script\"\n" 8 | " position {\n" 9 | " x: 0.0\n" 10 | " y: 0.0\n" 11 | " z: 0.0\n" 12 | " }\n" 13 | " rotation {\n" 14 | " x: 0.0\n" 15 | " y: 0.0\n" 16 | " z: 0.0\n" 17 | " w: 1.0\n" 18 | " }\n" 19 | "}\n" 20 | "embedded_components {\n" 21 | " id: \"factory\"\n" 22 | " type: \"factory\"\n" 23 | " data: \"prototype: \\\"/examples/example_with_defgraph/example_with_defgraph_dot.go\\\"\\n" 24 | "load_dynamically: false\\n" 25 | "\"\n" 26 | " position {\n" 27 | " x: 0.0\n" 28 | " y: 0.0\n" 29 | " z: 0.0\n" 30 | " }\n" 31 | " rotation {\n" 32 | " x: 0.0\n" 33 | " y: 0.0\n" 34 | " z: 0.0\n" 35 | " w: 1.0\n" 36 | " }\n" 37 | "}\n" 38 | "" 39 | position { 40 | x: 300.0 41 | y: 300.0 42 | z: 0.0 43 | } 44 | rotation { 45 | x: 0.0 46 | y: 0.0 47 | z: 0.0 48 | w: 1.0 49 | } 50 | scale3 { 51 | x: 1.0 52 | y: 1.0 53 | z: 1.0 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/example_with_defgraph/example_with_defgraph_main.script: -------------------------------------------------------------------------------- 1 | -- require defarmy 2 | local defarmy = require "defarmy.defarmy" 3 | 4 | local defgraph = require "defgraph.defgraph" 5 | 6 | function init(self) 7 | msg.post(".", "acquire_input_focus") 8 | 9 | self.speed = 50 10 | 11 | -- Creating a graph with defgraph 12 | defgraph.map_set_properties(2, 5, 5, 50, true) 13 | defgraph.debug_set_properties(vmath.vector4(1, 0, 1, 1), vmath.vector4(0, 1, 0, 1), vmath.vector4(0, 1, 1, 1), 3) 14 | local node01 = defgraph.map_add_node(vmath.vector3(100, 100, 0)) 15 | local node02 = defgraph.map_add_node(vmath.vector3(100, 270, 0)) 16 | local node03 = defgraph.map_add_node(vmath.vector3(100, 500, 0)) 17 | local node04 = defgraph.map_add_node(vmath.vector3(500, 500, 0)) 18 | local node05 = defgraph.map_add_node(vmath.vector3(500, 270, 0)) 19 | local node06 = defgraph.map_add_node(vmath.vector3(500, 100, 0)) 20 | defgraph.map_add_route(node01, node02, true) 21 | defgraph.map_add_route(node02, node03, true) 22 | defgraph.map_add_route(node03, node04, true) 23 | defgraph.map_add_route(node04, node05, true) 24 | defgraph.map_add_route(node05, node06, true) 25 | defgraph.map_add_route(node06, node01, true) 26 | 27 | self.go_pos_1 = vmath.vector3(270, 300, 0) 28 | self.go_pos_2 = vmath.vector3(320, 300, 0) 29 | self.movement_data_1 = defgraph.move_initialize(self.go_pos_1, { node02, node05 }, defgraph.ROUTETYPE.CYCLE, vmath.vector3(0, 1, 0)) 30 | self.movement_data_2 = defgraph.move_initialize(self.go_pos_2, { node02, node05 }, defgraph.ROUTETYPE.CYCLE, vmath.vector3(0, 1, 0)) 31 | 32 | -- Creating an army and a soldier 33 | self.army_id_1 = defarmy.army_create(self.go_pos_1, go.get_rotation(), 18, true, defarmy.PATTERN.TOP_TO_BOTTOM_SQUARE) 34 | self.army_id_2 = defarmy.army_create(self.go_pos_2, go.get_rotation(), 18, false, defarmy.PATTERN.BOTTOM_TO_TOP_SQUARE) 35 | 36 | factory.create("#factory", vmath.vector3(300, 300, 0), nil, { army_id = self.army_id_1, tint = vmath.vector4(1, 0, 0, 1) }, 0.35) 37 | factory.create("#factory", vmath.vector3(300, 300, 0), nil, { army_id = self.army_id_2, tint = vmath.vector4(0, 1, 0, 1) }, 0.35) 38 | 39 | timer.delay(1.5, true, function() 40 | factory.create("#factory", vmath.vector3(300, 300, 0), nil, { army_id = self.army_id_1, tint = vmath.vector4(1, 0, 0, 1) }, 0.35) 41 | factory.create("#factory", vmath.vector3(300, 300, 0), nil, { army_id = self.army_id_2, tint = vmath.vector4(0, 1, 0, 1) }, 0.35) 42 | end) 43 | end 44 | 45 | function on_input(self, action_id, action) 46 | if action_id == hash("left_click") and action.pressed then 47 | factory.create("#factory", vmath.vector3(action.x, action.y, 0), nil, { army_id = self.army_id_1, tint = vmath.vector4(1, 0, 0, 1) }, 0.35) 48 | end 49 | if action_id == hash("right_click") and action.pressed then 50 | factory.create("#factory", vmath.vector3(action.x, action.y, 0), nil, { army_id = self.army_id_2, tint = vmath.vector4(0, 1, 0, 1) }, 0.35) 51 | end 52 | end 53 | 54 | function update(self, dt) 55 | 56 | self.movement_data_1, self.move_result_1 = defgraph.move_player(self.go_pos_1, self.speed * dt, self.movement_data_1) 57 | self.movement_data_2, self.move_result_2 = defgraph.move_player(self.go_pos_2, self.speed * dt, self.movement_data_2) 58 | 59 | self.go_pos_1 = self.move_result_1.position 60 | self.go_pos_2 = self.move_result_2.position 61 | 62 | defarmy.army_update_position(self.army_id_1, self.move_result_1.position) 63 | defarmy.army_update_position(self.army_id_2, self.move_result_2.position) 64 | 65 | defarmy.army_update_rotation(self.army_id_1, self.move_result_1.rotation) 66 | defarmy.army_update_rotation(self.army_id_2, self.move_result_2.rotation) 67 | 68 | -- draw debug info of nodes and routes 69 | defgraph.debug_draw_map_nodes() 70 | defgraph.debug_draw_map_routes() 71 | 72 | defgraph.debug_draw_player_move(self.movement_data_1, vmath.vector4(1, 1, 0, 1)) 73 | defgraph.debug_draw_player_move(self.movement_data_2, vmath.vector4(1, 0, 1, 1)) 74 | 75 | msg.post("@render:", "draw_text", { text = "example with DefGraph", position = vmath.vector3(20, 630, 0) } ) 76 | msg.post("@render:", "draw_text", { text = "left click: deploy red dot - right click: deploy green dot", position = vmath.vector3(20, 610, 0) } ) 77 | msg.post("@render:", "draw_text", { text = "red pattern: TOP_TO_BOTTOM_SQUARE - green pattern: BOTTOM_TO_TOP_SQUARE", position = vmath.vector3(20, 590, 0) } ) 78 | msg.post("@render:", "draw_text", { text = "red army: sticky - green army: not sticky", position = vmath.vector3(20, 570, 0) } ) 79 | end -------------------------------------------------------------------------------- /examples/example_rotation/example_rotation_main.script: -------------------------------------------------------------------------------- 1 | -- require defarmy 2 | local defarmy = require "defarmy.defarmy" 3 | 4 | function init(self) 5 | msg.post(".", "acquire_input_focus") 6 | 7 | self.soldier_debug = false 8 | self.army_debug = false 9 | self.army_is_sticky = true 10 | self.army_pattern = defarmy.PATTERN.BOTTOM_TO_TOP_SQUARE 11 | 12 | -- Creating an army and a soldier 13 | self.army_id = defarmy.army_create(vmath.vector3(250, 250, 0), vmath.quat(), 25, self.army_is_sticky, self.army_pattern) 14 | 15 | for i = 1, 50 do 16 | factory.create("#factory", vmath.vector3(250, 250, 0), nil, { army_id = self.army_id, debug = self.soldier_debug }, 0.45) 17 | end 18 | end 19 | 20 | local function custom_pattern_function(count) 21 | local result = {} 22 | local sum = count 23 | for i = 1 ,count do 24 | local partial = math.modf(sum / 4) 25 | if partial == 1 then 26 | table.insert(result, sum) 27 | return result 28 | end 29 | table.insert(result, partial) 30 | sum = sum - partial 31 | end 32 | end 33 | 34 | function on_input(self, action_id, action) 35 | if action_id == hash("left_click") and action.pressed then 36 | factory.create("#factory", vmath.vector3(action.x, action.y, 0), nil, { army_id = self.army_id, debug = self.soldier_debug }, 0.45) 37 | end 38 | if action_id == hash("right_click") then 39 | local direction_vector = vmath.vector3(action.x, action.y, 0) - vmath.vector3(250, 250, 0) 40 | direction_vector = vmath.normalize(direction_vector) 41 | local quat = vmath.quat_from_to(vmath.vector3(0, 1, 0), direction_vector) 42 | 43 | defarmy.army_update_rotation(self.army_id, quat) 44 | end 45 | if action_id == hash("key_a") and action.pressed then 46 | local members = defarmy.army_members(self.army_id) 47 | if self.soldier_debug then 48 | for i = 1, #members do defarmy.soldier_debug_off(members[i]) end 49 | self.soldier_debug = false 50 | else 51 | for i = 1, #members do defarmy.soldier_debug_on(members[i], vmath.vector4(1, 0, 0, 1)) end 52 | self.soldier_debug = true 53 | end 54 | end 55 | if action_id == hash("key_s") and action.pressed then 56 | if self.army_debug then self.army_debug = false else self.army_debug = true end 57 | end 58 | if action_id == hash("key_d") and action.pressed then 59 | if self.army_is_sticky then 60 | self.army_is_sticky = false 61 | else 62 | self.army_is_sticky = true 63 | end 64 | defarmy.army_update_stickiness(self.army_id, self.army_is_sticky) 65 | end 66 | if action_id == hash("key_1") and action.pressed then 67 | self.army_pattern = defarmy.PATTERN.BOTTOM_TO_TOP_SQUARE 68 | defarmy.army_update_pattern(self.army_id, self.army_pattern) 69 | end 70 | if action_id == hash("key_2") and action.pressed then 71 | self.army_pattern = defarmy.PATTERN.TOP_TO_BOTTOM_SQUARE 72 | defarmy.army_update_pattern(self.army_id, self.army_pattern) 73 | end 74 | if action_id == hash("key_3") and action.pressed then 75 | self.army_pattern = defarmy.PATTERN.TRIANGLE 76 | defarmy.army_update_pattern(self.army_id, self.army_pattern) 77 | end 78 | if action_id == hash("key_4") and action.pressed then 79 | self.army_pattern = defarmy.PATTERN.RHOMBUS_TALL 80 | defarmy.army_update_pattern(self.army_id, self.army_pattern) 81 | end 82 | if action_id == hash("key_5") and action.pressed then 83 | self.army_pattern = defarmy.PATTERN.RHOMBUS_SHORT 84 | defarmy.army_update_pattern(self.army_id, self.army_pattern) 85 | end 86 | if action_id == hash("key_6") and action.pressed then 87 | self.army_pattern = defarmy.PATTERN.CUSTOMIZED 88 | defarmy.army_update_pattern(self.army_id, self.army_pattern, custom_pattern_function) 89 | end 90 | end 91 | 92 | function update(self, dt) 93 | if self.army_debug then 94 | defarmy.army_debug_draw(self.army_id, vmath.vector4(0, 1, 0, 1)) 95 | end 96 | 97 | msg.post("@render:", "draw_text", { text = "example rotation", position = vmath.vector3(20, 630, 0) } ) 98 | msg.post("@render:", "draw_text", { text = "left click: deploy soldier - right click: change rotation", position = vmath.vector3(20, 610, 0) } ) 99 | 100 | if self.soldier_debug then 101 | msg.post("@render:", "draw_text", { text = "key \"A\" trigger soldiers debug draw: ON", position = vmath.vector3(20, 590, 0) } ) 102 | else 103 | msg.post("@render:", "draw_text", { text = "key \"A\" trigger soldiers debug draw: OFF", position = vmath.vector3(20, 590, 0) } ) 104 | end 105 | 106 | if self.army_debug then 107 | msg.post("@render:", "draw_text", { text = "key \"S\" trigger army debug draw: ON", position = vmath.vector3(20, 570, 0) } ) 108 | else 109 | msg.post("@render:", "draw_text", { text = "key \"S\" trigger army debug draw: OFF", position = vmath.vector3(20, 570, 0) } ) 110 | end 111 | 112 | if self.army_is_sticky then 113 | msg.post("@render:", "draw_text", { text = "key \"D\" trigger army stickiness: Sticky", position = vmath.vector3(20, 550, 0) } ) 114 | else 115 | msg.post("@render:", "draw_text", { text = "key \"D\" trigger army stickiness: Not Sticky", position = vmath.vector3(20, 550, 0) } ) 116 | end 117 | 118 | if self.army_pattern == defarmy.PATTERN.BOTTOM_TO_TOP_SQUARE then 119 | msg.post("@render:", "draw_text", { text = "key \"1-2-3-4-5-6\" change army pattern: BOTTOM_TO_TOP_SQUARE", position = vmath.vector3(20, 530, 0) } ) 120 | elseif self.army_pattern == defarmy.PATTERN.TOP_TO_BOTTOM_SQUARE then 121 | msg.post("@render:", "draw_text", { text = "key \"1-2-3-4-5-6\" change army pattern: TOP_TO_BOTTOM_SQUARE", position = vmath.vector3(20, 530, 0) } ) 122 | elseif self.army_pattern == defarmy.PATTERN.TRIANGLE then 123 | msg.post("@render:", "draw_text", { text = "key \"1-2-3-4-5-6\" change army pattern: TRIANGLE", position = vmath.vector3(20, 530, 0) } ) 124 | elseif self.army_pattern == defarmy.PATTERN.RHOMBUS_TALL then 125 | msg.post("@render:", "draw_text", { text = "key \"1-2-3-4-5-6\" change army pattern: RHOMBUS_TALL", position = vmath.vector3(20, 530, 0) } ) 126 | elseif self.army_pattern == defarmy.PATTERN.RHOMBUS_SHORT then 127 | msg.post("@render:", "draw_text", { text = "key \"1-2-3-4-5-6\" change army pattern: RHOMBUS_SHORT", position = vmath.vector3(20, 530, 0) } ) 128 | else 129 | msg.post("@render:", "draw_text", { text = "key \"1-2-3-4-5-6\" change army pattern: CUSTOMIZED", position = vmath.vector3(20, 530, 0) } ) 130 | end 131 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DefArmy v1 2 | 3 | defarmy banner 4 | 5 | This module helps you to create groups (army) of game objects (soldiers) and organize them in several different patterns or your customized pattern and manage moving and rotating game objects as a customizable group. 6 | 7 | The bellow git shows how DefArmy groups members and how it handles changing army pattern in real-time. 8 | change pattern 9 | 10 | This module can easily integrate with my other extension, [DefGraph](https://github.com/dev-masih/defgraph). You can use DefGraph to handle movements and rotation of the entire army and then use DefArmy to handle each soldier in the army. 11 | An example of integrating DefGraph with DefArmy, All green soldiers are in a separate army as red soldiers. 12 | integrating with defgraph 13 | 14 | This is a community project you are welcome to contribute to it, sending PR, suggest a feature or report a bug. 15 | 16 | ## Installation 17 | You can use DefArmy in your project by adding this project as a [Defold library dependency](http://www.defold.com/manuals/libraries/). Open your game.project file and in the dependencies field under project add: 18 | 19 | https://github.com/dev-masih/defarmy/archive/master.zip 20 | 21 | Once added, you must require the main Lua module via 22 | 23 | ``` 24 | local defarmy = require("defarmy.defarmy") 25 | ``` 26 | Then you can use the DefArmy functions using this module. 27 | 28 | [Official Defold game asset page for DefArmy](https://defold.com/assets/defarmy/) 29 | 30 | ## Army Settings 31 | There are several parameters that you can assign to an army and you can change these parameters at any time. 32 | #### **Pattern:** 33 | This parameter determines the placement shape of soldiers in an army. this property is an enumeration value that can access via `defarmy.PATTERN` table. you can pass a custom function to create army pattern or choose one of the 5 built-in patterns. for example, in bellow images showed how each pattern will place 18 soldiers. 34 | 35 | BOTTOM_TO_TOP_SQUARE | TOP_TO_BOTTOM_SQUARE | TRIANGLE 36 | :-------------: | :-------------: | :-------------: 37 | **BOTTOM_TO_TOP_SQUARE** | **TOP_TO_BOTTOM_SQUARE** | **TRIANGLE** 38 | 39 | RHOMBUS_TALL | RHOMBUS_SHORT 40 | :-------------: | :-------------: 41 | **RHOMBUS_TALL** | **RHOMBUS_SHORT** 42 | 43 | **CUSTOMIZED** 44 | You can create a customized pattern, you should select `defarmy.PATTERN.CUSTOMIZED` as the pattern and pass a customized function to create pattern schema. This function will get a `number` as a parameter that is the total count of members in army and return a `table` that specify that each row of army pattern should have what number of soldiers in it, row count should **start from the bottom of the army and ends with top**. note that the sum of row values must be equal to the total count that passed to the customized pattern function. the 0 value rows will automatically remove from the table. 45 | 46 | > example: 47 | > TRIANGLE 48 | > for this pattern customize function should return a table like: `{3, 5, 4, 3, 2, 1}` with exactly this order. 49 | 50 | #### **Stickiness:** 51 | This parameter determines is soldiers completely stick to their placements in an army or they follow their placements. this parameter mostly seen when an army is rotating. 52 | 53 | Sticky Army | Not Sticky Army 54 | :-------------: | :-------------: 55 | **Sticky** | **Not Sticky** 56 | 57 | ## Functions 58 | These are the list of available functions to use, for better understanding of how this module works, please take a look at project examples. 59 | 60 | ### `defarmy.army_create(army_center_position, army_initial_rotation, member_padding, is_sticky, army_pattern, [pattern_func])` 61 | Create a group of game objects (army) with specified member padding and pattern. 62 | #### **arguments:** 63 | * **army_center_position** `(vector3)` - Army center position 64 | * **army_initial_rotation** `(quat)` - Army initial rotation quat 65 | * **member_padding** `(number)` - Padding between members 66 | * **is_sticky** `(boolean)` - Is members glued to their places in army 67 | * **army_pattern** `(PATTERN)` - Army pattern 68 | * **pattern_func** `(func)` - Optional army customized pattern function `[nil]` *(see Pattern.CUSTOMIZED section)* 69 | #### **return:** 70 | * `(number)` - Newly added army id 71 | 72 | ### `defarmy.army_remove(army_id)` 73 | Remove a grouping of game objects (army) and release it's members. 74 | #### **arguments:** 75 | * **army_id** `(number)` - Army id number 76 | 77 | ### `defarmy.army_members(army_id)` 78 | Return an army members (soldiers) id. 79 | #### **arguments:** 80 | * **army_id** `(number)` - Army id number 81 | #### **return:** 82 | * `(table)` - List of soldier's id that were members of that army 83 | 84 | ### `defarmy.army_update_position(army_id, army_center_position)` 85 | Update an army center position. 86 | #### **arguments:** 87 | * **army_id** `(number)` - Army id number 88 | * **army_center_position** `(vector3)` - New army center position 89 | 90 | ### `defarmy.army_update_rotation(army_id, army_rotation)` 91 | Update an army rotation. 92 | #### **arguments:** 93 | * **army_id** `(number)` - Army id number 94 | * **army_rotation** `(quat)` - New army rotation quat 95 | 96 | ### `defarmy.army_update_pattern(army_id, army_pattern, [pattern_func])` 97 | Update an army pattern. 98 | #### **arguments:** 99 | * **army_id** `(number)` - Army id number 100 | * **army_pattern** `(PATTERN)` - New army pattern 101 | * **pattern_func** `(func)` - Optional army customized pattern function `[nil]` *(see Pattern.CUSTOMIZED section)* 102 | 103 | ### `defarmy.army_update_stickiness(army_id, is_sticky)` 104 | Update an army stickiness. 105 | #### **arguments:** 106 | * **army_id** `(number)` - Army id number 107 | * **is_sticky** `(boolean)` - Is members glued to their places in army 108 | 109 | ### `defarmy.soldier_create(position, initial_direction, [army_id])` 110 | Create a new soldier and optionally assign it to an army. 111 | #### **arguments:** 112 | * **position** `(vector3)` - Soldier current position 113 | * **initial_direction** `(vector3)` - Soldier initial direction vecotr 114 | * **army_id** `(optinal number)` - Optional army id number `[nil]` 115 | #### **return:** 116 | * `(number)` - Newly added soldier id 117 | 118 | ### `defarmy.soldier_join_army(soldier_id, army_id)` 119 | Assign an existing soldier to a given army. 120 | #### **arguments:** 121 | * **soldier_id** `(number)` - Soldier id number 122 | * **army_id** `(number)` - Army id number 123 | 124 | ### `defarmy.soldier_leave_army(soldier_id)` 125 | Deassign a soldier from army. 126 | #### **arguments:** 127 | * **soldier_id** `(number)` - Soldier id number 128 | 129 | ### `defarmy.soldier_remove(soldier_id)` 130 | Completely remove a soldier. 131 | #### **arguments:** 132 | * **soldier_id** `(number)` - Soldier id number 133 | 134 | ### `defarmy.soldier_move(soldier_id, current_position, speed, [threshold])` 135 | Calculate a soldier next postion and rotation. 136 | #### **arguments:** 137 | * **soldier_id** `(number)` - Soldier id number 138 | * **current_position** `(number)` - Soldier current position 139 | * **speed** `(number)` - Soldier speed 140 | * **threshold** `(optinal number)` - Optional soldier placement detection threshold `[1]` 141 | #### **return:** 142 | * `(vector3)` - Soldier next position 143 | * `(quat)` - Soldier next rotation 144 | 145 | ### `defarmy.army_debug_draw(army_id, debug_color)` 146 | Army debugging. 147 | #### **arguments:** 148 | * **army_id** `(number)` - Army id number 149 | * **debug_color** `(vector4)` - Color used for debugging 150 | 151 | ### `defarmy.soldier_debug_on(soldier_id, debug_color)` 152 | Turn on soldier position debugging. 153 | #### **arguments:** 154 | * **soldier_id** `(number)` - Soldier id number 155 | * **debug_color** `(vector4)` - Color used for debugging 156 | 157 | ### `defarmy.soldier_debug_off(soldier_id)` 158 | Turn off soldier position debugging. 159 | #### **arguments:** 160 | * **soldier_id** `(number)` - Soldier id number 161 | 162 | ## Donations 163 | If you really like my work and want to support me, consider donating to me with LTC, BTC or ETH. All donations are optional and are greatly appreciated. 🙏 164 | 165 | | LTC | BTC | ETH | 166 | | ------------- | ------------- | ------------- | 167 | | ![LTC](https://user-images.githubusercontent.com/8469800/160749469-811a4395-4b71-4e03-890d-6f260c4ff36a.jpeg) | ![BTC](https://user-images.githubusercontent.com/8469800/160749502-c5b380d3-455f-483f-9f7f-d6948949259d.jpeg) | ![ETH](https://user-images.githubusercontent.com/8469800/160749569-ad99965c-ddd5-43da-8728-4b79c37fc3f5.jpeg) | 168 | | ltc1qm6r32vjahm8wwd688enxnutks0jffc3kqg7ps5 | bc1qcuuc5r4jw38vf2eztsxag68papuwzd25chrepx | 0x02c22832bc115933Ac11388D5A91e0990eE84667 | 169 | 170 | ## License 171 | DefArmy is released under the MIT License. See the [bundled LICENSE](https://github.com/dev-masih/defarmy/blob/master/LICENSE) file for details. 172 | -------------------------------------------------------------------------------- /defarmy/defarmy.lua: -------------------------------------------------------------------------------- 1 | -- DefArmy 2 | -- This module helps you to create groups (army) of game objects (soldiers) and organize 3 | -- them in several different patterns or your customized pattern and manage moving and 4 | -- rotating game objects as a group. 5 | 6 | local M = {} 7 | 8 | --- Store army data and it's members. 9 | --- Structure: army_list[army_id] = { center_position, pattern_func, radius, rotation, member_padding, is_sticky, member_id_list[]:number } 10 | local army_list = {} 11 | 12 | --- Store soldiers data. 13 | --- Structure: soldier_list[soldier_id] = { army_id, position, direction, status, is_assigned, initial_direction, distance_order, debug_color } 14 | local soldier_list = {} 15 | 16 | local army_id_iterator = 0 17 | local soldier_id_iterator = 0 18 | 19 | -- Math functions 20 | local huge = math.huge 21 | local sqrt = math.sqrt 22 | local pow = math.pow 23 | local modf = math.modf 24 | local atan2 = math.atan2 25 | local max = math.max 26 | local fmod = math.fmod 27 | local ceil = math.ceil 28 | 29 | local SOLDIER_STATUS = {} 30 | SOLDIER_STATUS.OUTSIDE = hash("soldier_status_outside") 31 | SOLDIER_STATUS.WAITING = hash("soldier_status_waiting") 32 | SOLDIER_STATUS.INSIDE = hash("soldier_status_inside") 33 | SOLDIER_STATUS.INPLACE = hash("soldier_status_inplace") 34 | 35 | --- Army patterns 36 | M.PATTERN = {} 37 | M.PATTERN.BOTTOM_TO_TOP_SQUARE = hash("pattern_bottom_to_top_square") 38 | M.PATTERN.TOP_TO_BOTTOM_SQUARE = hash("pattern_top_to_bottom_square") 39 | M.PATTERN.TRIANGLE = hash("pattern_triangle") 40 | M.PATTERN.RHOMBUS_TALL = hash("pattern_rhombus_tall") 41 | M.PATTERN.RHOMBUS_SHORT = hash("pattern_rhombus_short") 42 | M.PATTERN.CUSTOMIZED = hash("pattern_customized") 43 | 44 | local function distance(source, destination) 45 | return sqrt(pow(source.x - destination.x, 2) + pow(source.y - destination.y, 2)) 46 | end 47 | 48 | local function debug_mark_soldier(position, soldier_id) 49 | if soldier_list[soldier_id].debug_color then 50 | local padding = army_list[soldier_list[soldier_id].army_id].member_padding / 2 51 | msg.post("@render:", "draw_line", { start_point = position + vmath.vector3(padding, padding, 0), end_point = position + vmath.vector3(-padding, -padding, 0), color = soldier_list[soldier_id].debug_color } ) 52 | msg.post("@render:", "draw_line", { start_point = position + vmath.vector3(-padding, padding, 0), end_point = position + vmath.vector3(padding, -padding, 0), color = soldier_list[soldier_id].debug_color } ) 53 | end 54 | end 55 | 56 | --- Army debugging. 57 | --- @param army_id (number) army id number 58 | --- @param debug_color (vector4) color used for debugging 59 | function M.army_debug_draw(army_id, debug_color) 60 | assert(army_id, "You must provide a army id") 61 | assert(debug_color, "You must provide a debug color") 62 | 63 | assert(army_list[army_id], ("Unknown army id %s"):format(tostring(army_id))) 64 | 65 | msg.post("@render:", "draw_line", { start_point = army_list[army_id].center_position + vmath.vector3(5, 5, 0), end_point = army_list[army_id].center_position + vmath.vector3(-5, -5, 0), color = debug_color } ) 66 | msg.post("@render:", "draw_line", { start_point = army_list[army_id].center_position + vmath.vector3(-5, 5, 0), end_point = army_list[army_id].center_position + vmath.vector3(5, -5, 0), color = debug_color } ) 67 | msg.post("@render:", "draw_line", { start_point = army_list[army_id].center_position + vmath.vector3(-5, 0, 0), end_point = army_list[army_id].center_position + vmath.vector3(5, 0, 0), color = debug_color } ) 68 | msg.post("@render:", "draw_line", { start_point = army_list[army_id].center_position + vmath.vector3(0, -5, 0), end_point = army_list[army_id].center_position + vmath.vector3(0, 5, 0), color = debug_color } ) 69 | 70 | local far_point = vmath.vector3(0, (army_list[army_id].radius + 1) * army_list[army_id].member_padding / 2, 0) 71 | far_point = vmath.rotate(army_list[army_id].rotation, far_point) + army_list[army_id].center_position 72 | 73 | msg.post("@render:", "draw_line", { start_point = army_list[army_id].center_position, end_point = far_point, color = debug_color } ) 74 | 75 | msg.post("@render:", "draw_line", { start_point = far_point + vmath.vector3(3, 3, 0), end_point = far_point + vmath.vector3(-3, -3, 0), color = debug_color } ) 76 | msg.post("@render:", "draw_line", { start_point = far_point + vmath.vector3(-3, 3, 0), end_point = far_point + vmath.vector3(3, -3, 0), color = debug_color } ) 77 | msg.post("@render:", "draw_line", { start_point = far_point + vmath.vector3(-3, 0, 0), end_point = far_point + vmath.vector3(3, 0, 0), color = debug_color } ) 78 | msg.post("@render:", "draw_line", { start_point = far_point + vmath.vector3(0, -3, 0), end_point = far_point + vmath.vector3(0, 3, 0), color = debug_color } ) 79 | end 80 | 81 | --- Turn on soldier position debugging. 82 | --- @param soldier_id (number) soldier id number 83 | --- @param debug_color (vector4) color used for debugging 84 | function M.soldier_debug_on(soldier_id, debug_color) 85 | assert(soldier_id, "You must provide a soldier id") 86 | assert(debug_color, "You must provide a debug color") 87 | 88 | assert(soldier_list[soldier_id], ("Unknown soldier id %s"):format(tostring(soldier_id))) 89 | 90 | soldier_list[soldier_id].debug_color = debug_color 91 | end 92 | 93 | --- Turn off soldier position debugging. 94 | --- @param soldier_id (number) soldier id number 95 | function M.soldier_debug_off(soldier_id) 96 | assert(soldier_id, "You must provide a soldier id") 97 | 98 | assert(soldier_list[soldier_id], ("Unknown soldier id %s"):format(tostring(soldier_id))) 99 | 100 | soldier_list[soldier_id].debug_color = nil 101 | end 102 | 103 | local function pattern_bottom_to_top_square(total_count) 104 | local army_schema = {} 105 | local row_width = ceil(sqrt(total_count)) 106 | local reminder = fmod(total_count, row_width) 107 | for i = 1, row_width do 108 | local value = modf(total_count / row_width) 109 | table.insert(army_schema, i, value) 110 | if reminder > 0 then 111 | army_schema[i] = army_schema[i] + 1 112 | reminder = reminder - 1 113 | end 114 | end 115 | return army_schema 116 | end 117 | 118 | local function pattern_top_to_bottom_square(total_count) 119 | local army_schema = {} 120 | local row_width = ceil(sqrt(total_count)) 121 | local reminder = fmod(total_count, row_width) 122 | local value = modf(total_count / row_width) 123 | for i = 1, value do 124 | table.insert(army_schema, row_width) 125 | end 126 | if reminder > 0 then 127 | table.insert(army_schema, reminder) 128 | end 129 | return army_schema 130 | end 131 | 132 | local function pattern_triangle(total_count) 133 | local army_schema = {} 134 | local sum = total_count 135 | local row_width = 1 136 | for i = 1, total_count / 2 + 1 do table.insert(army_schema, 0) end 137 | for i = modf(total_count / 2 + 1), 1, -1 do 138 | if sum < row_width then 139 | army_schema[i] = sum 140 | break 141 | else 142 | sum = sum - row_width 143 | army_schema[i] = row_width 144 | row_width = row_width + 1 145 | end 146 | end 147 | return army_schema 148 | end 149 | 150 | local function pattern_rhombus_tall(total_count) 151 | local army_schema = {} 152 | local sum = total_count 153 | local row_width = 0 154 | local middle = modf(total_count / 4) + 1 155 | for i = 1, total_count / 2 + 1 do table.insert(army_schema, 0) end 156 | for i = 1, total_count do 157 | army_schema[middle] = army_schema[middle] + 1 158 | sum = sum - 1 159 | if sum == 0 then break end 160 | for i = 1, row_width do 161 | army_schema[middle + i] = army_schema[middle + i] + 1 162 | sum = sum - 1 163 | if sum == 0 then break end 164 | army_schema[middle - i] = army_schema[middle - i] + 1 165 | sum = sum - 1 166 | if sum == 0 then break end 167 | end 168 | if sum == 0 then break end 169 | row_width = row_width + 1 170 | end 171 | return army_schema 172 | end 173 | 174 | local function pattern_rhombus_short(total_count) 175 | local army_schema = {} 176 | local row_width = 0 177 | local sum = total_count 178 | local middle = modf(total_count / 4) + 1 179 | for i = 1, total_count / 2 + 1 do table.insert(army_schema, 0) end 180 | for i = 1, total_count do 181 | army_schema[middle] = army_schema[middle] + 1 182 | sum = sum - 1 183 | if sum == 0 then break end 184 | for i = 1, modf(row_width) do 185 | army_schema[middle + i] = army_schema[middle + i] + 1 186 | sum = sum - 1 187 | if sum == 0 then break end 188 | army_schema[middle - i] = army_schema[middle - i] + 1 189 | sum = sum - 1 190 | if sum == 0 then break end 191 | end 192 | if sum == 0 then break end 193 | row_width = row_width + 0.5 194 | end 195 | return army_schema 196 | end 197 | 198 | --- Create a group of game objects (army) with specified member padding and pattern. 199 | --- @param army_center_position (vector3) army center position 200 | --- @param army_initial_rotation (quat) army initial rotation quat 201 | --- @param member_padding (number) padding between members 202 | --- @param is_sticky (boolean) is members glued to their places in army 203 | --- @param army_pattern (PATTERN) army pattern 204 | --- @param pattern_func (func|nil) army customized pattern function [nil] 205 | --- @return (number) army id 206 | function M.army_create(army_center_position, army_initial_rotation, member_padding, is_sticky, army_pattern, pattern_func) 207 | assert(army_center_position, "You must provide an army center position") 208 | assert(army_initial_rotation, "You must provide an army initial rotation") 209 | assert(member_padding, "You must provide an member padding") 210 | if is_sticky == nil then assert(is_sticky, "You must provide a stickiness") end 211 | assert(army_pattern, "You must provide an army pattern") 212 | 213 | local pattern_function 214 | if army_pattern == M.PATTERN.BOTTOM_TO_TOP_SQUARE then 215 | pattern_function = pattern_bottom_to_top_square 216 | elseif army_pattern == M.PATTERN.TOP_TO_BOTTOM_SQUARE then 217 | pattern_function = pattern_top_to_bottom_square 218 | elseif army_pattern == M.PATTERN.TRIANGLE then 219 | pattern_function = pattern_triangle 220 | elseif army_pattern == M.PATTERN.RHOMBUS_TALL then 221 | pattern_function = pattern_rhombus_tall 222 | elseif army_pattern == M.PATTERN.RHOMBUS_SHORT then 223 | pattern_function = pattern_rhombus_short 224 | elseif army_pattern == M.PATTERN.CUSTOMIZED then 225 | pattern_function = pattern_func 226 | assert(pattern_function, "You must provide an army pattern function") 227 | else 228 | assert(false, ("Unknown army pattern %s"):format(tostring(army_pattern))) 229 | end 230 | 231 | army_id_iterator = army_id_iterator + 1 232 | local army_id = army_id_iterator 233 | army_list[army_id] = { center_position = army_center_position, member_padding = member_padding, radius = member_padding, 234 | rotation = army_initial_rotation, pattern_func = pattern_function, is_sticky = is_sticky, member_id_list = {} } 235 | return army_id 236 | end 237 | 238 | --- Return an army members id. 239 | --- @param army_id (number) army id number 240 | --- @return (table) list of soldier's id that were members of that army 241 | function M.army_members(army_id) 242 | assert(army_id, "You must provide an army id") 243 | 244 | assert(army_list[army_id], ("Unknown army id %s"):format(tostring(army_id))) 245 | 246 | return army_list[army_id].member_id_list 247 | end 248 | 249 | --- Remove a grouping of game objects (army) and release it's members. 250 | --- @param army_id (number) army id number 251 | function M.army_remove(army_id) 252 | assert(army_id, "You must provide an army id") 253 | 254 | assert(army_list[army_id], ("Unknown army id %s"):format(tostring(army_id))) 255 | 256 | for i = 1, #army_list[army_id].member_id_list do 257 | soldier_list[army_list[army_id].member_id_list[i]].army_id = 0 258 | soldier_list[army_list[army_id].member_id_list[i]].status = SOLDIER_STATUS.OUTSIDE 259 | soldier_list[army_list[army_id].member_id_list[i]].is_assigned = false 260 | end 261 | army_list[army_id] = nil 262 | end 263 | 264 | local function count_soldiers_not_outside_army(army_id) 265 | local count = 0 266 | for _, soldier_id in pairs(army_list[army_id].member_id_list) do 267 | if soldier_list[soldier_id].status ~= SOLDIER_STATUS.OUTSIDE then 268 | count = count + 1 269 | end 270 | end 271 | return count 272 | end 273 | 274 | local function rearenge_army(army_id, prefered_start_position) 275 | -- create army schema according to pattern function 276 | local total_count = count_soldiers_not_outside_army(army_id) 277 | local army_schema = army_list[army_id].pattern_func(total_count) 278 | 279 | -- remove empty rows in schema 280 | local counter = 0 281 | local j, len = 1, #army_schema 282 | for i = 1, len do 283 | if (army_schema[i] ~= 0) then 284 | counter = counter + army_schema[i] 285 | if (i ~= j) then 286 | army_schema[j] = army_schema[i] 287 | army_schema[i] = nil 288 | end 289 | j = j + 1 290 | else 291 | army_schema[i] = nil 292 | end 293 | end 294 | assert(counter == total_count, "Pattern is not add up to total count") 295 | 296 | -- assign army radius 297 | army_list[army_id].radius = #army_schema 298 | 299 | -- Store placeholder data. 300 | -- Structure: placeholder_list[placeholder_id] = { position, is_occupied, distance_order } 301 | local placeholder_list = {} 302 | 303 | -- create pattern placeholder from schema 304 | local reltive_x = 0 305 | local reltive_y = 0 306 | local padding = army_list[army_id].member_padding 307 | for i = 1, #army_schema do 308 | for j = 1, army_schema[i] do 309 | local mod_cell = army_schema[i] % 2 310 | local mod_row = #army_schema % 2 311 | 312 | if mod_cell == 0 then 313 | local rel_x = j - (army_schema[i] / 2) 314 | reltive_x = (rel_x - 1) * padding + (padding / 2) 315 | else 316 | local rel_x = j - ((army_schema[i] + 1) / 2) 317 | reltive_x = rel_x * padding 318 | end 319 | 320 | if mod_row == 0 then 321 | local rel_y = i - (#army_schema / 2) 322 | reltive_y = (rel_y - 1) * padding + (padding / 2) 323 | else 324 | local rel_y = i - ((#army_schema + 1) / 2) 325 | reltive_y = rel_y * padding 326 | end 327 | 328 | table.insert(placeholder_list, { position = vmath.vector3(reltive_x, reltive_y, 0), is_occupied = false, distance_order = 0 }) 329 | end 330 | army_list[army_id].radius = max(army_list[army_id].radius, army_schema[i]) 331 | end 332 | 333 | -- restart soldiers assigned flag 334 | local waiting_soldier_member_id = 1 335 | for i = 1, #army_list[army_id].member_id_list do 336 | soldier_list[army_list[army_id].member_id_list[i]].is_assigned = false 337 | if soldier_list[army_list[army_id].member_id_list[i]].status == SOLDIER_STATUS.INPLACE then 338 | soldier_list[army_list[army_id].member_id_list[i]].status = SOLDIER_STATUS.INSIDE 339 | elseif soldier_list[army_list[army_id].member_id_list[i]].status == SOLDIER_STATUS.WAITING then 340 | waiting_soldier_member_id = i 341 | end 342 | end 343 | 344 | -- allow the waiting soldier to first pick nearest placeholder if prefered_start_position was not set 345 | if not prefered_start_position then 346 | local min_dist = huge 347 | local min_placeholder_index = 1 348 | 349 | for i = 1, #placeholder_list do 350 | local new_dist = distance(vmath.rotate(army_list[army_id].rotation, placeholder_list[i].position), 351 | soldier_list[army_list[army_id].member_id_list[waiting_soldier_member_id]].position) 352 | if new_dist < min_dist then 353 | min_dist = new_dist 354 | min_placeholder_index = i 355 | end 356 | end 357 | 358 | prefered_start_position = placeholder_list[min_placeholder_index].position 359 | soldier_list[army_list[army_id].member_id_list[waiting_soldier_member_id]].position = prefered_start_position 360 | soldier_list[army_list[army_id].member_id_list[waiting_soldier_member_id]].is_assigned = true 361 | placeholder_list[min_placeholder_index].is_occupied = true 362 | end 363 | 364 | -- calculate each placeholder distance_order in relative to first picked placeholder 365 | for i = 1, #placeholder_list do 366 | placeholder_list[i].distance_order = distance(placeholder_list[i].position, prefered_start_position) 367 | end 368 | 369 | -- sort placeholders from high to low distance_order 370 | table.sort(placeholder_list, function(x, y) return x.distance_order > y.distance_order end) 371 | 372 | -- calculate each soldier distance_order in relative to first picked soldier 373 | for i = 1, #army_list[army_id].member_id_list do 374 | soldier_list[army_list[army_id].member_id_list[i]].distance_order = distance(soldier_list[army_list[army_id].member_id_list[i]].position, 375 | prefered_start_position) 376 | end 377 | 378 | -- sort soldiers from high to low distance_order 379 | table.sort(army_list[army_id].member_id_list, function(x, y) return soldier_list[x].distance_order > soldier_list[y].distance_order end) 380 | 381 | -- assign each placeholder to it's nearest soldier 382 | for i = 1, #placeholder_list do 383 | if not placeholder_list[i].is_occupied then 384 | local min_dist = huge 385 | local min_soldier_id = 1 386 | 387 | for j = 1, #army_list[army_id].member_id_list do 388 | if not soldier_list[army_list[army_id].member_id_list[j]].is_assigned then 389 | local dist = distance(placeholder_list[i].position, 390 | soldier_list[army_list[army_id].member_id_list[j]].position) 391 | 392 | if soldier_list[army_list[army_id].member_id_list[j]].status ~= SOLDIER_STATUS.OUTSIDE then 393 | local far_dist = distance(soldier_list[army_list[army_id].member_id_list[j]].position, prefered_start_position) - 394 | distance(placeholder_list[i].position, prefered_start_position) 395 | dist = dist - far_dist 396 | end 397 | 398 | if dist < min_dist then 399 | min_dist = dist 400 | min_soldier_id = j 401 | end 402 | end 403 | end 404 | 405 | soldier_list[army_list[army_id].member_id_list[min_soldier_id]].position = placeholder_list[i].position 406 | soldier_list[army_list[army_id].member_id_list[min_soldier_id]].is_assigned = true 407 | placeholder_list[i].is_occupied = true 408 | end 409 | end 410 | end 411 | 412 | --- Update an army center position. 413 | --- @param army_id (number) army id number 414 | --- @param army_center_position (vector3) new army center position 415 | function M.army_update_position(army_id, army_center_position) 416 | assert(army_id, "You must provide an army id") 417 | assert(army_center_position, "You must provide an army center position") 418 | 419 | assert(army_list[army_id], ("Unknown army id %s"):format(tostring(army_id))) 420 | 421 | army_list[army_id].center_position = army_center_position 422 | end 423 | 424 | --- Update an army rotation. 425 | --- @param army_id (number) army id number 426 | --- @param army_rotation (quat) new army rotation quat 427 | function M.army_update_rotation(army_id, army_rotation) 428 | assert(army_id, "You must provide an army id") 429 | assert(army_rotation, "You must provide an army rotation") 430 | 431 | assert(army_list[army_id], ("Unknown army id %s"):format(tostring(army_id))) 432 | 433 | army_list[army_id].rotation = army_rotation 434 | end 435 | 436 | --- Update an army pattern. 437 | --- @param army_id (number) army id number 438 | --- @param army_pattern (PATTERN) new army pattern 439 | --- @param pattern_func (func|nil) army customized pattern function [nil] 440 | function M.army_update_pattern(army_id, army_pattern, pattern_func) 441 | assert(army_id, "You must provide an army id") 442 | assert(army_pattern, "You must provide an army pattern") 443 | 444 | assert(army_list[army_id], ("Unknown army id %s"):format(tostring(army_id))) 445 | 446 | local pattern_function 447 | if army_pattern == M.PATTERN.BOTTOM_TO_TOP_SQUARE then 448 | pattern_function = pattern_bottom_to_top_square 449 | elseif army_pattern == M.PATTERN.TOP_TO_BOTTOM_SQUARE then 450 | pattern_function = pattern_top_to_bottom_square 451 | elseif army_pattern == M.PATTERN.TRIANGLE then 452 | pattern_function = pattern_triangle 453 | elseif army_pattern == M.PATTERN.RHOMBUS_TALL then 454 | pattern_function = pattern_rhombus_tall 455 | elseif army_pattern == M.PATTERN.RHOMBUS_SHORT then 456 | pattern_function = pattern_rhombus_short 457 | elseif army_pattern == M.PATTERN.CUSTOMIZED then 458 | pattern_function = pattern_func 459 | assert(pattern_function, "You must provide an army pattern function") 460 | else 461 | assert(false, ("Unknown army pattern %s"):format(tostring(army_pattern))) 462 | end 463 | 464 | army_list[army_id].pattern_func = pattern_function 465 | rearenge_army(army_id, vmath.vector3(0, 0, 0)) 466 | end 467 | 468 | --- Update an army stickiness. 469 | --- @param army_id (number) army id number 470 | --- @param is_sticky (boolean) is members glued to their places in army 471 | function M.army_update_stickiness(army_id, is_sticky) 472 | assert(army_id, "You must provide an army id") 473 | if is_sticky == nil then assert(is_sticky, "You must provide a stickiness") end 474 | 475 | assert(army_list[army_id], ("Unknown army id %s"):format(tostring(army_id))) 476 | 477 | army_list[army_id].is_sticky = is_sticky 478 | 479 | for i = 1, #army_list[army_id].member_id_list do 480 | if soldier_list[army_list[army_id].member_id_list[i]].status == SOLDIER_STATUS.INPLACE then 481 | soldier_list[army_list[army_id].member_id_list[i]].status = SOLDIER_STATUS.INSIDE 482 | end 483 | end 484 | end 485 | 486 | --- Create a new soldier and optionally assign it to an army. 487 | --- @param position (vector3) soldier current position 488 | --- @param initial_direction (vector3) soldier initial direction vecotr 489 | --- @param army_id (number|nil) optional army id number 490 | --- @return (number) soldier id 491 | function M.soldier_create(position, initial_direction, army_id) 492 | assert(position, "You must provide a position") 493 | assert(initial_direction, "You must provide an initial direction") 494 | 495 | if army_id then 496 | assert(army_list[army_id], ("Unknown army id %s"):format(tostring(army_id))) 497 | else 498 | army_id = 0 499 | end 500 | 501 | soldier_id_iterator = soldier_id_iterator + 1 502 | local soldier_id = soldier_id_iterator 503 | 504 | soldier_list[soldier_id] = { army_id = army_id, position = position, direction = initial_direction, 505 | status = SOLDIER_STATUS.OUTSIDE, is_assigned = false, 506 | initial_direction = initial_direction, distance_order = 0 } 507 | 508 | if army_id ~= 0 then 509 | table.insert(army_list[army_id].member_id_list, soldier_id) 510 | end 511 | 512 | return soldier_id 513 | end 514 | 515 | --- Assign an existing soldier to a given army. 516 | --- @param soldier_id (number) soldier id number 517 | --- @param army_id (number) army id number 518 | function M.soldier_join_army(soldier_id, army_id) 519 | assert(soldier_id, "You must provide a soldier id") 520 | assert(army_id, "You must provide an army id") 521 | 522 | assert(soldier_list[soldier_id], ("Unknown soldier id %s"):format(tostring(soldier_id))) 523 | assert(army_list[army_id], ("Unknown army id %s"):format(tostring(army_id))) 524 | 525 | local last_army_id = soldier_list[soldier_id].army_id 526 | if last_army_id ~= 0 then 527 | for i = 1, #army_list[last_army_id].member_id_list do 528 | if soldier_id == army_list[last_army_id].member_id_list[i] then 529 | table.remove(army_list[last_army_id].member_id_list, i) 530 | break 531 | end 532 | end 533 | end 534 | 535 | soldier_list[soldier_id].army_id = army_id 536 | table.insert(army_list[army_id].member_id_list, soldier_id) 537 | end 538 | 539 | --- Deassign a soldier from army. 540 | --- @param soldier_id (number) soldier id number 541 | function M.soldier_leave_army(soldier_id) 542 | assert(soldier_id, "You must provide a soldier id") 543 | 544 | assert(soldier_list[soldier_id], ("Unknown soldier id %s"):format(tostring(soldier_id))) 545 | 546 | if soldier_list[soldier_id].army_id ~= 0 then 547 | local army_id = soldier_list[soldier_id].army_id 548 | 549 | soldier_list[soldier_id].army_id = 0 550 | soldier_list[soldier_id].status = SOLDIER_STATUS.OUTSIDE 551 | soldier_list[soldier_id].is_assigned = false 552 | 553 | for i = 1, #army_list[army_id].member_id_list do 554 | if soldier_id == army_list[army_id].member_id_list[i] then 555 | table.remove(army_list[army_id].member_id_list, i) 556 | break 557 | end 558 | end 559 | 560 | rearenge_army(army_id, soldier_list[soldier_id].postion) 561 | end 562 | end 563 | 564 | --- Completely remove a soldier. 565 | --- @param soldier_id (number) soldier id number 566 | function M.soldier_remove(soldier_id) 567 | assert(soldier_id, "You must provide a soldier id") 568 | 569 | assert(soldier_list[soldier_id], ("Unknown soldier id %s"):format(tostring(soldier_id))) 570 | 571 | local army_id = soldier_list[soldier_id].army_id 572 | local last_position = soldier_list[soldier_id].postion 573 | soldier_list[soldier_id] = nil 574 | 575 | if army_id ~= 0 then 576 | for i = 1, #army_list[army_id].member_id_list do 577 | if soldier_id == army_list[army_id].member_id_list[i] then 578 | table.remove(army_list[army_id].member_id_list, i) 579 | break 580 | end 581 | end 582 | rearenge_army(army_id, last_position) 583 | end 584 | end 585 | 586 | local function normalize(vec) 587 | if vec.x == 0 and vec.y == 0 then 588 | return vmath.vector3() 589 | else 590 | return vmath.normalize(vec) 591 | end 592 | end 593 | 594 | --- Calculate a soldier next postion and rotation. 595 | --- @param soldier_id (number) soldier id number 596 | --- @param current_position (vecotr3) soldier current position 597 | --- @param speed (number) soldier speed 598 | --- @param threshold (number|nil) optional soldier placement detection threshold [1] 599 | --- @return (vector3) soldier next position 600 | --- @return (quat) soldier next rotation 601 | function M.soldier_move(soldier_id, current_position, speed, threshold) 602 | assert(soldier_id, "You must provide a soldier id") 603 | assert(current_position, "You must provide a current position") 604 | assert(speed, "You must provide a speed") 605 | 606 | -- if soldier removed 607 | if soldier_list[soldier_id] == nil then 608 | return nil, nil 609 | end 610 | 611 | -- if soldier is not in any army 612 | if soldier_list[soldier_id].army_id == 0 then 613 | local rotation = vmath.quat_rotation_z(atan2(soldier_list[soldier_id].direction.y, soldier_list[soldier_id].direction.x) 614 | - atan2(soldier_list[soldier_id].initial_direction.y, soldier_list[soldier_id].initial_direction.x)) 615 | 616 | debug_mark_soldier(current_position, soldier_id) 617 | return current_position, rotation 618 | end 619 | 620 | local army_id = soldier_list[soldier_id].army_id 621 | 622 | if soldier_list[soldier_id].status == SOLDIER_STATUS.OUTSIDE then 623 | if distance(army_list[army_id].center_position, current_position) < (army_list[army_id].radius + 3) * army_list[army_id].member_padding / 2 then 624 | -- soldier is inside army area 625 | soldier_list[soldier_id].status = SOLDIER_STATUS.WAITING 626 | soldier_list[soldier_id].position = soldier_list[soldier_id].position - army_list[army_id].center_position 627 | 628 | rearenge_army(army_id) 629 | 630 | soldier_list[soldier_id].status = SOLDIER_STATUS.INSIDE 631 | local direction_vector = vmath.rotate(army_list[army_id].rotation, soldier_list[soldier_id].position) + army_list[army_id].center_position - current_position 632 | direction_vector.z = 0 633 | direction_vector = normalize(direction_vector) 634 | local position = (current_position + direction_vector * speed) 635 | 636 | local rotation_vector = vmath.lerp(0.2 * speed, soldier_list[soldier_id].direction, direction_vector) 637 | local rotation = vmath.quat_rotation_z(atan2(rotation_vector.y, rotation_vector.x) - atan2(soldier_list[soldier_id].initial_direction.y, soldier_list[soldier_id].initial_direction.x)) 638 | soldier_list[soldier_id].direction = rotation_vector 639 | 640 | return position, rotation 641 | else 642 | -- soldier is outside of army area 643 | local dist_partial = (army_list[army_id].radius + 2) * army_list[army_id].member_padding / 2 644 | local dist_total = distance(current_position, army_list[army_id].center_position) 645 | soldier_list[soldier_id].position = vmath.lerp(dist_partial / dist_total, army_list[army_id].center_position, current_position) 646 | 647 | debug_mark_soldier(soldier_list[soldier_id].position, soldier_id) 648 | 649 | local direction_vector = soldier_list[soldier_id].position - current_position 650 | direction_vector.z = 0 651 | direction_vector = normalize(direction_vector) 652 | local position = (current_position + direction_vector * speed) 653 | 654 | local rotation_vector = vmath.lerp(0.2 * speed, soldier_list[soldier_id].direction, direction_vector) 655 | local rotation = vmath.quat_rotation_z(atan2(rotation_vector.y, rotation_vector.x) - atan2(soldier_list[soldier_id].initial_direction.y, soldier_list[soldier_id].initial_direction.x)) 656 | soldier_list[soldier_id].direction = rotation_vector 657 | 658 | return position, rotation 659 | end 660 | elseif soldier_list[soldier_id].status == SOLDIER_STATUS.INPLACE then 661 | local position = vmath.rotate(army_list[army_id].rotation, soldier_list[soldier_id].position) + army_list[army_id].center_position 662 | 663 | local rotated_initial_direction = vmath.rotate(army_list[army_id].rotation, soldier_list[soldier_id].initial_direction) 664 | local rotation_vector = vmath.lerp(0.2 * speed, soldier_list[soldier_id].direction, rotated_initial_direction) 665 | local rotation = vmath.quat_rotation_z(atan2(rotation_vector.y, rotation_vector.x) - atan2(soldier_list[soldier_id].initial_direction.y, soldier_list[soldier_id].initial_direction.x)) 666 | soldier_list[soldier_id].direction = rotation_vector 667 | 668 | debug_mark_soldier(position, soldier_id) 669 | return position, rotation 670 | else 671 | local soldier_position = vmath.rotate(army_list[army_id].rotation, soldier_list[soldier_id].position) + army_list[army_id].center_position 672 | if not threshold then threshold = 1 end 673 | if distance(soldier_position, current_position) > threshold then 674 | -- soldier is not in placeholder 675 | local direction_vector = vmath.rotate(army_list[army_id].rotation, soldier_list[soldier_id].position) + army_list[army_id].center_position 676 | 677 | debug_mark_soldier(direction_vector, soldier_id) 678 | 679 | direction_vector = direction_vector - current_position 680 | direction_vector.z = 0 681 | direction_vector = normalize(direction_vector) 682 | local position = (current_position + direction_vector * speed) 683 | 684 | local rotation_vector = vmath.lerp(0.2 * speed, soldier_list[soldier_id].direction, direction_vector) 685 | local rotation = vmath.quat_rotation_z(atan2(rotation_vector.y, rotation_vector.x) - atan2(soldier_list[soldier_id].initial_direction.y, soldier_list[soldier_id].initial_direction.x)) 686 | soldier_list[soldier_id].direction = rotation_vector 687 | 688 | return position, rotation 689 | else 690 | -- soldier is in placeholder 691 | if army_list[army_id].is_sticky then 692 | soldier_list[soldier_id].status = SOLDIER_STATUS.INPLACE 693 | end 694 | 695 | local position = vmath.rotate(army_list[army_id].rotation, soldier_list[soldier_id].position) + army_list[army_id].center_position 696 | 697 | local rotated_initial_direction = vmath.rotate(army_list[army_id].rotation, soldier_list[soldier_id].initial_direction) 698 | local rotation_vector = vmath.lerp(0.2 * speed, soldier_list[soldier_id].direction, rotated_initial_direction) 699 | local rotation = vmath.quat_rotation_z(atan2(rotation_vector.y, rotation_vector.x) - atan2(soldier_list[soldier_id].initial_direction.y, soldier_list[soldier_id].initial_direction.x)) 700 | soldier_list[soldier_id].direction = rotation_vector 701 | 702 | debug_mark_soldier(position, soldier_id) 703 | return position, rotation 704 | end 705 | end 706 | end 707 | 708 | return M --------------------------------------------------------------------------------