chichilku3 14.0.4 → 15.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,26 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../share/math'
4
+
5
+ # high level game logic
1
6
  class GameLogic
2
7
  def initialize(console)
3
8
  @console = console
4
9
  @alive_players = 0
5
10
  end
6
11
 
12
+ def on_player_connect(client, players)
13
+ player = Player.get_player_by_id(players, client[PLAYER_ID])
14
+ return if player.nil?
15
+
16
+ port, ip = Socket.unpack_sockaddr_in(client[NET_CLIENT].getpeername)
17
+ @console.log "player joined ID=#{player.id} IP=#{ip}:#{port} name='#{player.name}'"
18
+ end
19
+
20
+ def on_player_disconnect(client, players)
21
+ player = Player.get_player_by_id(players, client[PLAYER_ID])
22
+ return if player.nil?
23
+
24
+ @console.log "player left ID=#{player.id} name='#{player.name}'"
25
+ end
26
+
7
27
  def check_collide(players, player)
8
28
  players.each do |other|
9
29
  next if other == player
10
- next if !player.collide[:down]
30
+ next unless player.collide[:down]
11
31
 
12
32
  x_force = player.check_player_collide(other)
13
- player.apply_force(x_force, -8) if !x_force.zero?
33
+ player.apply_force(x_force, -8) unless x_force.zero?
14
34
  end
15
35
  end
16
36
 
17
- def tick(players, dt)
37
+ def tick(game_map, players, dt, tick)
18
38
  players.each do |player|
19
39
  # reset values (should stay first)
20
40
  player.reset_collide
21
41
 
22
- gravity(player, dt)
42
+ gravity(game_map, player, dt, tick)
23
43
  player.tick
44
+ game_map_collision_vertical(game_map, player)
45
+ if player.dx.positive?
46
+ player.check_move_right(game_map)
47
+ elsif player.dx.negative?
48
+ player.check_move_left(game_map)
49
+ end
24
50
  player.projectile.tick(players)
25
51
  # player collsions works
26
52
  # but it eats performance and delays jumping
@@ -28,14 +54,51 @@ class GameLogic
28
54
  end
29
55
  end
30
56
 
31
- def handle_client_requests(data, id, players, dt)
57
+ def game_map_collision_vertical(game_map, player)
58
+ if player.dy.positive?
59
+ # left bottom
60
+ col = game_map.collision?(player.x / TILE_SIZE, (player.y + player.h) / TILE_SIZE)
61
+ if col
62
+ player.y = col[:y] * TILE_SIZE
63
+ player.y -= player.crouching? ? PLAYER_SIZE / 2 : PLAYER_SIZE
64
+ player.do_collide(:down, true)
65
+ end
66
+ # right bottom
67
+ col = game_map.collision?((player.x + player.w) / TILE_SIZE, (player.y + player.h) / TILE_SIZE)
68
+ if col
69
+ player.y = col[:y] * TILE_SIZE
70
+ player.y -= player.crouching? ? PLAYER_SIZE / 2 : PLAYER_SIZE
71
+ player.do_collide(:down, true)
72
+ end
73
+ elsif player.dy.negative?
74
+ # left top
75
+ col = game_map.collision?(player.x / TILE_SIZE, player.y / TILE_SIZE)
76
+ if col
77
+ player.y = (col[:y] * TILE_SIZE) + player.h
78
+ player.y += 1
79
+ player.y += PLAYER_SIZE / 2 if player.crouching?
80
+ player.do_collide(:up, true)
81
+ end
82
+ # right top
83
+ col = game_map.collision?((player.x + player.w) / TILE_SIZE, player.y / TILE_SIZE)
84
+ if col
85
+ player.y = (col[:y] * TILE_SIZE) + player.h
86
+ player.y += 1
87
+ player.y += PLAYER_SIZE / 2 if player.crouching?
88
+ player.do_collide(:up, true)
89
+ end
90
+ end
91
+ nil
92
+ end
93
+
94
+ def handle_client_requests(game_map, data, id, players, _dt)
32
95
  player = Player.get_player_by_id(players, id)
33
96
  if player.nil?
34
97
  @console.log "WARNING failed to update nil player with id=#{id}"
