
Director object, which will be a regular object (not one that inherits from a GameObject, but that used in the Area). In it we put our code: Director = Object:extend() function Director:new(stage) self.stage = stage end function Director:update(dt) end function Stage:new() ... self.director = Director(self) end function Stage:update(dt) self.director:update(dt) ... end stage.area . The director will also need access to the time, so he needs the appropriate update.difficulty and a few auxiliary ones to control the time for increasing this attribute. This temporary change code will be the same as that used in the acceleration or cycle mechanisms of the Player. function Director:new(...) ... self.difficulty = 1 self.round_duration = 22 self.round_timer = 0 end function Director:update(dt) self.round_timer = self.round_timer + dt if self.round_timer > self.round_duration then self.round_timer = 0 self.difficulty = self.difficulty + 1 self:setEnemySpawnsForThisRound() end end difficulty increases every 22 seconds in accordance with rule 1. We can also call the setEnemySpawnsForThisRound function, which will execute rule 2. function Director:new(...) ... self.difficulty_to_points = {} self.difficulty_to_points[1] = 16 for i = 2, 1024, 4 do self.difficulty_to_points[i] = self.difficulty_to_points[i-1] + 8 self.difficulty_to_points[i+1] = self.difficulty_to_points[i] self.difficulty_to_points[i+2] = math.floor(self.difficulty_to_points[i+1]/1.5) self.difficulty_to_points[i+3] = math.floor(self.difficulty_to_points[i+2]*2) end end -
1 - 16
2 - 24
3 - 24
4 - 16
5 - 32
6 - 40
7 - 40
8 - 26
9 - 56
10 - 64
11 - 64
12 - 42
13 - 84 function Director:new(...) ... self.enemy_to_points = { ['Rock'] = 1, ['Shooter'] = 2, } end setEnemySpawnsForThisRound function. But before we get to it, I want to introduce you to a very important construction related to chances and probabilities. We will use it throughout the game.love.math.random - to make it generate a value from 1 to 100, and then check where the value is. If it is less than 25, then we say that event X occurred, if from 25 to 50, then event Y, and if greater than 50, then event Z.love.math.random 100 times, X will occur exactly 25 times. If we do it 10,000 times, then the probability will probably be close to 25%, but often we need to have more control over the situation. Therefore, the simple solution is to create what I call a “list of changes” ( chanceList ).next function. This function will give us a random value from the list, say, 28. This means that event Y will occur. The difference is that when we call a function, we also remove the selected random value from the list. In essence, this means that 28 will never again fall out and the event Y now has a slightly lower probability than the other two events. The more often we call next , the more empty the list becomes, and when it becomes completely empty, we simply recreate all 100 numbers again. events = chanceList({'X', 25}, {'Y', 25}, {'Z', 50}) for i = 1, 100 do print(events:next()) --> will print X 25 times, Y 25 times and Z 50 times end events = chanceList({'X', 5}, {'Y', 5}, {'Z', 10}) for i = 1, 20 do print(events:next()) --> will print X 5 times, Y 5 times and Z 10 times end events = chanceList({'X', 5}, {'Y', 5}, {'Z', 10}) for i = 1, 40 do print(events:next()) --> will print X 10 times, Y 10 times and Z 20 times end utils.lua function in chanceList and use some of the Lua features that we covered in the second part of this tutorial.next function. The easiest way to achieve this is to simply give this object a simple table that looks like this: function chanceList(...) return { next = function(self) end } end ... which we will process in more detail later. Then we return a table that has a function next . This function receives self as the only argument, since we know that calling the function with : transfers as its first argument itself. That is, inside the function next self refers to a table that is returned by chanceList .next function, we can define several attributes that this function will have. The first is the chance_list itself, which will contain the values ​​returned by the next function: function chanceList(...) return { chance_list = {}, next = function(self) end } end next function. In our example: events = chanceList({'X', 3}, {'Y', 3}, {'Z', 4}) chance_list attribute will look something like this: .chance_list = {'X', 'X', 'X', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z'} chance_definitions , which will store all the values ​​and probabilities passed to the chanceList function: function chanceList(...) return { chance_list = {}, chance_definitions = {...}, next = function(self) end } end next function. We need two behaviors from this function: it must return a random value according to the probabilities described in chance_definitions , and also restore the internal chance_list when it reaches zero elements. Assuming the list is filled with elements, we can implement the first behavior as follows: next = function(self) return table.remove(self.chance_list, love.math.random(1, #self.chance_list)) end chance_list table and return it. Due to the internal structure of the elements, all restrictions are satisfied.chance_list table chance_list . It turns out that we can use to build the list the same code that will be used to empty it. It will look like this: next = function(self) if #self.chance_list == 0 then for _, chance_definition in ipairs(self.chance_definitions) do for i = 1, chance_definition[2] do table.insert(self.chance_list, chance_definition[1]) end end end return table.remove(self.chance_list, love.math.random(1, #self.chance_list)) end chance_list zero. This will be true when you first call next , and also when the list is empty after a lot of calls. If this is true, then we begin to bypass the chance_definitions table, which contains tables that we call chance_definition with the values ​​and probabilities of these values. That is, if we called the function chanceList like this: events = chanceList({'X', 3}, {'Y', 3}, {'Z', 4}) chance_definitions table looks like this: .chance_definitions = {{'X', 3}, {'Y', 3}, {'Z', 4}} chance_definitions[1] this list, chance_definitions[1] refers to the value, and chance_definitions[2] refers to the number of times the value is found in the chance_list . Knowing this, to fill the list we simply insert chance_definition[1] into chance_list chance_definition[2] times. And we do the same for all chance_definitions tables. events = chanceList({'X', 2}, {'Y', 2}, {'Z', 4}) for i = 1, 16 do print(events:next()) end setEnemySpawnsForThisRound . The first thing we want to do is determine the likelihood of creating each enemy. Different levels of complexity will have different probabilities of creation, and we will need to set at least the first few difficulties manually. Then the subsequent difficulties will be set randomly, because they will have so many points that the player will be too overloaded anyway. function Director:new(...) ... self.enemy_spawn_chances = { [1] = chanceList({'Rock', 1}), [2] = chanceList({'Rock', 8}, {'Shooter', 4}), [3] = chanceList({'Rock', 8}, {'Shooter', 8}), [4] = chanceList({'Rock', 4}, {'Shooter', 8}), } end function Director:new(...) ... for i = 5, 1024 do self.enemy_spawn_chances[i] = chanceList( {'Rock', love.math.random(2, 12)}, {'Shooter', love.math.random(2, 12)} ) end end setEnemySpawnsForThisRound function. The first thing we do is use the creation of enemies in the list according to the enemy_spawn_chances table until we run out of points for the current difficulty level. It might look something like this: function Director:setEnemySpawnsForThisRound() local points = self.difficulty_to_points[self.difficulty] -- Find enemies local enemy_list = {} while points > 0 do local enemy = self.enemy_spawn_chances[self.difficulty]:next() points = points - self.enemy_to_points[enemy] table.insert(enemy_list, enemy) end end enemy_list will be filled with the rows Rock and Shooter according to the probabilities of the current complexity. We put this code inside a while loop, which stops execution when the number of remaining points reaches zero.enemy_list table will be created. It might look something like this: function Director:setEnemySpawnsForThisRound() ... -- Find enemies spawn times local enemy_spawn_times = {} for i = 1, #enemy_list do enemy_spawn_times[i] = random(0, self.round_duration) end table.sort(enemy_spawn_times, function(a, b) return a < b end) end enemy_list assigned a random number between 0 and round_duration , stored in the enemy_spawn_times table. We will sort this table so that the values ​​are in order. That is, if our enemy_list table looks like this: .enemy_list = {'Rock', 'Shooter', 'Rock'} enemy_spawn_times table will look like this: .enemy_spawn_times = {2.5, 8.4, 14.8} timer:after : function Director:setEnemySpawnsForThisRound() ... -- Set spawn enemy timer for i = 1, #enemy_spawn_times do self.timer:after(enemy_spawn_times[i], function() self.stage.area:addGameObject(enemy_list[i]) end) end end enemy_spawn_times and set the creation of enemies from enemy_list in accordance with the numbers in the first table. The last thing to do is to call this function once when starting the game: function Director:new(...) ... self:setEnemySpawnsForThisRound() end function Director:new(...) ... self.resource_spawn_chances = chanceList({'Boost', 28}, {'HP', 14}, {'SkillPoint', 58}) end finish function in the Stage class, which uses gotoRoom to switch to another Stage room. This function looks like this: function Stage:finish() timer:after(1, function() gotoRoom('Stage') end) end gotoRoom will deal with the destruction of the previous instance of the Stage and the creation of a new one so that we do not have to destroy the objects manually. The only thing we need to take care of is to set the player attribute in the Stage class to nil in its destroy function, otherwise the Player object will not be deleted correctly.finish function can be called from the Player object itself when the player dies: function Player:die() ... current_room:finish() end current_room is a global variable containing the current active room, and when the die function is called for the player, the only active room will be the Stage, so everything will work as it should. If we run the code, it will work as we expected. If the player dies, then after 1 second a new room of the Stage starts and you can start the game again.score attribute in the Stage class, which will track the points we collect. After the game is completed, this score will be saved somewhere, and we will be able to compare it with previous records. For now, we'll skip the part with the comparison of points and focus only on the basics. function Stage:new() ... self.score = 0 end function Player:addAmmo(amount) self.ammo = math.min(self.ammo + amount, self.max_ammo) current_room.score = current_room.score + 50 end addAmmo), and then simply add here the code that changes the account. Just as we did for the function finish, here we can access the Stage room through current_room, because the Stage room is the only one that can be active in this case.
function Stage:draw() love.graphics.setCanvas(self.main_canvas) love.graphics.clear() ... love.graphics.setFont(self.font) -- Score love.graphics.setColor(default_color) love.graphics.print(self.score, gw - 20, 10, 0, 1, 1, math.floor(self.font:getWidth(self.score)/2), self.font:getHeight()/2) love.graphics.setColor(255, 255, 255) love.graphics.setCanvas() ... end depthso that it is drawn on top of everything, or we can just draw over the Area in the canvas main_canvasthat the Stage room uses. I decided to choose the second method, but they both work.love.graphics.setFont: function Stage:new() ... self.font = fonts.m5x7_16 end function Stage:draw() ... love.graphics.setCanvas(self.main_canvas) love.graphics.clear() ... -- HP local r, g, b = unpack(hp_color) local hp, max_hp = self.player.hp, self.player.max_hp love.graphics.setColor(r, g, b) love.graphics.rectangle('fill', gw/2 - 52, gh - 16, 48*(hp/max_hp), 4) love.graphics.setColor(r - 32, g - 32, b - 32) love.graphics.rectangle('line', gw/2 - 52, gh - 16, 48, 4) love.graphics.setCanvas() end gw/2 - 52, gh - 16, and its width will be equal 48. That is, both bars will be drawn relative to the center of the screen with a small gap of 8 pixels. From this we can also understand that the position of the bar on the right will be gw/2 + 4, gh - 16.hp_color, and its outline will be a rectangle with color hp_color - 32. Since we cannot subtract from the table, we need to divide the table hp_colorinto separate components and subtract from each.hp/max_hp. For example, if it hp/max_hpis 1, then HP is full. If 0.5, thenhphas half the size max_hp. If 0.25, then 1/4 of the size. And if we multiply this ratio by the width that the strip should have, then we get a beautiful visualization of the player’s HP fill. If we implement it, the game will look like this:
function Stage:draw() ... love.graphics.setCanvas(self.main_canvas) love.graphics.clear() ... -- HP ... love.graphics.print('HP', gw/2 - 52 + 24, gh - 24, 0, 1, 1, math.floor(self.font:getWidth('HP')/2), math.floor(self.font:getHeight()/2)) love.graphics.setCanvas() end gw/2 - 52 + 24, that is, relative to the center of the strip, that is, we need to shift it to the width of this text typed in this font (and we do this with the help of the function getWidth). function Stage:draw() ... love.graphics.setCanvas(self.main_canvas) love.graphics.clear() ... -- HP ... love.graphics.print(hp .. '/' .. max_hp, gw/2 - 52 + 24, gh - 6, 0, 1, 1, math.floor(self.font:getWidth(hp .. '/' .. max_hp)/2), math.floor(self.font:getHeight()/2)) love.graphics.setCanvas() end gw/2 - 52, 16.gw/2 + 4, 16.gw/2 + 4, gh - 16.— , , . , , ( ), .
, , , . , , .
all_colorscontaining a list of all colors. These colors never change and never write to this table. At the same time, it is read from different objects, for example, when we need to get a random color.current_room. Already her name itself implies some uncertainty, since the current room can be an object Stage, or an object Console, or an object SkillTree, or any other type of Room object. I decided that for our game this would be an acceptable decrease in clarity.all_colorsinterfere with us in the long run. As long as we monitor variables like current_roomand do not allow them to be too multiple or too confusing (for example, it current_roomchanges only when a function is called gotoRoom), we can control everything. local direction = table.random({-1, 1}) self.x = gw/2 + direction*(gw/2 + 48) self.y = random(16, gh - 16) spawner_component = SpawnerComponent(), and it will magically process for us all the logic of the spawn of objects. In our example, these are three lines, but the same logic applies to more complex behaviors. local direction = table.random({-1, 1}) self.x = gw/2 + direction*(gw/2 + 48) self.y = gh/2 function Rock:new(area, x, y, opts) ... self.w, self.h = 8, 8 self.collider = self.area.world:newPolygonCollider(createIrregularPolygon(8)) self.collider:setPosition(self.x, self.y) self.collider:setObject(self) self.collider:setCollisionClass('Enemy') self.collider:setFixedRotation(false) self.v = -direction*random(20, 40) self.collider:setLinearVelocity(self.v, 0) self.collider:applyAngularImpulse(random(-100, 100)) ... end function Rock:update(dt) ... self.collider:setLinearVelocity(self.v, 0) end 



