a9e18444095a645077cfc9cedd5756872c06c23b
[nit.git] / contrib / tinks / src / game / world.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Physical world logic
16 module world is serialize
17
18 import noise
19 import more_collections
20
21 import framework
22
23 redef class TGame
24 # The physical world in which this game happens
25 var world = new World(self)
26 end
27
28 # A terrain features (a rock, a tree, etc.)
29 class Feature
30 super Ruled
31 redef type R: FeatureRule
32
33 # Top-left corner of this feature
34 var pos: Pos
35 end
36
37 # Metadata for a `Feature`
38 class FeatureRule
39 super Rule
40
41 # Strength to resist on `World::explode`
42 var strength: Int
43 end
44
45 # The physical world of the `game`
46 class World
47 # Associated `TGame`
48 var game: TGame
49
50 # Past blast sites
51 var blast_sites = new Array[Pos]
52
53 private var mountain_map: Noise do
54 var map = new PerlinNoise
55 map.period = 10.0
56 return map
57 end
58
59 private var forest_map: Noise do
60 var map = new PerlinNoise
61 map.period = 24.0
62 return map
63 end
64
65 # Cache of discovered features, also keeps tracks of changes
66 private var features_cache = new FeatureMap
67
68 # Get the `Feature` at `x, y`, if any
69 fun [](x, y: Int): nullable Feature
70 do
71 if features_cache.has(x, y) then return features_cache[x, y]
72
73 # Generate a feature from the noise map
74 var pos = new Pos(x.to_f+0.5, y.to_f+0.5)
75
76 var feature = null
77 if mountain_map[x.to_f, y.to_f] > 0.55 then
78 feature = new Feature(game.story.rock, pos)
79 else if forest_map[x.to_f, y.to_f] > 0.5 then
80 feature = new Feature(game.story.tree, pos)
81 end
82
83 # Update cache
84 features_cache[x, y] = feature
85
86 return feature
87 end
88
89 # Detect the first collision with a `Feature` from `src` to `dst`
90 #
91 # This is the main collision detection method used by tanks shots, and at tank movement.
92 # The idea is to check all cases between `src` and `dst` and return the first feature found.
93 # Returns `null` if there is no obstacle features.
94 #
95 # Example of the cases that would be checked between `s` and `d`:
96 #
97 # ~~~raw
98 # ................
99 # .s###...........
100 # ....######......
101 # .........####d..
102 # ................
103 # ~~~
104 fun first_collision(src, dst: Pos): nullable Feature
105 do
106 var going_left = dst.x < src.x
107 var angle = src.atan2(dst)
108 var slope = angle.tan
109
110 # Soften slopes approaching infinity
111 if slope > 100.0 then slope = 100.0
112 if slope < -100.0 then slope = -100.0
113
114 # For each column (over x) from src.x to dst.x
115 var x0 = src.x.floor.to_i
116 var x1 = dst.x.floor.to_i
117 for x in [x0 .. x1].smart_step do
118 var dx = x.to_f - src.x
119 var y0 = src.y + dx*slope
120 var y1 = src.y + (dx+1.0)*slope
121
122 var first = y0.floor.to_i
123 var last = y1.floor.to_i
124 if going_left then
125 # Invert the first and last element of the range
126 var swap = first
127 first = last
128 last = swap
129 end
130
131 # For each row (over y)
132 # from where the line enters the column to where it leaves it
133 for y in [first .. last].smart_step do
134 if not y.in_between_floats(src.y, dst.y) then continue
135
136 var feature = self[x.to_i, y]
137 if feature != null then return feature
138 end
139 end
140
141 return null
142 end
143
144 # Apply an explosion at `center` of the given `power`
145 fun explode(turn: TTurn, center: Pos, power: Int)
146 do
147 var x = center.x.floor.to_i
148 var y = center.y.floor.to_i
149 var range = [-power .. power]
150 var features = new Array[Feature]
151
152 for dx in range do
153 for dy in range do
154 var f = self[x+dx, y+dy]
155 var force = (power-dx.abs) + (power-dy.abs)
156 if f != null and f.rule.strength <= force then features.add f
157 end
158 end
159
160 turn.add new ExplosionEvent(center, power, features)
161 end
162 end
163
164 # Map of features organized by their coordinates
165 #
166 # The naive implementation is using a `HashMap2`.
167 # This class can be redefed with optimizations as needed.
168 class FeatureMap
169 super HashMap2[Int, Int, nullable Feature]
170 end
171
172 redef class Story
173 # Forest tree
174 var tree = new FeatureRule(self, 2)
175
176 # Big rock
177 var rock = new FeatureRule(self, 3)
178
179 # Metallic debris
180 var debris = new FeatureRule(self, 4)
181 end
182
183 # An explosion
184 class ExplosionEvent
185 super TEvent
186
187 # Center of the explosion
188 var pos: Pos
189
190 # Power of the blast
191 var power: Int
192
193 # All the features this explosion destroys
194 var destroyed_features: Array[Feature]
195
196 redef fun apply(game)
197 do
198 for feature in destroyed_features do
199 game.world.features_cache[feature.pos.x.floor.to_i, feature.pos.y.floor.to_i] = null
200 end
201
202 game.world.blast_sites.add pos
203 if game.world.blast_sites.length > 100 then game.world.blast_sites.shift
204 end
205 end
206
207 # The feature at `pos` changes to `feature`
208 class FeatureChangeEvent
209 super TEvent
210
211 # New `Feature`, if any
212 var feature: nullable Feature
213
214 # `Pos` of this change
215 var pos: Pos
216
217 redef fun apply(game)
218 do
219 game.world.features_cache[pos.x.floor.to_i, pos.y.floor.to_i] = feature
220 end
221 end
222
223 # ---
224 # Services
225
226 # Position in the world
227 class Pos
228 super Point[Float]
229
230 # Add `self` to `other` and return the new position
231 fun +(other: Point[Float]): Pos
232 do
233 var nx = other.x.add(x)
234 var ny = other.y.add(y)
235 return new Pos(x.value_of(nx), y.value_of(ny))
236 end
237 end
238
239 redef universal Int
240 # Is `self` in between `a` and `b`?
241 #
242 # ~~~
243 # assert 1.in_between_floats(0.0, 2.0)
244 # assert 1.in_between_floats(2.0, 0.0)
245 # assert not 1.in_between_floats(2.0, 4.0)
246 # ~~~
247 fun in_between_floats(a, b: Float): Bool
248 do
249 var f = to_f
250 if a < b then return a.floor - 1.0 < f and f < b.ceil
251 return a.ceil > f and f > b.floor - 1.0
252 end
253 end
254
255 redef universal Float
256 # Get the vector with `self` as direction and the given `magnitude`
257 fun to_vector(magnitude: Float): Pos
258 do
259 return new Pos(cos*magnitude, sin*magnitude)
260 end
261 end
262
263 redef class Range[E]
264 # Step appropriately to go from `first` to `last`
265 #
266 # ~~~
267 # assert [1..3].smart_step.to_a == [1, 2, 3]
268 # assert [3..1].smart_step.to_a == [3, 2, 1]
269 # ~~~
270 fun smart_step: Iterator[E]
271 do
272 var step = 1
273 if first > last then step = -1
274 return self.step(step)
275 end
276 end