doom 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77ff94b86295598915c520657fb36512c6fc091d24d6660cc8dea588a8939062
4
- data.tar.gz: 3b3632ab2f9adbd0d8d4078c71f5c9f941943dd30d96c951bb27e803256a87d1
3
+ metadata.gz: 70d72175c1f115b3363e188d7075047bc32cece4f3a3dd1feda17dc6f965b07f
4
+ data.tar.gz: 22eb9172267c578a6fbaa3dd526ec19e8d4587dba9a349080ad1626d3e04fd92
5
5
  SHA512:
6
- metadata.gz: 61d4a1cf4f3542c3ce93d4dc624b2cc36c4d93be39a9d65cc832198be7bbd04b9bd86cc7723d13181b80907809005571d7a9f85217e628d6f5a5b7a02e35908a
7
- data.tar.gz: 514bc92b65f0934cbf31828e4553d6978d86932afa690f70b89a69ed6d6bbe71cb98e0d3bfbe86916e0c799498366eedcad2f74904f78be84fc3d559f43a8dd3
6
+ metadata.gz: 6f5fdd281119ba4bec5a29b8f9cd32f8d0aa37d1674125c3095835ba24942e66e6356037c3e424f320578cac5b1ed94dc7738388d3ced296f8de22ce78e9e79e
7
+ data.tar.gz: 35de5ff2f711aa4084a54d811d26b919cf8f1b00be1d9b06597b7e7aac09eab3ee8ff597b1ece2ac451d175dad2faf3c16bc73f3dbf56b50867407509fc4f234
data/README.md CHANGED
@@ -1,159 +1,119 @@
1
- # Ruby Doom
1
+ # DOOM Ruby
2
2
 
3
- A Ruby gem that ports the classic Doom game to Ruby, focusing on core gameplay without sound or networking features.
3
+ A faithful ruby port of the DOOM (1993) rendering engine to Ruby.
4
4
 