35
- if players.count > 0
36
- @console.log "connected players:"
98
+ if players.count.positive?
99
+ @console.log 'connected players:'
37
100
  else
38
- @console.log "no players currently connected!"
101
+ @console.log 'no players currently connected!'
39
102
  end
40
103
  players.each do |p|
41
104
  @console.log "id=#{p.id} name='#{p.name}'"
@@ -44,28 +107,37 @@ class GameLogic
44
107
  end
45
108
 
46
109
  # reset values (should stay first)
47
- player.state[:crouching] = false
110
+ player.wants_crouch = false
48
111
 
49
112
  # move request
50
113
  if data[0] == '1'
51
114
  @console.dbg "player=#{id} wants to crouch"
52
- player.state[:crouching] = true
53
- player.x -= TILE_SIZE / 4 unless player.was_crouching
115
+ player.crouch!
116
+ player.wants_crouch = true
117
+ player.x -= PLAYER_SIZE / 4 unless player.was_crouching
118
+ # TODO: why is it checking right when on left side!?
119
+ if closest_interval_side(TILE_SIZE, player.x) == SIDE_LEFT
120
+ player.check_move_right(game_map)
121
+ else
122
+ player.check_move_left(game_map)
123
+ end
54
124
  player.was_crouching = true
55
125
  end
56
126
  if data[1] == 'l'
127
+ game_map_collision_vertical(game_map, player)
57
128
  @console.dbg "player=#{id} wants to walk left"
58
- player.move_left
129
+ player.move_left(game_map)
59
130
  end
60
131
  if data[1] == 'r'
61
132
  @console.dbg "player=#{id} wants to walk right"
62
- player.move_right
133
+ game_map_collision_vertical(game_map, player)
134
+ player.move_right(game_map)
63
135
  end
64
136
  if data[2] == '1'
65
137
  @console.dbg "player=#{id} wants to jump"
66
138
  player.do_jump
67
139
  end
68
- if data[3] == '1' && player.state[:crouching] == false
140
+ if data[3] == '1' && player.crouching? == false
69
141
  @console.dbg "player=#{id} wants to fire"
70
142
  player.fire_ticks += 1
71
143
  if player.fire_ticks > 29
@@ -76,39 +148,40 @@ class GameLogic
76
148
  player.state[:fire] = 1
77
149
  end
78
150
  else
79
- if player.fire_ticks > 0
80
- dx = (player.aimX - player.x).clamp(-200, 200) / 20
81
- dy = (player.aimY - player.y).clamp(-200, 200) / 20
151
+ if player.fire_ticks.positive?
152
+ dx = (player.aim_x - player.x).clamp(-200, 200) / 20
153
+ dy = (player.aim_y - player.y).clamp(-200, 200) / 20
82
154
  dx *= (player.fire_ticks / 10).clamp(1, 3)
83
155
  dy *= (player.fire_ticks / 10).clamp(1, 3)
84
- player.projectile.fire(player.x + TILE_SIZE/4, player.y + TILE_SIZE/2, dx, dy, player)
156
+ player.projectile.fire(player.x + TILE_SIZE / 4, player.y + TILE_SIZE / 2, dx, dy, player)
85
157
  end
86
158
  player.fire_ticks = 0
87
159
  player.state[:fire] = 0
88
160
  end
89
- player.aimX = net_unpack_bigint(data[4..5])
90
- player.aimY = net_unpack_bigint(data[6..7])
91
- # player.projectile.x = player.aimX + 20
92
- # player.projectile.y = player.aimY + 20
161
+ player.aim_x = net_unpack_bigint(data[4..5])
162
+ player.aim_y = net_unpack_bigint(data[6..7])
163
+ # player.projectile.x = player.aim_x + 20
164
+ # player.projectile.y = player.aim_y + 20
93
165
 
94
- player.check_out_of_world
166
+ player.check_out_of_game_map
95
167
 
96
168
  # return updated players
97
169
  players
98
170
  end
99
171
 
100
- def posttick(players, dt)
172
+ def posttick(game_map, players, _dt)
101
173
  players.each do |player|
102
174
  # stopped crouching -> stand up
