1-- This file is a part of Avoision.
2--
3-- Copyright (C) 2011 Jared Krinke.
4--
5-- Avoision is free software; you can redistribute it and/or
6-- modify it under the terms of the GNU General Public License
7-- as published by the Free Software Foundation; either version 2
8-- of the License, or (at your option) any later version.
9--
10-- This program is distributed in the hope that it will be useful,
11-- but WITHOUT ANY WARRANTY; without even the implied warranty of
12-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13-- GNU General Public License for more details.
14--
15-- You should have received a copy of the GNU General Public License
16-- along with this program; if not, write to the Free Software
17-- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18
19require("Avoision/Common.lua")
20require("Avoision/Enemy.lua")
21require("Avoision/Seeker.lua")
22require("Effects/Ghost.lua")
23require("Avoision/Goal.lua")
24require("Avoision/Player.lua")
25
26local transitionPeriod = 1000
27local boardWidth = 400
28local boardHeight = 400
29local boardTimeout = 19000
30local boardPlayerMinimumDistance = 0.15
31local enemyWidth = 1 / 30
32local enemyWidthMin = 1 / 90
33local enemyWidthMax = 1 / 15
34local enemySpeed = 0.2 / 1000
35local enemySpeedMin = 0.1 / 1000
36local enemySpeedMax = 0.6 / 1000
37local enemyGhostPeriod = 500
38local enemyGhostScaleMax = 0
39local goalWidth = 1 / 30
40local goalHeight = 1 / 30
41local goalSpeed = 0.1 / 1000
42local goalDirectionPeriod = 3000
43
44local pointProgression = { 30, 20, 15, 10, 8, 7, 6, 5, 4, 3, 2, 1, 0 }
45
46Board = { }
47
48function Board.new(layer, lost, ended)
49    local board = Entity.new(0, 0, 0, boardWidth, boardHeight)
50
51    board.paused = false
52    board.finishTimer = 0
53    board.difficulty = Common.difficultyLevel.Easy
54    board.score = 0
55    board.elements:add(Element.Image.new("Images/Background.png", 0, 0, 0, 1, 1, 0))
56    board.player = Player.new()
57    board.goal = Goal.new(0, 0, goalWidth, goalHeight, 0, 0)
58    board.cd = layer:createCollisionDetector()
59
60    -- TODO: Test this more thoroughly
61    function board.getSafePosition(self, width, height)
62        local ax1 = max(-0.5 + width / 2, self.player.x - (self.player.width / 2 + width / 2 + boardPlayerMinimumDistance))
63        local ax2 = min(0.5 - width / 2, self.player.x + (self.player.width / 2 + width / 2 + boardPlayerMinimumDistance))
64        local ay1 = max(-0.5 + height / 2, self.player.y - (self.player.height / 2 + height / 2 + boardPlayerMinimumDistance))
65        local ay2 = min(0.5 - height / 2, self.player.y + (self.player.height / 2 + height / 2 + boardPlayerMinimumDistance))
66
67        repeat
68            local x = (random() - 0.5) * (1 - width)
69            local y = (random() - 0.5) * (1 - height)
70
71            local bx1 = x - width / 2
72            local bx2 = x + width / 2
73            local by1 = y - height / 2
74            local by2 = y + height / 2
75
76            local valid = true
77
78            if ((ax1 >= bx1 and ax1 <= bx2) or (ax2 >= bx1 and ax2 <= bx2) or (bx1 >= ax1 and bx1 <= ax2))
79                and ((ay1 >= by1 and ay1 <= by2) or (ay2 >= by1 and ay2 <= by2) or (by1 >= ay1 and by1 <= ay2))
80            then
81                valid = false
82            else
83                return x, y
84            end
85        until valid
86    end
87
88    function board.resetGoal(self)
89        self:setPoints(pointProgression[1] * self.difficulty)
90        self.timer = 0
91
92        self.goal.x, self.goal.y = self:getSafePosition(self.goal.width, self.goal.height)
93
94        -- The goal moves on hard
95        if self.difficulty >= Common.difficultyLevel.Hard then
96            self.goal.axisSpeed = goalSpeed
97        else
98            self.goal.axisSpeed = 0
99        end
100
101        -- Update goal speeds as needed
102        self:resetGoalDirection()
103    end
104
105    function board.addEnemy(self)
106        local size = enemyWidth
107        local speed = enemySpeed
108
109        -- Enemy speeds vary on normal
110        if self.difficulty >= Common.difficultyLevel.Normal then
111            speed = enemySpeedMin + (enemySpeedMax - enemySpeedMin) * random()
112        end
113
114        -- Enemies are faster on extreme
115        if self.difficulty >= Common.difficultyLevel.Extreme then
116            speed = speed * 1.1
117        end
118
119        -- Enemy sizes vary on hard (and speed varies more)
120        if self.difficulty >= Common.difficultyLevel.Hard then
121            size = enemyWidthMin + (enemyWidthMax - enemyWidthMin) * random()
122        end
123
124        -- Seekers may appear on extreme
125        local x = random()
126        local seekerProbability = 0
127
128        if self.difficulty >= Common.difficultyLevel.Extreme then
129            seekerProbability = 0.15
130        end
131
132        local e = nil
133
134        if x >= seekerProbability then
135            local speedX = 0
136            local speedY = 0
137
138            -- Direction
139            if random() > 0.5 then
140                speed = -speed
141            end
142
143            -- Axis
144            if random() > 0.5 then
145                speedX = speed
146            else
147                speedY = speed
148            end
149
150            -- On extreme, blocks may move diagonally
151            if self.difficulty >= Common.difficultyLevel.Extreme and random() > 0.67 then
152                local otherSpeed = enemySpeedMin + (enemySpeedMax - enemySpeedMin) * random()
153
154                if speedX == 0 then
155                    speedX = otherSpeed
156                elseif speedY == 0 then
157                    speedY = otherSpeed
158                end
159            end
160
161            e = Enemy.new(0, 0, size, size, speedX, speedY)
162        else
163            e = Seeker.new(0, 0, size, size, speed, self.player)
164        end
165
166        e.x, e.y = self:getSafePosition(e.width, e.height)
167
168        Ghost.new(e, enemyGhostPeriod, enemyGhostScaleMax, false, function()
169            self:addChild(e)
170            self.cd:addChild(e)
171        end):setParent(self)
172    end
173
174    function board.resetGoalDirection(self)
175        local speed = self.goal.axisSpeed
176
177        -- Direction
178        if random() > 0.5 then
179            speed = -speed
180        end
181
182        -- Axis
183        if random() > 0.5 then
184            self.goal.speed.x = speed
185            self.goal.speed.y = 0
186        else
187            self.goal.speed.x = 0
188            self.goal.speed.y = speed
189        end
190
191        self.goalDirectionTimer = self.timer + goalDirectionPeriod * random()
192    end
193
194    function board.update(self, ms)
195        -- Update children first
196        self:updateChildren(ms)
197
198        -- Update timer
199        self.timer = self.timer + ms
200
201        if not self.paused then
202            local done = false
203
204            -- Check for goal intersection
205            if self.cd:checkCollision(self.player, self.goal) then
206                self:captureGoal()
207            else
208                -- Update goal direction
209                if self.difficulty >= Common.difficultyLevel.Hard and self.timer >= self.goalDirectionTimer then
210                    self:resetGoalDirection()
211                end
212
213                local newPoints = self.difficulty * pointProgression[max(1, min(#pointProgression, 1 + floor(self.timer / boardTimeout * #pointProgression)))]
214
215                if newPoints ~= self.points then
216                    self:setPoints(newPoints)
217                end
218            end
219
220            -- Check for enemy intersection
221            self.cd:forEachCollision(function(p, e)
222                done = true
223            end, Common.group.player, Common.group.enemy)
224
225            if done then
226                self:lose()
227            end
228        else
229            if self.finishTimer > 0 and self.timer >= self.finishTimer then
230                ended()
231            end
232        end
233    end
234
235    function board.setPoints(self, points)
236        self.points = points
237    end
238
239    function board.getDifficulty(self)
240        return self.difficulty
241    end
242
243    function board.setDifficulty(self, difficulty)
244        self.difficulty = difficulty
245    end
246
247    function board.getScore(self)
248        return self.score
249    end
250
251    function board.setScore(self, score)
252        self.score = score
253    end
254
255    function board.getPaused(self)
256        return self.paused
257    end
258
259    function board.addToScore(self, points)
260        self:setScore(self.score + points)
261    end
262
263    function board.captureGoal(self)
264        self:addToScore(self.points)
265
266        -- Create a ghost and move the goal
267        self.goal:createGhost():setParent(self)
268        self:resetGoal()
269
270        Audio.play("Sounds/Score.wav")
271
272        -- Add another enemy
273        self:addEnemy()
274    end
275
276    function board.lose(self)
277        self.player:clearMovingStates()
278        self.player:createGhost():setParent(self)
279        self:removeChild(self.player)
280        self.paused = true
281        self.finishTimer = self.timer + transitionPeriod
282
283        Audio.play("Sounds/Boom.wav")
284
285        lost()
286    end
287
288    function board.reset(self)
289        self.paused = false
290        self.finishTimer = 0
291        self:setScore(0)
292
293        self:clearChildren()
294        self.cd:clearChildren()
295
296        self:addChild(self.player)
297        self:addChild(self.goal)
298        self.cd:addChild(self.player)
299        self.cd:addChild(self.goal)
300
301        self:resetGoal()
302    end
303
304    return board
305end
306
307