max_value , current_value . HP , , .max_v . mvspd_multiplier 1.5, 1.5 ( 50%). function Player:new(...) ... -- Boost self.max_boost = 100 self.boost = self.max_boost ... -- HP self.max_hp = 100 self.hp = self.max_hp -- Ammo self.max_ammo = 100 self.ammo = self.max_ammo ... end function Player:new(...) ... -- Movement self.r = -math.pi/2 self.rv = 1.66*math.pi self.v = 0 self.base_max_v = 100 self.max_v = self.base_max_v self.a = 100 ... end function Player:new(...) ... -- Cycle self.cycle_timer = 0 self.cycle_cooldown = 5 ... end hp_multiplierthat initially has a value of 1, and then apply all the increases from the tree to this variable and at some point multiply it by max_hp. Let's start with the first: function Player:new(...) ... -- Multipliers self.hp_multiplier = 1 end tree[2] = {'HP', {'6% Increased HP', 'hp_multiplier', 0.06}} HP, has a description 6% Increased HPand affects the variable hp_multiplierat 0.06 (6%). There is a function treeToPlayerthat receives all 900 of these node definitions and applies them to the player object. It is also important to note that the variable name used in the definition of the node must be the same name that is defined in the player object, otherwise it will not work. This is a very subtly connected and error-prone way, but as I said in the previous part, you can put up with such things, because we write everything alone.hp_multiplierbymax_hp? The natural choice is to simply do it in the constructor, because it is in him that a new player is created, and a new player is created when creating a new Stage room, which also happens when a new game starts. However, we will do this at the very end of the constructor, after all the resources, factors, and probabilities are determined: function Player:new(...) ... -- treeToPlayer(self) self:setStats() end setStatswe can do the following: function Player:setStats() self.max_hp = self.max_hp*self.hp_multiplier self.hp = self.max_hp end hp_multipliervalue 1.5 and start the game, we note that the player will have 150 HP instead of 100.treeToPlayerand transfer the player's object to this function. Later, when we write the skill tree code and implement this function, it will set the values ​​of all multipliers based on the bonuses from the tree, and after setting the values, we can call setStatsto use them to change player parameters.ammo_multiplier.boost_multiplier.flat_hpthat is added to max_hp(before multiplying by a factor): function Player:new(...) ... -- Flats self.flat_hp = 0 end function Player:setStats() self.max_hp = (self.max_hp + self.flat_hp)*self.hp_multiplier self.hp = self.max_hp end tree[15] = {'Flat HP', {'+10 Max HP', 'flat_hp', 10}} flat_ammo.flat_boost.ammo_gainthat is added to the amount of ammunition received when a player selects a resource. Change the calculations in the function accordingly addAmmo.attackvalue is assigned to the attribute 'Homing'. The homing code will be the same as the code used for the Ammo resource: function Projectile:update(dt) ... self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) -- Homing if self.attack == 'Homing' then -- Move towards target if self.target then local projectile_heading = Vector(self.collider:getLinearVelocity()):normalized() local angle = math.atan2(self.target.y - self.y, self.target.x - self.x) local to_target_heading = Vector(math.cos(angle), math.sin(angle)):normalized() local final_heading = (projectile_heading + 0.1*to_target_heading):normalized() self.collider:setLinearVelocity(self.v*final_heading.x, self.v*final_heading.y) end end end target. For an Ammo object, the variable targetpoints to the player's object, but in the case of a projectile, it will indicate the nearest enemy. To get the nearest enemy, we can use the function getAllGameObjectsThatdefined in the Area class and apply a filter that will select only objects that are enemies and are close enough. To do this, we must first determine which of the objects are enemies, and which objects are not. The easiest way to do this is to create a global table enemiesthat will contain a list of rows with the names of the enemy classes. That is, globals.luawe can add the following definition: enemies = {'Rock', 'Shooter'} local targets = self.area:getAllGameObjectsThat(function(e) for _, enemy in ipairs(enemies) do if e:is(_G[enemy]) then return true end end end) _G[enemy]to access the class definition of the current row that we loop through. That is, _G['Rock']returns a table containing the class definition Rock. We looked at this in several parts of the tutorial, so you should already understand why this works. local targets = self.area:getAllGameObjectsThat(function(e) for _, enemy in ipairs(enemies) do if e:is(_G[enemy]) and (distance(ex, ey, self.x, self.y) < 400) then return true end end end) distanceIs a function that we can define in utils.lua. It returns the distance between two positions: function distance(x1, y1, x2, y2) return math.sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)) end targets. Then the only thing we need is to choose one of them at random and indicate it as the targetprojectile is heading to: self.target = table.remove(targets, love.math.random(1, #targets)) function Projectile:update(dt) ... self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) -- Homing if self.attack == 'Homing' then -- Acquire new target if not self.target then local targets = self.area:getAllGameObjectsThat(function(e) for _, enemy in ipairs(enemies) do if e:is(_G[enemy]) and (distance(ex, ey, self.x, self.y) < 400) then return true end end end) self.target = table.remove(targets, love.math.random(1, #targets)) end if self.target and self.target.dead then self.target = nil end -- Move towards target if self.target then local projectile_heading = Vector(self.collider:getLinearVelocity()):normalized() local angle = math.atan2(self.target.y - self.y, self.target.x - self.x) local to_target_heading = Vector(math.cos(angle), math.sin(angle)):normalized() local final_heading = (projectile_heading + 0.1*to_target_heading):normalized() self.collider:setLinearVelocity(self.v*final_heading.x, self.v*final_heading.y) end end end self.targetvalue nil in the case when the target is killed. Due to this, when the target for the projectile ceases to exist, the self.targetvalue nil is assigned and a new target is obtained, because the condition will be satisfied not self.target, after which the whole process is repeated. It is also important to mention that after receiving the goal, we do not do any more calculations, so there is no need to worry about the speed of the function getAllGameObjectsThat, which naively bypasses in the loop all the living objects in the game.setLinearVelocityto set the velocity of the projectile, and then reuse it inside the cycle if self.attack == 'Homing', since the speed will only change if the projectile is actually homing and if there is a target. But for some reason, this leads to all sorts of problems, so we have to call setLinearVelocityonly once, that is, write something like this: -- Homing if self.attack == 'Homing' then ... -- Normal movement else self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) end attackwhich is assigned a value 'Homing', then it should look like this:
Homing. Its definition in the attack table looks like this: attacks['Homing'] = {cooldown = 0.56, ammo = 4, abbreviation = 'H', color = skill_point_color} 
skill_point_color), which also has a track that has the color of the player.launch_homing_projectile_on_ammo_pickup_chance, and when selecting a resource Ammo, we will call a function that “throws out on the dice” the probability of the event being executed.chanceList. If an event has a probability of 5%, then we need to make sure that these 5% are observed fairly enough, so it is logical to use chanceList.setStatsin the Player constructor, we will also call a function generateChancesthat will create all the chanceList lists that apply throughout the game. Since the game will have many different events for which you need to “roll a cube”, we will put all the chanceList lists in the table chancesand arrange everything so that when we need to “roll the cube” on the likelihood of doing something, we could do something like: if self.chances.launch_homing_projectile_on_ammo_pickup_chance:next() then -- launch homing projectile end chancesmanually, that is, each time we add a new type variable _chancein which the probability of a certain event will be stored, we will also add and generate generateChancesits chanceList list in the function . But here we can do a little smarter and decide that every variable that deals with probabilities will end in _chance, and use this to our advantage: function Player:generateChances() self.chances = {} for k, v in pairs(self) do if k:find('_chance') and type(v) == 'number' then end end end _chance, as well as being a number. If both of these conditions are true, then, based on our own decision, this is a variable related to the likelihood of an event. That is, we just need to create then the chanceList and add it to the table chances: function Player:generateChances() self.chances = {} for k, v in pairs(self) do if k:find('_chance') and type(v) == 'number' then self.chances[k] = chanceList({true, math.ceil(v)}, {false, 100-math.ceil(v)}) end end end vof which will be true, and 100-v- false. That is, if the only “probability” type variable defined in the player's object was launch_homing_projectile_on_ammo_pickup_chance, and it has a value of 5 (meaning a 5% probability of making an event), then chanceList will have 5 true values ​​and 95 false values, which gives us the desired result.generateChancesfor the player constructor: function Player:new(...) ... -- treeToPlayer(self) self:setStats() self:generateChances() end launch_homing_projectile_on_ammo_pickup_chance: function Player:new(...) ... -- Chances self.launch_homing_projectile_on_ammo_pickup_chance = 0 end :next()to see what happens.onAmmoPickupthat is called when selecting the Ammo resource: function Player:update(dt) ... if self.collider:enter('Collectable') then ... if object:is(Ammo) then object:die() self:addAmmo(5) self:onAmmoPickup() ... end end function Player:onAmmoPickup() if self.chances.launch_homing_projectile_on_ammo_pickup_chance:next() then local d = 1.2*self.w self.area:addGameObject('Projectile', self.x + d*math.cos(self.r), self.y + d*math.sin(self.r), {r = self.r, attack = 'Homing'}) self.area:addGameObject('InfoText', self.x, self.y, {text = 'Homing Projectile!'}) end end 
regain_hp_on_ammo_pickup_chance. The amount of HP recovered will be 25. It should be added using a function addHPthat adds the specified amount of HP to the value hp, checking that it does not exceed max_hp. In addition, an object must be created InfoTextwith text 'HP Regain!'and color hp_color.regain_hp_on_sp_pickup_chance. The amount of HP recovered will be 25. It should be added using a function addHPthat adds the specified amount of HP to the value hp, checking that it does not exceed max_hp. An object must also be created InfoTextwith text 'HP Regain!'and color hp_color. In addition, you need to add a function to the Player class.onSPPickup, and all the work must be done in it (in the same way as it was with the function onAmmoPickup).aspd_multiplierand then multiply this variable by the time of the “respite” between shooting: function Player:new(...) ... -- Multipliers self.aspd_multiplier = 1 end function Player:update(dt) ... -- Shoot self.shoot_timer = self.shoot_timer + dt if self.shoot_timer > self.shoot_cooldown*self.aspd_multiplier then self.shoot_timer = 0 self:shoot() end end shoot_cooldown, smaller values ​​mean that the pause will be shorter, that is, the player will shoot faster. We use this knowledge when creating an object HasteArea.aspd_multiplierby a certain amount while the player is in it. To achieve this, we will create a new object HasteAreathat will control the logic to check whether the player is inside and set the appropriate values. The basic structure of this object is as follows: function HasteArea:new(...) ... self.r = random(64, 96) self.timer:after(4, function() self.timer:tween(0.25, self, {r = 0}, 'in-out-cubic', function() self.dead = true end) end) end function HasteArea:update(dt) ... end function HasteArea:draw() love.graphics.setColor(ammo_color) love.graphics.circle('line', self.x, self.y, self.r + random(-2, 2)) love.graphics.setColor(default_color) end aspd_multiplierwhen it happens. You can do it like this: function HasteArea:update(dt) ... local player = current_room.player if not player then return end local d = distance(self.x, self.y, player.x, player.y) if d < self.r and not player.inside_haste_area then -- Enter event player:enterHasteArea() elseif d >= self.r and player.inside_haste_area then -- Leave event player:exitHasteArea() end end inside_haste_areato track whether a player is in the area. This variable takes the value true inside enterHasteAreaand false inside exitHasteArea, that is, these functions will be called only when these events occur in the object HasteArea. In the Player class, both functions will simply apply the necessary modifications: function Player:enterHasteArea() self.inside_haste_area = true self.pre_haste_aspd_multiplier = self.aspd_multiplier self.aspd_multiplier = self.aspd_multiplier/2 end function Player:exitHasteArea() self.inside_haste_area = false self.aspd_multiplier = self.pre_haste_aspd_multiplier self.pre_haste_aspd_multiplier = nil end HasteArea, and not to link it with the player through a variable inside_haste_area. We cannot do this because if we do, then problems will arise when the player simultaneously enters several areas at the same time or leaves them. In the current form, the existence of a variable inside_haste_areameans that we will apply the bonus only once, even if the player is on top of three intersecting HasteArea objects.spawn_haste_area_on_hp_pickup_chance. The object InfoTextmust be created with text.'Haste Area!' .In addition, you need to add a function to the Player class onHPPickup.spawn_haste_area_on_sp_pickup_chance. The object InfoTextmust be created with text.'Haste Area!' .spawn_sp_on_cycle_chance. We fully know how to implement it. The “in cycle” part behaves very much like “when selecting a resource”; the only difference is that we will call the function onCyclewhen executing a new cycle, and not when selecting a resource. And the “creation of SP” part is just the creation of a new SP resource, the implementation of which is known to us.cycleand call onCycle: function Player:cycle() ... self:onCycle() end spawn_sp_on_cycle_chance: function Player:new(...) ... -- Chances self.spawn_sp_on_cycle_chance = 0 end onCycle: function Player:onCycle() if self.chances.spawn_sp_on_cycle_chance:next() then self.area:addGameObject('SkillPoint') self.area:addGameObject('InfoText', self.x, self.y, {text = 'SP Spawn!', color = skill_point_color}) end end 
barrage_on_kill_chance. The only thing that we do not know yet is the part with the “queue”. Triggering an event during a kill is similar to the previous one, except that instead of calling during the execution of a cycle, we will call the player’s function onKillwhen the enemy dies.barrage_on_kill_chance: function Player:new(...) ... -- Chances self.barrage_on_kill_chance = 0 end onKilland call it when the enemy dies. There are two approaches to challenge onKillwhen a player dies. The first is to simply call a function from a function dieor hiteach enemy. The problem here is that when adding new enemies we will have to add the same calling code to all of them onKill. The second option is to challenge onKillif there is a collision with an enemy of the object Projectile. The problem here is that some projectiles can collide with enemies, but not kill them (because enemies have more HP or a projectile does less damage), so we need to find a way to check if the enemy is actually dead. It turns out that performing this check is quite simple, so I will choose this method: function Projectile:update(dt) ... if self.collider:enter('Enemy') then ... if object then object:hit(self.damage) self:die() if object.hp <= 0 then current_room.player:onKill() end end end end hitenemy is simply to check if the enemy's HP is zero. If it is equal, then it means that he is dead, and we can call onKill. function Player:onKill() if self.chances.barrage_on_kill_chance:next() then for i = 1, 8 do self.timer:after((i-1)*0.05, function() local random_angle = random(-math.pi/8, math.pi/8) local d = 2.2*self.w self.area:addGameObject('Projectile', self.x + d*math.cos(self.r + random_angle), self.y + d*math.sin(self.r + random_angle), {r = self.r + random_angle, attack = self.attack}) end) end self.area:addGameObject('InfoText', self.x, self.y, {text = 'Barrage!!!'}) end end afterto separate the creation of shells with a pause of 0.05 seconds. In all other respects, we simply create a projectile with given limitations. All this should look like this:
InfoTextwith the corresponding colors so that the player can understand what is happening.spawn_hp_on_cycle_chance.regain_hp_on_cycle_chance. The number of recovered HP should be equal to 25.regain_full_ammo_on_cycle_chance.change_attack_on_cycle_chance. New attack is chosen randomly.spawn_haste_area_on_cycle_chance.barrage_on_cycle_chance.launch_homing_projectile_on_cycle_chance.regain_ammo_on_kill_chance. The number of recovered ammunition should be equal to 20.launch_homing_projectile_on_kill_chance.regain_boost_on_kill_chance. The number of recovered acceleration should be equal to 40.spawn_boost_on_kill_chance.HasteArea. Now we want to implement another one in which we will have a chance to get an increase in the speed of attacks after killing an enemy. However, if we try to implement it in the same way as the previous acceleration of ASPD, we will soon encounter problems. To refresh your memories, I’ll give an example of how acceleration is implemented in HasteArea: function HasteArea:update(dt) HasteArea.super.update(self, dt) local player = current_room.player if not player then return end local d = distance(self.x, self.y, player.x, player.y) if d < self.r and not player.inside_haste_area then player:enterHasteArea() elseif d >= self.r and player.inside_haste_area then player:exitHasteArea() end end enterHasteAreathey exitHasteArealook like this: function Player:enterHasteArea() self.inside_haste_area = true self.pre_haste_aspd_multiplier = self.aspd_multiplier self.aspd_multiplier = self.aspd_multiplier/2 end function Player:exitHasteArea() self.inside_haste_area = false self.aspd_multiplier = self.pre_haste_aspd_multiplier self.pre_haste_aspd_multiplier = nil end aspd_boost_on_kill_chancesame way, it would look something like this: function Player:onKill() ... if self.chances.aspd_boost_on_kill_chance:next() then self.pre_boost_aspd_multiplier = self.aspd_multiplier self.aspd_multiplier = self.aspd_multiplier/2 self.timer:after(4, function() self.aspd_multiplier = self.pre_boost_aspd_multiplier self.pre_boost_aspd_multiplier = nil end) end end aspd_multiplierwill be restored to the value before the ASPD accelerates, that is, when leaving the area, all other acceleration bonuses will speed up the attacks.pre_boost_aspd_multiplierrestore to a aspd_multipliervalue that does not take into account the increase in the HasteArea attack speed. But more importantly, when the player leaves HasteArea, he will have a constantly increased attack speed, because the stored attack speed at the entrance will be that increased by the acceleration of ASPD. function Player:new(...) ... self.base_aspd_multiplier = 1 self.aspd_multiplier = 1 self.additional_aspd_multiplier = {} end aspd_multiplier, we will have base_aspd_multiplierand additional_aspd_multiplier. The variable aspd_multiplierwill store the current factor taking into account all accelerations. base_aspd_multiplierwill contain the original multiplier, taking into account only the percentage increase. That is, if we get from the tree an increase in the attack speed by 50%, it will be applied in the constructor (c setStats) to base_aspd_multiplier. Then it additional_aspd_multiplierwill contain all the added values ​​of all accelerations. That is, when the player is in HasteArea, we will add the corresponding value to this table, and then in each frame multiply its amount by the base. Therefore, for example, the update function will be as follows: function Player:update(dt) ... self.additional_aspd_multiplier = {} if self.inside_haste_area then table.insert(self.additional_aspd_multiplier, -0.5) end if self.aspd_boosting then table.insert(self.additional_aspd_multiplier, -0.5) end local aspd_sum = 0 for _, aspd in ipairs(self.additional_aspd_multiplier) do aspd_sum = aspd_sum + aspd end self.aspd_multiplier = self.base_aspd_multiplier/(1 - aspd_sum) end aspd_multiplierin accordance with the value of the base, as well as all the accelerations. We will have a few more factors that function in a very similar way, so I will create a common object for this, since repeating the code with different variable names every time will be tedious.Statlooks like this: Stat = Object:extend() function Stat:new(base) self.base = base self.additive = 0 self.additives = {} self.value = self.base*(1 + self.additive) end function Stat:update(dt) for _, additive in ipairs(self.additives) do self.additive = self.additive + additive end if self.additive >= 0 then self.value = self.base*(1 + self.additive) else self.value = self.base/(1 - self.additive) end self.additive = 0 self.additives = {} end function Stat:increase(percentage) table.insert(self.additives, percentage*0.01) end function Stat:decrease(percentage) table.insert(self.additives, -percentage*0.01) end function Player:new(...) ... self.aspd_multiplier = Stat(1) end function Player:update(dt) ... if self.inside_haste_area then self.aspd_multiplier:decrease(100) end if self.aspd_boosting then self.aspd_multiplier:decrease(100) end self.aspd_multiplier:update(dt) ... end aspd_multiplier:update, referring to aspd_multiplier.value, which will return the correct result in accordance with the base and all the possible accelerations. Therefore, we must change the way the variable is used aspd_multiplier: function Player:update(dt) ... -- Shoot self.shoot_timer = self.shoot_timer + dt if self.shoot_timer > self.shoot_cooldown*self.aspd_multiplier.value then self.shoot_timer = 0 self:shoot() end end self.shoot_cooldown*self.aspd_multiplieron self.shoot_cooldown*self.aspd_multiplier.value, because otherwise it will not work anything. In addition, we have to change something else here. The way the variable works is aspd_multiplierso far contrary to how all the other variables of the game work. When we say that we have increased HP by 10%, we know that it hp_multiplieris equal to 1.1, but when we say that we have increased by 10% ASPD, it aspd_multiplieris actually equal to 0.9. We can change this and make it aspd_multiplierbehave in the same way as other variables, with the division instead of multiplying shoot_cooldown: if self.shoot_timer > self.shoot_cooldown/self.aspd_multiplier.value then decreasewe will call increase: function Player:update(dt) ... if self.inside_haste_area then self.aspd_multiplier:increase(100) end if self.aspd_boosting then self.aspd_multiplier:increase(100) end self.aspd_multiplier:update(dt) end aspd_multiplieris an object Stat, and not just a number, then when you implement the tree and import its values ​​into the Player object, we will have to process them in a different way. Therefore, in the above function, treeToPlayerwe will have to take this into account. function Player:new(...) ... -- Chances self.gain_aspd_boost_on_kill_chance = 0 end function Player:onKill() ... if self.chances.gain_aspd_boost_on_kill_chance:next() then self.aspd_boosting = true self.timer:after(4, function() self.aspd_boosting = false end) self.area:addGameObject('InfoText', self.x, self.y, {text = 'ASPD Boost!', color = ammo_color}) end end enterHasteAreaand exitHasteArea, and also slightly change the operation of the HasteArea object: function HasteArea:update(dt) HasteArea.super.update(self, dt) local player = current_room.player if not player then return end local d = distance(self.x, self.y, player.x, player.y) if d < self.r then player.inside_haste_area = true elseif d >= self.r then player.inside_haste_area = false end end inside_haste_areaPlayer object attribute to true or false depending on whether it is in the region, and then, due to the way we implemented the object Stat, the application of the acceleration rate of attacks received from HasteArea will be performed automatically.mvspd_boost_on_cycle_chance. “Increasing MVSPD” gives the player a 50 percent increase in movement speed for 4 seconds. Also implement the variable mvspd_multiplierand multiply it in the appropriate place.pspd_boost_on_cycle_chance. “Increasing PSPD” gives the shells created by the player a 100 percent increase in speed by 4 seconds. Also, implement the variablepspd_multiplierand multiply it in the appropriate place.pspd_inhibit_on_cycle_chance. “Reducing PSPD” gives the shells created by the player a 50 percent reduction in speed by 4 seconds.launch_homing_projectile_while_boosting_chance. It will work like this: there is a normal probability of a shot by a homing projectile, and this probability will be checked in the interval of 0.2 seconds when performing acceleration (Boost). This means that if we accelerate within 1 second, “probability cubes” will be thrown 5 times.onBoostStartandonBoostEnd. They will activate the passive skill at the start of acceleration and deactivate it at the end of acceleration. To add these two functions, we need to change the acceleration code slightly: function Player:update(dt) ... -- Boost ... if self.boost_timer > self.boost_cooldown then self.can_boost = true end ... if input:pressed('up') and self.boost > 1 and self.can_boost then self:onBoostStart() end if input:released('up') then self:onBoostEnd() end if input:down('up') and self.boost > 1 and self.can_boost then ... if self.boost <= 1 then self.boosting = false self.can_boost = false self.boost_timer = 0 self:onBoostEnd() end end if input:pressed('down') and self.boost > 1 and self.can_boost then self:onBoostStart() end if input:released('down') then self:onBoostEnd() end if input:down('down') and self.boost > 1 and self.can_boost then ... if self.boost <= 1 then self.boosting = false self.can_boost = false self.boost_timer = 0 self:onBoostEnd() end end ... end input:pressedand input:releasedthat return true only when these events occur, and thanks to this we can be sure that onBoostStartthey onBoostEndwill only be called when these events occur. We also add inside the conditional design input:down onBoostEndin case the player does not release the button, but the amount of acceleration available to him ends, and therefore the acceleration ends.launch_homing_projectile_while_boosting_chance: function Player:new(...) ... -- Chances self.launch_homing_projectile_while_boosting_chance = 0 end function Player:onBoostStart() self.timer:every('launch_homing_projectile_while_boosting_chance', 0.2, function() if self.chances.launch_homing_projectile_while_boosting_chance:next() then local d = 1.2*self.w self.area:addGameObject('Projectile', self.x + d*math.cos(self.r), self.y + d*math.sin(self.r), {r = self.r, attack = 'Homing'}) self.area:addGameObject('InfoText', self.x, self.y, {text = 'Homing Projectile!'}) end end) end function Player:onBoostEnd() self.timer:cancel('launch_homing_projectile_while_boosting_chance') end timer:everyto test the probability of firing a homing projectile every 0.2 seconds, and then, when the acceleration ends, we cancel this timer. Here is what it looks like when the probability of an event is 100%:
cycle_speed_multiplier. This variable, depending on its value, increases or decreases the speed of the cycle. That is, for example, if it cycle_speed_multiplieris equal to 2, and the cycle duration defaults to 5 seconds, then the use of a variable will lead to a decrease in the cycle duration to 2.5 seconds.increased_cycle_speed_while_boosting. This variable should be of type boolean and signal whether the speed of the cycle should increase when the player performs the acceleration. Acceleration should be an increase in cycle speed multiplier by 200%.invulnerability_while_boosting. This variable is of type boolean and indicates whether the player should be invulnerable during acceleration. Use the existing attribute invinciblethat is responsible for the invulnerability of the player.luck_multiplier. Luck is one of the basic parameters of the game; it increases the likelihood of performing the desired events. Suppose we have a 10 percent chance of hitting a projectile with a kill. If it luck_multiplieris equal to 2, then this probability becomes 20 percent.generateChances, so we can simply implement it here: function Player:generateChances() self.chances = {} for k, v in pairs(self) do if k:find('_chance') and type(v) == 'number' then self.chances[k] = chanceList( {true, math.ceil(v*self.luck_multiplier)}, {false, 100-math.ceil(v*self.luck_multiplier)}) end end end vby luck_multiplierand it should work exactly as it should. Due to this, we can realize the passive skill increased_luck_while_boostingas follows: function Player:onBoostStart() ... if self.increased_luck_while_boosting then self.luck_boosting = true self.luck_multiplier = self.luck_multiplier*2 self:generateChances() end end function Player:onBoostEnd() ... if self.increased_luck_while_boosting and self.luck_boosting then self.luck_boosting = false self.luck_multiplier = self.luck_multiplier/2 self:generateChances() end end HasteArea. We can do it now because we will not have any other passive skills that give the player an increase in luck, that is, we don’t need to worry about a few bonuses that can override each other. If we had a few passive skills that give an increase in luck, we would have to make them an object Stat, as was the case with aspd_multiplier.generateChancesOtherwise, our increase in luck will not affect anything. Such a solution has a flaw - all lists are reset, so if a list accidentally selected a series of unsuccessful “throws” and then was reset, then again it can re-select a series of unsuccessful “throws” instead of using the chanceList property, in which over time the selection of less successful shots becomes less likely. But this is a very frivolous problem, which personally does not bother me very much.hp_spawn_chance_multiplierwhich increases the likelihood that when the director creates a new resource, this resource will be HP. If you remember how the director works, then this implementation will be pretty simple: function Player:new(...) ... -- Multipliers self.hp_spawn_chance_multiplier = 1 end function Director:new(...) ... self.resource_spawn_chances = chanceList({'Boost', 28}, {'HP', 14*current_room.player.hp_spawn_chance_multiplier}, {'SkillPoint', 58}) end resource_spawn_chances, so all we need is to use hp_spawn_chance_multiplierto increase the likelihood that the HP resource will be created in accordance with the multiplier.spawn_sp_chance_multiplier.spawn_boost_chance_multiplier.drop_double_ammo_chance. When an enemy dies, there must be a chance that he will create two Ammo objects instead of one.attack_twice_chance. When a player attacks, there must be a probability of calling the function shoottwice.spawn_double_hp_chance. When a director creates an HP resource, there must be a chance that instead of one, he will create two HP objects.spawn_double_sp_chanceWhen a director creates a SkillPoint resource, there must be a chance that he will create two instead of one SkillPoint object.gain_double_sp_chance. When a player selects a SkillPoint resource, there must be a chance that he will get two skill points instead of one.enemy_spawn_rate_multiplierwill control how quickly Director changes the difficulty levels. By default, this happens every 22 seconds, but if it enemy_spawn_rate_multiplieris 2, then it will happen every 11 seconds. The implementation of this is also quite simple: function Player:new(...) ... -- Multipliers self.enemy_spawn_rate_multiplier = 1 end function Director:update(dt) ... -- Difficulty self.round_timer = self.round_timer + dt if self.round_timer > self.round_duration/self.stage.player.enemy_spawn_rate_multiplier then ... end end round_durationby enemy_spawn_rate_multiplierto get the desired duration of the round.resource_spawn_rate_multiplier.attack_spawn_rate_multiplier.turn_rate_multiplier. This passive skill increases or decreases the turning speed of a player’s ship.boost_effectiveness_multiplier. This passive skill increases or decreases acceleration effectiveness. This means that if a variable has a value of 2, then acceleration will work twice as fast or slower.projectile_size_multiplier. This is a passive skill that increases or decreases the size of projectiles.boost_recharge_rate_multiplier. This is a passive skill that increases or decreases the acceleration recharge rate.invulnerability_time_multiplier. This is a passive skill that increases or decreases the player’s invulnerability time when damage is dealt.ammo_consumption_multiplier. This passive skill increases or decreases the amount of ammunition consumed during all attacks.size_multiplier. This passive skill increases or decreases the size of the player’s ship. It should be noted that the positions of all traces of all ships, as well as the positions of projectiles must be changed accordingly.stat_boost_duration_multiplier. This passive skill increases or decreases the duration of the time bonuses given to the player.EnemyProjectile, after which we will be able to create enemies using some of these skills. For example, there is a passive skill that causes projectiles to rotate around a ship, rather than fly straight. Later we will add an enemy around whom piles of shells will fly, and in both cases the same technology will be used.projectile_ninety_degree_change. He will periodically change the angle of the projectile to 90 degrees. It will look like this:
projectile_ninety_degree_changeboolean variable that will affect when true. Since we are going to apply this effect in the class Projectile, we have two options on how to read the value from it projectile_ninety_degree_changefrom the Player: either transfer it to the table optswhen creating a new projectile as a function shoot, or read it directly from the Player, accessing throughcurrent_room.player. I will take the second path, because it is simpler and there are no serious flaws in it, except that we will have to replace current_room.playerit with something else when we move part of this code to EnemyProjectile. All this will look something like this: function Player:new(...) ... -- Booleans self.projectile_ninety_degree_change = false end function Projectile:new(...) ... if current_room.player.projectile_ninety_degree_change then end end function Projectile:new(...) ... if current_room.player.projectile_ninety_degree_change then self.timer:after(0.2, function() self.ninety_degree_direction = table.random({-1, 1}) self.r = self.r + self.ninety_degree_direction*math.pi/2 end) end end 
timer:every: function Projectile:new(...) ... if current_room.player.projectile_ninety_degree_change then self.timer:after(0.2, function() self.ninety_degree_direction = table.random({-1, 1}) self.r = self.r + self.ninety_degree_direction*math.pi/2 self.timer:every('ninety_degree_first', 0.25, function() self.r = self.r - self.ninety_degree_direction*math.pi/2 self.timer:after('ninety_degree_second', 0.1, function() self.r = self.r - self.ninety_degree_direction*math.pi/2 self.ninety_degree_direction = -1*self.ninety_degree_direction end) end) end) end end everywe change the direction in which it should turn, otherwise it will not fluctuate between the up / down directions and will move up / down rather than in a straight line. By implementing this, we get the following:
projectile_random_degree_changethat changes the angle of the projectile at random. Unlike turns at 90 degrees, shells in this case should not restore their original direction.angle_change_frequency_multiplier. This skill increases or decreases the rate of change of the angles of the previous two passive skills. If angle_change_frequency_multiplier, for example, is 2, then instead of changing the angles after 0.25 and 0.1 seconds, they will change after 0.125 and 0.05 seconds.timer:tween, thus obtaining the effect of a wave projectile:
timer:tween: function Projectile:new(...) ... if current_room.player.wavy_projectiles then local direction = table.random({-1, 1}) self.timer:tween(0.25, self, {r = self.r + direction*math.pi/8}, 'linear', function() self.timer:tween(0.25, self, {r = self.r - direction*math.pi/4}, 'linear') end) self.timer:every(0.75, function() self.timer:tween(0.25, self, {r = self.r + direction*math.pi/4}, 'linear', function() self.timer:tween(0.5, self, {r = self.r - direction*math.pi/4}, 'linear') end) end) end end timer:every, in the code it does not begin to perform its functions until the end of the initial time, so we first perform one iteration of the loop manually, and then execute each loop. In the first iteration, we also use the initial value of math.pi / 8 instead of math.pi / 4, because we want the projectile to oscillate two times smaller than necessary, because it is initially in the middle position (as it was just shot Player), and not on one of the oscillation boundaries.projectile_waviness_multiplier. This skill increases or decreases the target angle that the projectile should reach when performing tween. For example, if If projectile_waviness_multiplieris 2, then the arc of its trajectory will be twice the normal.
function Projectile:new(...) ... if current_room.player.fast_slow then local initial_v = self.v self.timer:tween('fast_slow_first', 0.2, self, {v = 2*initial_v}, 'in-out-cubic', function() self.timer:tween('fast_slow_second', 0.3, self, {v = initial_v/2}, 'linear') end) end if current_room.player.slow_fast then local initial_v = self.v self.timer:tween('slow_fast_first', 0.2, self, {v = initial_v/2}, 'in-out-cubic', function() self.timer:tween('slow_fast_second', 0.3, self, {v = 2*initial_v}, 'linear') end) end end projectile_acceleration_multiplier. This skill controls the amount of acceleration as the speed increases from the original value.projectile_deceleration_multiplier. This skill controls the amount of inhibition when the speed decreases from the initial value.
Ax = Bx + R*math.cos(time) Ay = By + R*math.sin(time) timeis a variable whose value increases with time. Before proceeding with the implementation, let's prepare everything else. shield_projectile_chancethe variable will not be a boolean, but a variable of the “probability” type, that is, each time a new projectile is created, there will appear a probability that it will begin to rotate around the player. function Player:new(...) ... -- Chances self.shield_projectile_chance = 0 end function Player:shoot() ... local shield = self.chances.shield_projectile_chance:next() if self.attack == 'Neutral' then self.area:addGameObject('Projectile', self.x + 1.5*d*math.cos(self.r), self.y + 1.5*d*math.sin(self.r), {r = self.r, attack = self.attack, shield = shield}) ... end shieldfor which the die rolls on whether this projectile should rotate around the player, after which we transfer it to the optschallenge table addGameObject. Here we need to repeat this step for each of the available attacks. Since we will have similar changes in the future, we can do something like this instead: function Player:shoot() ... local mods = { shield = self.chances.shield_projectile_chance:next() } if self.attack == 'Neutral' then self.area:addGameObject('Projectile', self.x + 1.5*d*math.cos(self.r), self.y + 1.5*d*math.sin(self.r), table.merge({r = self.r, attack = self.attack}, mods)) ... end mods. The function table.mergeis not yet defined, but based on how we use it here, you can guess what it does. function table.merge(t1, t2) local new_table = {} for k, v in pairs(t2) do new_table[k] = v end for k, v in pairs(t1) do new_table[k] = v end return new_table end shield. First, we want to define variables such as radius, rotational speed, and so on. For now, I define them as follows: function Projectile:new(...) ... if self.shield then self.orbit_distance = random(32, 64) self.orbit_speed = random(-6, 6) self.orbit_offset = random(0, 2*math.pi) end end orbit_distancedenotes the radius around the player. orbit_speedwill be multiplied by time, that is, at larger absolute values, the projectile will move faster, and at smaller values, it will move more slowly. Negative values ​​cause the projectile to move in a different direction, which adds a bit of randomness. orbit_offset- this is the initial angular displacement that each projectile has. It also adds a bit of randomness and does not allow all shells to be created in approximately one position. And now, when we have determined all this, we can apply the parametric equation of a circle to the position of the projectile: function Projectile:update(dt) ... -- Shield if self.shield then local player = current_room.player self.collider:setPosition( player.x + self.orbit_distance*math.cos(self.orbit_speed*time + self.orbit_offset), player.y + self.orbit_distance*math.sin(self.orbit_speed*time + self.orbit_offset)) end ... end setLinearVelocity, otherwise nothing will work. We also need to remember to add a global variable timeand increment it in each frame by dt. If we do everything right, it will look like this:
function Projectile:new(...) ... self.previous_x, self.previous_y = self.collider:getPosition() end function Projectile:update(dt) ... -- Shield if self.shield then ... local x, y = self.collider:getPosition() local dx, dy = x - self.previous_x, y - self.previous_y self.r = Vector(dx, dy):angle() end ... -- At the very end of the update function self.previous_x, self.previous_y = self.collider:getPosition() end rin which the angle of the projectile will be stored, taken into account during rotation. Since we use setLinearVelocitythis angle, when the projectile is drawn in Projectile:drawand used Vector(self.collider:getLinearVelocity()):angle())to get direction, everything will be set according to how the variable is set r. And all this will look like this:
function Projectile:new(...) ... if self.shield then ... self.invisible = true self.timer:after(0.05, function() self.invisible = false end) end end function Projectile:draw() if self.invisible then return end ... end function Projectile:new(...) ... if self.shield then ... self.timer:after(6, function() self:die() end) end end Source: https://habr.com/ru/post/350316/
All Articles