103
- if player.was_crouching && player.state[:crouching] == false
104
- player.y -= TILE_SIZE
105
- player.x += TILE_SIZE / 4
106
- player.was_crouching = false
107
- end
175
+ next unless player.was_crouching && player.wants_crouch == false
176
+
177
+ player.x += PLAYER_SIZE / 4
178
+ player.was_crouching = false
179
+ player.stop_crouch!
180
+ game_map_collision_vertical(game_map, player)
108
181
  end
109
182
  end
110
183
 
111
- def gravity(player, dt)
184
+ def gravity(game_map, player, _dt, _tick)
112
185
  if player.dead
113
186
  player.dead_ticks += 1
114
187
  player.state[:bleeding] = true
@@ -117,31 +190,14 @@ class GameLogic
117
190
  player.state[:bleeding] = false
118
191
  player.die
119
192
  end
120
- else
121
- if player.y + player.h > 384 # too far down --> die
122
- player.dead = true
123
- player.dead_ticks = 0
124
- end
125
- end
126
-
127
- # outside of the save zone
128
- if player.x < 214 || player.x > 800 || player.dead
129
- if player.y + player.h > 484
130
- # player.collide[:down] = true
131
- player.do_collide(:down, true)
132
- return
133
- end
134
- else # on the save zone
135
- if player.y + player.h > 324
136
- # player.collide[:down] = true
137
- player.do_collide(:down, true)
138
- return
139
- end
193
+ elsif game_map.death?(player.x / TILE_SIZE, (player.y + player.h) / TILE_SIZE)
194
+ player.dead = true
195
+ player.dead_ticks = 0
140
196
  end
141
197
 
142
198
  # grav = 100000 * dt
143
199
  # @console.log "grav: #{grav}"
144
200
  # player.y += grav
145
- player.dy += 2 if player.dy < 16
201
+ player.dy += 1 if player.dy < 16
146
202
  end
147
203
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require_relative '../share/config'
3
5
 
data/lib/share/config.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'os'
3
5
  require 'fileutils'
@@ -7,7 +9,7 @@ class Config
7
9
  attr_reader :data, :chichilku3_dir
8
10
 
9
11
  def initialize(console, file)
10
- @chichilku3_dir = ""
12
+ @chichilku3_dir = ''
11
13
  if OS.linux?
12
14
  @chichilku3_dir = "#{ENV['HOME']}/.chichilku/chichilku3/"
13
15
  elsif OS.mac?
@@ -15,13 +17,24 @@ class Config
15
17
  # elsif OS.windows?
16
18
  # @chichilku3_dir = "%APPDATA%\\chichilku\\chichilku3\\"
17
19
  else
18
- puts "os not supported."
20
+ puts 'os not supported.'
19
21
  exit
20
22
  end
21
- puts "path: " + @chichilku3_dir
23
+ puts "path: #{@chichilku3_dir}"
22
24
  FileUtils.mkdir_p @chichilku3_dir
23
25
  FileUtils.mkdir_p "#{@chichilku3_dir}recordings"
26
+ FileUtils.mkdir_p "#{@chichilku3_dir}maps_b64"
27
+ FileUtils.mkdir_p "#{@chichilku3_dir}downloadedmaps"
28
+ FileUtils.mkdir_p "#{@chichilku3_dir}tmp"
29
+ unless File.directory? "#{@chichilku3_dir}maps"
30
+ if File.directory? 'maps'
31
+ FileUtils.cp_r 'maps', "#{@chichilku3_dir}maps"
32
+ else
33
+ FileUtils.mkdir_p "#{@chichilku3_dir}maps"
34
+ end
35
+ end
24
36
  create_default_cfg(file, "#{@chichilku3_dir}/#{file}")
37
+ @source_file = file
25
38
  @file = @chichilku3_dir + file
26
39
  @console = console
27
40
  @data = load
@@ -31,7 +44,7 @@ class Config
31
44
  return if File.file?(to)
32
45
 
33
46
  tmp = JSON.parse(File.read(from))
34
- File.open(to,"w") do |f|
47
+ File.open(to, 'w') do |f|
35
48
  f.write(tmp.to_json)
36
49
  end
37
50
  end
@@ -41,13 +54,14 @@ class Config
41
54
  end
42
55
 
43
56
  def load