5
- ## Installation
5
+ ![DOOM Ruby Screenshot](https://raw.githubusercontent.com/khasinski/doom-rb/main/e1m1_spawn.png)
6
6
 
7
- Add this line to your application's Gemfile:
7
+ ## Features
8
8
 
9
- ```ruby
10
- gem 'doom'
11
- ```
9
+ - Pure Ruby implementation of DOOM's BSP rendering engine
10
+ - Accurate wall, floor, and ceiling rendering with proper texture mapping
11
+ - Sprite rendering with depth-correct clipping
12
+ - Original DOOM lighting and colormap support
13
+ - Mouse look and WASD movement controls
14
+ - Supports original WAD files (shareware and registered)
12
15
 
13
- And then execute:
16
+ ## Installation
14
17
 
15
18
  ```bash
16
- $ bundle install
19
+ gem install doom
17
20
  ```
18
21
 
19
- Or install it yourself as:
22
+ ## Quick Start
23
+
24
+ Just run `doom` - it will offer to download the free shareware version:
20
25
 
21
26
  ```bash
22
- $ gem install doom
27
+ doom
23
28
  ```
24
29
 
25
- ## Requirements
26
-
27
- - Ruby 2.6 or higher
28
- - A legal copy of Doom for the WAD files (e.g., DOOM.WAD)
29
-
30
- ## Usage
31
-
32
- ### Command Line Interface
33
-
34
- The gem includes two command-line tools:
35
-
36
- #### WAD Explorer
37
-
38
- The `wad` command allows you to explore WAD files:
30
+ Or specify your own WAD file:
39
31
 
40
32
  ```bash
41
- # Show general information about a WAD file
42
- $ wad -i DOOM.WAD
43
-
44
- # List all maps in a WAD file
45
- $ wad -l DOOM.WAD
33
+ doom /path/to/doom.wad
34
+ ```
46
35
 
47
- # Show details for a specific map
48
- $ wad -m E1M1 DOOM.WAD
36
+ ## Controls
49
37
 
50
- # List all textures in a WAD file
51
- $ wad -t DOOM.WAD
38
+ | Key | Action |
39
+ |-----|--------|
40
+ | W / Up Arrow | Move forward |
41
+ | S / Down Arrow | Move backward |
42
+ | A | Strafe left |
43
+ | D | Strafe right |
44
+ | Left Arrow | Turn left |
45
+ | Right Arrow | Turn right |
46
+ | Mouse | Look around (click to capture) |
47
+ | Escape | Release mouse / Quit |
52
48
 
53
- # Show details for a specific texture
54
- $ wad -x STARTAN1 DOOM.WAD
49
+ ## Requirements
55
50
 
56
- # List all sprites in a WAD file
57
- $ wad -s DOOM.WAD
51
+ - Ruby 3.1 or higher
52
+ - Gosu gem (for window/graphics)
53
+ - SDL2 (native library required by Gosu)
58
54
 
59
- # Show details for a specific sprite
60
- $ wad -p TROOA1 DOOM.WAD
55
+ ### Installing SDL2
61
56
 
62
- # Show help
63
- $ wad -h
57
+ **macOS:**
58
+ ```bash
59
+ brew install sdl2
64
60
  ```
65
61
 
66
- #### Game Launcher
67
-
68
- The `doom` command allows you to start the game:
69
-
62
+ **Ubuntu/Debian:**
70
63
  ```bash
71
- # Start the game with default settings
72
- $ doom DOOM.WAD
73
-
74
- # Start the game with a specific map
75
- $ doom -m E1M1 DOOM.WAD
76
-
77
- # Start the game with a custom window size
78
- $ doom -w 800 -h 600 DOOM.WAD
79
-
80
- # Start the game in fullscreen mode
81
- $ doom -f DOOM.WAD
82
-
83
- # Show help
84
- $ doom --help
64
+ sudo apt-get install build-essential libsdl2-dev libgl1-mesa-dev libfontconfig1-dev
85
65
  ```
86
66
 
87
- ### Ruby API
88
-
89
- You can also use the Ruby API in your own code:
90
-
91
- ```ruby
92
- require 'doom'
93
-
94
- # Load a WAD file
95
- loaders = Doom.load_wad('path/to/DOOM.WAD')
96
-
97
- # Access WAD information
98
- wad_loader = loaders[:wad]
99
- puts "WAD type: #{wad_loader.wad_type}"
100
- puts "Number of lumps: #{wad_loader.lumps.size}"
101
-
102
- # Access map information
103
- map_loader = loaders[:maps]
104
- puts "Available maps: #{map_loader.maps.join(', ')}"
105
-
106
- # Load a specific map
107
- map_data = map_loader.load_map('E1M1')
108
- puts "Number of things in E1M1: #{map_data[:things].size}"
109
-
110
- # Access texture information
111
- texture_loader = loaders[:textures]
112
- puts "Available textures: #{texture_loader.texture_names.join(', ')}"
113
-
114
- # Access sprite information
115
- sprite_loader = loaders[:sprites]
116
- puts "Available sprites: #{sprite_loader.sprite_names.join(', ')}"
67
+ **Fedora:**
68
+ ```bash
69
+ sudo dnf install SDL2-devel mesa-libGL-devel fontconfig-devel gcc-c++
117
70
  ```
118
71
 
119
- ## Features
72
+ **Arch Linux:**
73
+ ```bash
74
+ sudo pacman -S sdl2 mesa
75
+ ```
120
76
 
121
- ### Phase 1: WAD File Loader
77
+ **Windows:**
78
+ No additional setup needed - the gem includes SDL2.
122
79
 
123
- - WAD file parsing
124
- - Map data extraction
125
- - Texture information parsing
126
- - Sprite data handling
80
+ ## Development
127
81
 
128
- ### Phase 2: Rendering Engine (Current)
82
+ ```bash
83
+ git clone https://github.com/khasinski/doom-rb.git
84
+ cd doom-rb
85
+ bundle install
86
+ ruby bin/doom doom1.wad
87
+ ```
129
88
 
130
- - Window management
131
- - BSP rendering
132
- - Texture mapping
133
- - Sprite rendering
134
- - HUD implementation
135
- - Basic game loop
89
+ Run specs:
136
90
 
137
- ### Future Phases
91
+ ```bash
92
+ bundle exec rspec
93
+ ```
138
94
 
139
- - Game mechanics
140
- - Advanced game loop and state management
141
- - Polishing
95
+ ## Technical Details
142
96
 
143
- ## Development
97
+ This implementation includes:
144
98
 
145
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
99
+ - **BSP Traversal**: Front-to-back rendering using the map's BSP tree
100
+ - **Visplanes**: Floor/ceiling rendering with R_CheckPlane splitting
101
+ - **Drawsegs**: Wall segment tracking for proper sprite clipping
102
+ - **Texture Mapping**: Perspective-correct texture coordinates
103
+ - **Lighting**: Distance-based light diminishing with colormaps
146
104
 
147
- To install this gem onto your local machine, run `bundle exec rake install`.
105
+ ## Legal
148
106
 
149
- ## Contributing
107
+ DOOM is a registered trademark of id Software LLC. This is an unofficial fan project.
150
108
 
151
- Bug reports and pull requests are welcome on GitHub at https://github.com/khasinski/doom-rb.
109
+ The shareware version of DOOM (Episode 1) is freely distributable. For the full game,
110
+ please purchase DOOM from [Steam](https://store.steampowered.com/app/2280/Ultimate_Doom/),
111
+ [GOG](https://www.gog.com/pl/game/doom_doom_ii), or other retailers.
152
112
 
153
113
  ## License
154
114
 
155
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
115
+ GPL-2.0 - Same license as the original DOOM source code.
156
116
 
157
- ## Legal
117
+ ## Author
158
118
 
159
- Users must own a legal copy of Doom for the WAD files. This project complies with id Software's terms regarding Doom content.
119
+ Chris Hasinski ([@khasinski](https://github.com/khasinski))
data/bin/doom CHANGED
@@ -1,70 +1,59 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler/setup"
5
- require "doom"
6
- require "optparse"
7
-
8
- options = {
9
- map: nil,
10
- width: 640,
11
- height: 480,
12
- fullscreen: false
13
- }
14
-
15
- parser = OptionParser.new do |opts|
16
- opts.banner = "Usage: doom [options] WAD_FILE"
17
-
18
- opts.on("-m", "--map MAP", "Start the game with a specific map") do |map|
19
- options[:map] = map
20
- end
21
-
22
- opts.on("-w", "--width WIDTH", Integer, "Set the window width (default: 640)") do |width|
23
- options[:width] = width
24
- end
25
-
26
- opts.on("-h", "--height HEIGHT", Integer, "Set the window height (default: 480)") do |height|
27
- options[:height] = height
28
- end
29
-
30
- opts.on("-f", "--fullscreen", "Start in fullscreen mode") do
31
- options[:fullscreen] = true
32
- end
33
-
34
- opts.on("--help", "Show this help message") do
35
- puts opts
36
- exit
37
- end
4
+ # Enable YJIT if available (Ruby 3.1+) for better performance
5
+ if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
6
+ RubyVM::YJIT.enable
38
7
  end
39
8
 
40
- parser.parse!
41
-
42
- if ARGV.empty?
43
- puts "Error: WAD file is required"
44
- puts parser
45
- exit 1
9
+ # Parse arguments before loading heavy dependencies
10
+ wad_path = ARGV[0]
11
+
12
+ # Show help (before loading anything)
13
+ if wad_path == '-h' || wad_path == '--help'
14
+ puts "DOOM - Ruby port of the classic 1993 game"
15
+ puts
16
+ puts "Usage: doom [OPTIONS] [WAD_FILE]"
17
+ puts
18
+ puts "Options:"
19
+ puts " -h, --help Show this help message"
20
+ puts " -v, --version Show version"
21
+ puts
22
+ puts "If no WAD file is specified, doom will look for doom1.wad in:"
23
+ puts " 1. Current directory"
24
+ puts " 2. ~/.doom/"
25
+ puts
26
+ puts "If not found, you'll be prompted to download the shareware version."
27
+ puts
28
+ puts "Controls:"
29
+ puts " W/Up - Move forward"
30
+ puts " S/Down - Move backward"
31
+ puts " A - Strafe left"
32
+ puts " D - Strafe right"
33
+ puts " Left - Turn left"
34
+ puts " Right - Turn right"
35
+ puts " Mouse - Look around (click to capture)"
36
+ puts " Escape - Release mouse / Quit"
37
+ exit 0
46
38
  end
47
39
 
48
- wad_file = ARGV[0]
49
- unless File.exist?(wad_file)
50
- puts "Error: WAD file '#{wad_file}' not found"
51
- exit 1
40
+ if wad_path == '-v' || wad_path == '--version'
41
+ require_relative '../lib/doom/version'
42
+ puts "DOOM Ruby v#{Doom::VERSION}"
43
+ exit 0
52
44
  end
53
45
 
46
+ # Now load the full library
47
+ require_relative '../lib/doom'
48
+
54
49
  begin
55
- puts "Starting Doom with WAD file: #{wad_file}"
56
- puts "Map: #{options[:map] || 'Default'}"
57
- puts "Window size: #{options[:width]}x#{options[:height]}"
58
- puts "Fullscreen: #{options[:fullscreen]}"
59
-
60
- Doom.start_game(
61
- wad_file,
62
- options[:map],
63
- options[:width],
64
- options[:height],
65
- options[:fullscreen]
66
- )
67
- rescue Doom::Error => e
50
+ # Find or download WAD
51
+ wad_path = Doom::WadDownloader.ensure_wad_available(wad_path)
52
+ Doom.run(wad_path)
53
+ rescue Doom::WadDownloader::DownloadError => e
68
54
  puts "Error: #{e.message}"
69
55
  exit 1
70
- end
56
+ rescue Interrupt
57
+ puts "\nQuitting..."
58
+ exit 0
59
+ end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ module Map
5
+ Vertex = Struct.new(:x, :y)
6
+
7
+ Thing = Struct.new(:x, :y, :angle, :type, :flags)
8
+
9
+ Linedef = Struct.new(:v1, :v2, :flags, :special, :tag, :sidedef_right, :sidedef_left) do
10
+ FLAGS = {
11
+ BLOCKING: 0x0001,
12
+ BLOCKMONSTERS: 0x0002,
13
+ TWOSIDED: 0x0004,
14
+ DONTPEGTOP: 0x0008,
15
+ DONTPEGBOTTOM: 0x0010,
16
+ SECRET: 0x0020,
17
+ SOUNDBLOCK: 0x0040,
18
+ DONTDRAW: 0x0080,
19
+ MAPPED: 0x0100
20
+ }.freeze
21
+
22
+ def two_sided?
23
+ (flags & FLAGS[:TWOSIDED]) != 0
24
+ end
25
+
26
+ def upper_unpegged?
27
+ (flags & FLAGS[:DONTPEGTOP]) != 0
28
+ end
29
+
30
+ def lower_unpegged?
31
+ (flags & FLAGS[:DONTPEGBOTTOM]) != 0
32
+ end
33
+ end
34
+
35
+ Sidedef = Struct.new(:x_offset, :y_offset, :upper_texture, :lower_texture, :middle_texture, :sector)
36
+
37
+ Sector = Struct.new(:floor_height, :ceiling_height, :floor_texture, :ceiling_texture, :light_level, :special, :tag)
38
+
39
+ Seg = Struct.new(:v1, :v2, :angle, :linedef, :direction, :offset)
40
+
41
+ Subsector = Struct.new(:seg_count, :first_seg)
42
+
43
+ class Node
44
+ SUBSECTOR_FLAG = 0x8000
45
+
46
+ attr_reader :x, :y, :dx, :dy, :bbox_right, :bbox_left, :child_right, :child_left
47
+
48
+ BBox = Struct.new(:top, :bottom, :left, :right)
49
+
50
+ def initialize(x, y, dx, dy, bbox_right, bbox_left, child_right, child_left)
51
+ @x = x
52
+ @y = y
53
+ @dx = dx
54
+ @dy = dy
55
+ @bbox_right = bbox_right
56
+ @bbox_left = bbox_left
57
+ @child_right = child_right
58
+ @child_left = child_left
59
+ end
60
+
61
+ def right_is_subsector?
62
+ (@child_right & SUBSECTOR_FLAG) != 0
63
+ end
64
+
65
+ def left_is_subsector?
66
+ (@child_left & SUBSECTOR_FLAG) != 0
67
+ end
68
+
69
+ def right_index
70
+ @child_right & ~SUBSECTOR_FLAG
71
+ end
72
+
73
+ def left_index
74
+ @child_left & ~SUBSECTOR_FLAG
75
+ end
76
+ end
77
+
78
+ class MapData
79
+ attr_reader :name, :things, :vertices, :linedefs, :sidedefs, :sectors, :segs, :subsectors, :nodes
80
+
81
+ def initialize(name)
82
+ @name = name
83
+ @things = []
84
+ @vertices = []
85
+ @linedefs = []
86
+ @sidedefs = []
87
+ @sectors = []
88
+ @segs = []
89
+ @subsectors = []
90
+ @nodes = []
91
+ end
92
+
93
+ def self.load(wad, map_name)
94
+ map = new(map_name)
95
+
96
+ lump_idx = wad.directory.index { |e| e.name == map_name.upcase }
97
+ raise Error, "Map #{map_name} not found" unless lump_idx
98
+
99
+ map.load_things(wad.read_lump_at(wad.directory[lump_idx + 1]))
100
+ map.load_linedefs(wad.read_lump_at(wad.directory[lump_idx + 2]))
101
+ map.load_sidedefs(wad.read_lump_at(wad.directory[lump_idx + 3]))
102
+ map.load_vertices(wad.read_lump_at(wad.directory[lump_idx + 4]))
103
+ map.load_segs(wad.read_lump_at(wad.directory[lump_idx + 5]))
104
+ map.load_subsectors(wad.read_lump_at(wad.directory[lump_idx + 6]))
105
+ map.load_nodes(wad.read_lump_at(wad.directory[lump_idx + 7]))
106
+ map.load_sectors(wad.read_lump_at(wad.directory[lump_idx + 8]))
107
+
108
+ map
109
+ end
110
+
111
+ def load_things(data)
112
+ count = data.size / 10
113
+ count.times do |i|
114
+ offset = i * 10
115
+ @things << Thing.new(
116
+ data[offset, 2].unpack1('s<'),
117
+ data[offset + 2, 2].unpack1('s<'),
118
+ data[offset + 4, 2].unpack1('v'),
119
+ data[offset + 6, 2].unpack1('v'),
120
+ data[offset + 8, 2].unpack1('v')
121
+ )
122
+ end
123
+ end
124
+
125
+ def load_vertices(data)
126
+ count = data.size / 4
127
+ count.times do |i|
128
+ offset = i * 4
129
+ @vertices << Vertex.new(
130
+ data[offset, 2].unpack1('s<'),
131
+ data[offset + 2, 2].unpack1('s<')
132
+ )
133
+ end
134
+ end
135
+
136
+ def load_linedefs(data)
137
+ count = data.size / 14
138
+ count.times do |i|
139
+ offset = i * 14
140
+ @linedefs << Linedef.new(
141
+ data[offset, 2].unpack1('v'),
142
+ data[offset + 2, 2].unpack1('v'),
143
+ data[offset + 4, 2].unpack1('v'),
144
+ data[offset + 6, 2].unpack1('v'),
145
+ data[offset + 8, 2].unpack1('v'),
146
+ data[offset + 10, 2].unpack1('s<'),
147
+ data[offset + 12, 2].unpack1('s<')
148
+ )
149
+ end
150
+ end
151
+
152
+ def load_sidedefs(data)
153
+ count = data.size / 30
154
+ count.times do |i|
155
+ offset = i * 30
156
+ @sidedefs << Sidedef.new(
157
+ data[offset, 2].unpack1('s<'),
158
+ data[offset + 2, 2].unpack1('s<'),
159
+ data[offset + 4, 8].delete("\x00").strip,
160
+ data[offset + 12, 8].delete("\x00").strip,
161
+ data[offset + 20, 8].delete("\x00").strip,
162
+ data[offset + 28, 2].unpack1('v')
163
+ )
164
+ end
165
+ end
166
+
167
+ def load_sectors(data)
168
+ count = data.size / 26
169
+ count.times do |i|
170
+ offset = i * 26
171
+ @sectors << Sector.new(
172
+ data[offset, 2].unpack1('s<'),
173
+ data[offset + 2, 2].unpack1('s<'),
174
+ data[offset + 4, 8].delete("\x00").strip,
175
+ data[offset + 12, 8].delete("\x00").strip,
176
+ data[offset + 20, 2].unpack1('v'),
177
+ data[offset + 22, 2].unpack1('v'),
178
+ data[offset + 24, 2].unpack1('v')
179
+ )
180
+ end
181
+ end
182
+
183
+ def load_segs(data)
184
+ count = data.size / 12
185
+ count.times do |i|
186
+ offset = i * 12
187
+ @segs << Seg.new(
188
+ data[offset, 2].unpack1('v'),
189
+ data[offset + 2, 2].unpack1('v'),
190
+ data[offset + 4, 2].unpack1('s<'),
191
+ data[offset + 6, 2].unpack1('v'),
192
+ data[offset + 8, 2].unpack1('v'),
193
+ data[offset + 10, 2].unpack1('s<')
194
+ )
195
+ end
196
+ end
197
+
198
+ def load_subsectors(data)
199
+ count = data.size / 4
200
+ count.times do |i|
201
+ offset = i * 4
202
+ @subsectors << Subsector.new(
203
+ data[offset, 2].unpack1('v'),
204
+ data[offset + 2, 2].unpack1('v')
205
+ )
206
+ end
207
+ end
208
+
209
+ def load_nodes(data)
210
+ count = data.size / 28
211
+ count.times do |i|
212
+ offset = i * 28
213
+ bbox_right = Node::BBox.new(
214
+ data[offset + 8, 2].unpack1('s<'),
215
+ data[offset + 10, 2].unpack1('s<'),
216
+ data[offset + 12, 2].unpack1('s<'),
217
+ data[offset + 14, 2].unpack1('s<')
218
+ )
219
+ bbox_left = Node::BBox.new(
220
+ data[offset + 16, 2].unpack1('s<'),
221
+ data[offset + 18, 2].unpack1('s<'),
222
+ data[offset + 20, 2].unpack1('s<'),
223
+ data[offset + 22, 2].unpack1('s<')
224
+ )
225
+ @nodes << Node.new(
226
+ data[offset, 2].unpack1('s<'),
227
+ data[offset + 2, 2].unpack1('s<'),
228
+ data[offset + 4, 2].unpack1('s<'),
229
+ data[offset + 6, 2].unpack1('s<'),
230
+ bbox_right,
231
+ bbox_left,
232
+ data[offset + 24, 2].unpack1('v'),
233
+ data[offset + 26, 2].unpack1('v')
234
+ )
235
+ end
236
+ end
237
+
238
+ def player_start
239
+ @things.find { |t| t.type == 1 }
240
+ end
241
+
242
+ # Find the sector at a given position by traversing the BSP tree
243
+ def sector_at(x, y)
244
+ subsector = subsector_at(x, y)
245
+ return nil unless subsector
246
+
247
+ # Get sector from first seg of subsector
248
+ seg = @segs[subsector.first_seg]
249
+ return nil unless seg
250
+
251
+ linedef = @linedefs[seg.linedef]
252
+ sidedef_idx = seg.direction == 0 ? linedef.sidedef_right : linedef.sidedef_left
253
+ return nil if sidedef_idx < 0
254
+
255
+ @sectors[@sidedefs[sidedef_idx].sector]
256
+ end
257
+
258
+ # Find the subsector containing a point
259
+ def subsector_at(x, y)
260
+ node_idx = @nodes.size - 1
261
+ while (node_idx & Node::SUBSECTOR_FLAG) == 0
262
+ node = @nodes[node_idx]
263
+ side = point_on_side(x, y, node)
264
+ node_idx = side == 0 ? node.child_right : node.child_left
265
+ end
266
+ @subsectors[node_idx & ~Node::SUBSECTOR_FLAG]
267
+ end
268
+
269
+ private
270
+
271
+ def point_on_side(x, y, node)
272
+ dx = x - node.x
273
+ dy = y - node.y
274
+ left = dy * node.dx
275
+ right = dx * node.dy
276
+ right >= left ? 0 : 1
277
+ end
278
+ end
279
+ end
280
+ end