57
+ defaults = JSON.parse(File.read(@source_file))
44
58
  data = JSON.parse(File.read(@file))
45
- data = sanitize_data(data)
46
- data
59
+ data = defaults.merge(data)
60
+ sanitize_data(data)
47
61
  end
48
62
 
49
63
  def save
50
- File.open(@file, "w") do |f|
64
+ File.open(@file, 'w') do |f|
51
65
  f.write(JSON.pretty_generate(data))
52
66
  end
53
67
  end
data/lib/share/console.rb CHANGED
@@ -1,22 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'string'
1
4
  DEBUG = false
5
+ DEBUG_PHYSICS = true
2
6
 
3
7
  # Console used by Client and Server
4
8
  class Console
5
9
  def log(message)
6
- t = Time.now
7
- puts format("[%02d:%02d:%02d][log] %s", t.hour, t.min, t.sec, message)
10
+ log_type('log', message)
8
11
  end
9
12
 
10
13
  def err(message)
11
- t = Time.now
12
- puts format("[%02d:%02d:%02d][error] %s", t.hour, t.min, t.sec, message)
14
+ log_type('error'.red, message)
15
+ end
16
+
17
+ def wrn(message)
18
+ log_type('warning'.yellow, message)
13
19
  end
14
20
 
15
21
  def dbg(message)
16
22
  return unless DEBUG
17
23
 
24
+ log_type('debug'.pink, message)
25
+ end
26
+
27
+ private
28
+
29
+ def log_type(type, message)
18
30
  t = Time.now
19
- puts format("[%02d:%02d:%02d][debug] %s", t.hour, t.min, t.sec, message)
31
+ puts format('[%<hour>02d:%<min>02d:%<sec>02d][%<type>s] %<msg>s',
32
+ hour: t.hour,
33
+ min: t.min,
34
+ sec: t.sec,
35
+ type: type,
36
+ msg: message)
20
37
  end
21
38
  end
22
39
 
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'digest/sha1'
5
+ require 'zip'
6
+ require 'fileutils'
7
+
8
+ require_relative '../external/rubyzip/recursive'
9
+
10
+ MAX_UNZIP_SIZE = 1024**2 # 1MiB
11
+ MAP_VERSION = 1
12
+ MAP_FILES = [
13
+ 'background.png',
14
+ 'gametiles.txt',
15
+ 'metadata.json'
16
+ ].freeze
17
+
18
+ # GameMap class handles the game_map file format
19
+ class GameMap
20
+ attr_reader :gametiles, :grass_rows, :ready, :metadata
21
+
22
+ def initialize(console, cfg, mapname, callback = nil, checksum = nil)
23
+ @console = console
24
+ @cfg = cfg
25
+ @mapname = mapname
26
+ @b64_size = -1
27
+ @b64_data = ''
28
+ @sha1sum = checksum
29
+ @gametiles = []
30
+ # grass_rows
31
+ # array of hashes containing connected grass tiles
32
+ # a row of grass from x 0 to x 10 at the height 2 would look like this
33
+ # {x1: 0, x2: 10, y: 2}
34
+ @grass_rows = []
35
+ @ready = false
36
+ @metadata = nil
37
+
38
+ # client
39
+ @callback = callback
40
+ @progress = 0
41
+ @tmpfile = nil
42
+ end
43
+
44
+ def checksum
45
+ @sha1sum
46
+ end
47
+
48
+ def name
49
+ @mapname
50
+ end
51
+
52
+ # TODO: fix this rubocop
53
+ # rubocop:disable Naming/AccessorMethodName
54
+ def set_name(name)
55
+ raise unless @mapname.nil?
56
+
57
+ @mapname = name
58
+ end
59
+
60
+ def size
61
+ @b64_size
62
+ end
63
+
64
+ def set_size(size)
65
+ raise unless @b64_size == -1
66
+
67
+ @b64_size = size
68
+ end
69
+ # rubocop:enable Naming/AccessorMethodName
70
+
71
+ def load_gametiles(map_dir)
72
+ gamefile = "#{map_dir}/gametiles.txt"
73
+ unless File.exist? gamefile
74
+ @console.err "could not load gametiles '#{gamefile}'"
75
+ exit 1
76
+ end
77
+
78
+ is_skip = true
79
+ @gametiles = []
80
+ @grass_rows = []
81
+ File.readlines(gamefile).each_with_index do |data, i|
82
+ gamerow = data[0..-2] # cut off newline
83
+ is_skip = !is_skip if gamerow =~ /\+-+\+/
84
+ gamerow = gamerow.match(/\|(.*)\|/)
85
+ next if gamerow.nil?
86
+
87
+ gamerow = gamerow[1]
88
+ next if is_skip
89
+
90
+ if gamerow.length != MAP_WIDTH
91
+ @console.err "invalid gametiles row=#{i} size=#{gamerow.length}/#{MAP_WIDTH}"
92
+ exit 1
93
+ end
94
+ @gametiles << gamerow
95
+ end
96
+ if @gametiles.length != MAP_HEIGHT
97
+ @console.err "invalid gametiles rows=#{@gametiles.length}/#{MAP_HEIGHT}"
98
+ exit 1
99
+ end
100
+ y = 0
101
+ grass = {}
102
+ @gametiles.each do |gamerow|
103
+ x = 0
104
+ gamerow.chars.each do |tile|
105
+ if tile == 'i'
106
+ grass[:x2] = x * TILE_SIZE + TILE_SIZE
107
+ grass[:y] = y * TILE_SIZE + TILE_SIZE / 2 + 2
108
+ grass[:x1] = x * TILE_SIZE if grass[:x1].nil?
109
+ else
110
+ @grass_rows.push(grass) unless grass == {}
111
+ grass = {}
112
+ end
113
+ x += 1
114
+ end
115
+ y += 1
116
+ end
117
+ nil
118
+ end
119
+
120
+ def load_metadata(map_dir)
121
+ metafile = "#{map_dir}/metadata.json"
122
+ unless File.exist? metafile
123
+ @console.err "could not load gametiles '#{metafile}'"
124
+ exit 1
125
+ end
126
+
127
+ @metadata = JSON.parse(File.read(metafile))
128
+ if @metadata['chichilku3-map-version'] != MAP_VERSION
129
+ @console.err "Failed to load map '#{@metadata['name']}':"
130
+ @console.err " Expected map version '#{MAP_VERSION}' but got '#{@metadata['chichilku3-map-version']}'"
131
+ exit 1
132
+ end
133
+ @console.log "loaded map '#{@metadata['name']}' (#{@metadata['version']}) by #{@metadata['authors'].join(',')}"
134
+ end
135
+
136
+ def load_data(map_dir)
137
+ load_gametiles(map_dir)
138
+ load_metadata(map_dir)
139
+ @ready = true
140
+ end
141
+
142
+ def death?(x, y)
143
+ { x: x, y: y } if @gametiles[y][x] == 'X'
144
+ end
145
+
146
+ def collision?(x, y)
147
+ { x: x, y: y } if @gametiles[y][x] == 'O'
148
+ end
149
+
150
+ def grass?(x, y)
151
+ { x: x, y: y } if @gametiles[y][x] == 'i'
152
+ end
153
+
154
+ # SERVER
155
+
156
+ def prepare_upload
157
+ return if @mapname == '' || @mapname.nil?
158
+
159
+ map_dir = "#{@cfg.chichilku3_dir}maps/#{@mapname}"
160
+ unless File.directory? map_dir
161
+ @console.err "failed to load map '#{@mapname}' (directory not found)"
162
+ exit 1
163
+ end
164
+ unless File.exist? "#{map_dir}/background.png"
165
+ @console.err "failed to load map '#{@mapname}' (no background.png)"
166
+ exit 1
167
+ end
168
+ unless File.exist? "#{map_dir}/gametiles.txt"
169
+ @console.err "failed to load map '#{@mapname}' (no gametiles.txt)"
170
+ exit 1
171
+ end
172
+ load_data(map_dir)
173
+ zip
174
+ encode
175
+ end
176
+
177
+ def zip
178
+ map_dir = "#{@cfg.chichilku3_dir}maps/#{@mapname}"
179
+ map_zip = "#{@cfg.chichilku3_dir}maps/#{@mapname}.zip"
180
+ File.delete map_zip if File.exist? map_zip
181
+
182
+ @console.log "archiving map '#{map_zip}' ..."
183
+ Zip::File.open(map_zip, Zip::File::CREATE) do |zipfile|
184
+ MAP_FILES.each do |filename|
185
+ filepath = File.join(map_dir, filename)
186
+ unless File.exist? filepath
187
+ @console.err "failed to zip map '#{@mapname}' missing file:"
188
+ @console.err filepath
189
+ exit 1
190
+ end
191
+ zipfile.add(filename, filepath)
192
+ end
193
+ end
194
+ end
195
+
196
+ def encode
197
+ rawfile = "#{@cfg.chichilku3_dir}maps/#{@mapname}.zip"
198
+ @console.log "encoding map archive '#{@mapname}' ..."
199
+ File.open(rawfile, 'rb') do |map_png|
200
+ raw_content = map_png.read
201
+ @sha1sum = Digest::SHA1.hexdigest raw_content
202
+ encodefile = "#{@cfg.chichilku3_dir}maps_b64/#{@mapname}_#{checksum}.zip"
203
+ File.open(encodefile, 'wb') do |map_encoded|
204
+ @b64_data = Base64.encode64(raw_content).delete! "\n"
205
+ @b64_size = @b64_data.size
206
+ map_encoded.write(@b64_data)
207
+ end
208
+ end
209
+ @console.log "finished encoding size=#{@b64_size} checksum=#{checksum}"
210
+ end
211
+
212
+ def get_data(offset, size)
213
+ return nil if @mapname == '' || @mapname.nil?
214
+
215
+ if offset + size > @b64_size
216
+ @b64_data[offset..-1].ljust(size, ' ')
217
+ else
218
+ @b64_data[offset...offset + size]
219
+ end
220
+ end
221
+
222
+ # CLIENT
223
+
224
+ def dl_path
225
+ "#{@cfg.chichilku3_dir}downloadedmaps/#{@mapname}_#{checksum}"
226
+ end
227
+
228
+ def prepare_download
229
+ @tmpfile = "#{@cfg.chichilku3_dir}tmp/#{@mapname}"
230
+ File.delete @tmpfile if File.exist? @tmpfile
231
+ end
232
+
233
+ def download(data)
234
+ data.strip!
235
+ @progress += data.size
236
+ @console.dbg "downloading #{@progress} / #{@b64_size} ..."
237
+ IO.write(@tmpfile, data, mode: 'a')
238
+ if @progress >= @b64_size
239
+ @console.log 'finished download'
240
+ @callback.call(load)
241
+ end
242
+ @progress
243
+ end
244
+
245
+ def found?
246
+ File.directory? dl_path
247
+ end
248
+
249
+ def unzip
250
+ map_archive = "#{dl_path}.zip"
251
+ map_dir = dl_path
252
+ FileUtils.mkdir_p map_dir
253
+ Dir.chdir map_dir do
254
+ Zip::File.open(map_archive) do |zip_file|
255
+ zip_file.each do |entry|
256
+ @console.log "extracting '#{entry.name}' ...'"
257
+ raise 'File too large when extracted' if entry.size > MAX_UNZIP_SIZE
258
+
259
+ entry.extract
260
+ end
261
+ end
262
+ end
263
+ File.delete map_archive if File.exist? map_archive
264
+ map_dir
265
+ end
266
+
267
+ def load
268
+ outfile = "#{dl_path}.zip"
269
+ @console.log 'converting downloaded map ...'
270
+ File.open(@tmpfile, 'rb') do |map_encoded|
271
+ File.open(outfile, 'wb') do |map_png|
272
+ map_png.write(
273
+ Base64.decode64(map_encoded.read)
274
+ )
275
+ end
276
+ end
277
+ unzip
278
+ end
279
+ end
data/lib/share/math.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ SIDE_LEFT = -1
4
+ SIDE_RIGHT = 1
5
+
6
+ ##
7
+ # Checks wether a given point is closer to the left or right step of a given interval
8
+ #
9
+ # @return -1 for left and 1 for right
10
+ # @param interval [Integer] interval size.
11
+ # @param point [Integer] point to check.
12
+ def closest_interval_side(interval, point)
13
+ (point % interval) * 2 < interval ? SIDE_LEFT : SIDE_RIGHT
14
+ end