• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nhartland / forma / 22257118818

21 Feb 2026 12:50PM UTC coverage: 97.413% (-1.0%) from 98.384%
22257118818

Pull #29

github

web-flow
Merge 8e7e72303 into 319687f37
Pull Request #29: Dev PR

471 of 487 new or added lines in 15 files covered. (96.71%)

28 existing lines in 4 files now uncovered.

2109 of 2165 relevant lines covered (97.41%)

7567.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.39
/forma/raycasting.lua
1
--- Ray tracing algorithms
2
-- Algorithms for identifying visible segments of a pattern from a single cell.
3
-- This can be used for 'field of view' applications.
4
--
5
-- Sources:
6
--
7
-- `http://www.adammil.net/blog/v125_Roguelike_Vision_Algorithms.html`
8
-- `http://www.roguebasin.com/index.php?title=LOS_using_strict_definition`
9
--
10
-- @module forma.raycasting
11

12
local ray = {}
1✔
13

14
local cell    = require('forma.cell')
1✔
15
local pattern = require('forma.pattern')
1✔
16

17
-- Internal ray cast
18
local function cast_xy(x0, y0, x1, y1, domain)
19
    -- Start or end cell was already blocked
20
    if not domain:has_cell(x0, y0) or not domain:has_cell(x1, y1) then
9,700✔
21
        return false
406✔
22
    end
23
    -- Initialise line walk
24
    local dx, dy = x1 - x0, y1 - y0
9,294✔
25
    local sx = (x0 < x1) and 1 or -1
9,294✔
26
    local sy = (y0 < y1) and 1 or -1
9,294✔
27
    -- Rasterise step by step
28
    local nx, ny = x0, y0
9,294✔
29
    local denom = math.sqrt(dx * dx + dy * dy)
9,294✔
30
    while nx ~= x1 or ny ~= y1 do
41,557✔
31
        -- Ray is blocked
32
        if not domain:has_cell(nx, ny) then
32,263✔
UNCOV
33
            return false
×
34
            -- Ray is not blocked, calculate next step
35
        elseif math.abs(dy * (nx - x0 + sx) - dx * (ny - y0)) / denom < 0.5 then
32,263✔
36
            nx = nx + sx
11,830✔
37
        elseif math.abs(dy * (nx - x0) - dx * (ny - y0 + sy)) / denom < 0.5 then
20,433✔
38
            ny = ny + sy
12,307✔
39
        else
40
            nx = nx + sx
8,126✔
41
            ny = ny + sy
8,126✔
42
        end
43
    end
44
    -- Successfully traced a ray
45
    return true
9,294✔
46
end
47

48
--- Casts a ray from a start to an end cell.
49
-- Returns {true/false} if the cast is successful/blocked.
50
-- Adapted from:
51
--
52
-- `http://www.roguebasin.com/index.php?title=LOS_using_strict_definition`
53
-- @param v0 starting cell of ray
54
-- @param v1 end cell of ray
55
-- @param domain the domain in which we are casting
56
-- @return true or false depending on whether the ray was successfully cast
57
function ray.cast(v0, v1, domain)
1✔
58
    assert(getmetatable(v0) == cell, "ray.cast requires a cell as the first argument")
100✔
59
    assert(getmetatable(v1) == cell, "ray.cast requires a cell as the second argument")
100✔
60
    assert(getmetatable(domain) == pattern, "ray.cast requires a pattern as the third argument")
100✔
61
    return cast_xy(v0.x, v0.y, v1.x, v1.y, domain)
100✔
62
end
63

64
--- Casts rays from a start cell across an octant.
65
-- @param v0 starting cell of ray
66
-- @param domain the domain in which we are casting
67
-- @param oct the octant identifier (integer between 1 and 8)
68
-- @param ray_length the maximum length of the ray
69
-- @return the pattern illuminated by the ray casting
70
function ray.cast_octant(v0, domain, oct, ray_length)
1✔
71
    assert(getmetatable(v0) == cell, "ray.cast_octant requires a cell as the first argument")
800✔
72
    assert(getmetatable(domain) == pattern, "ray.cast_octant requires a pattern as the second argument")
800✔
73
    assert(type(oct) == 'number', "ray.cast_octant requires a number as the third argument")
800✔
74
    assert(type(ray_length) == 'number', "ray.cast_octant requires a number as the fourth argument")
800✔
75
    local function transformOctant(r, c)
76
        if oct == 1 then return r, -c end
16,000✔
77
        if oct == 2 then return r, c end
14,000✔
78
        if oct == 3 then return c, r end
12,000✔
79
        if oct == 4 then return -c, r end
10,000✔
80
        if oct == 5 then return -r, c end
8,000✔
81
        if oct == 6 then return -r, -c end
6,000✔
82
        if oct == 7 then return -c, -r end
4,000✔
83
        if oct == 8 then return c, -r end
2,000✔
84
    end
85
    local x0, y0 = v0.x, v0.y
800✔
86
    local ray_length_sq = ray_length * ray_length
800✔
87
    local lit_pattern = pattern.new()
800✔
88
    for row = 1, ray_length do
4,800✔
89
        for col = 0, row do
20,000✔
90
            local tcol, trow = transformOctant(row, col)
16,000✔
91
            local x1, y1 = x0 + tcol, y0 - trow
16,000✔
92
            local dx, dy = x1 - x0, y1 - y0
16,000✔
93
            if dx * dx + dy * dy < ray_length_sq then
16,000✔
94
                if cast_xy(x0, y0, x1, y1, domain) then
9,600✔
95
                    lit_pattern:insert(x1, y1)
9,194✔
96
                end
97
            end
98
        end
99
    end
100
    return lit_pattern
800✔
101
end
102

103
--- Casts rays from a starting cell in all directions
104
-- @param v0 starting cell of ray
105
-- @param domain the domain in which we are casting
106
-- @param ray_length the maximum length of the ray
107
-- @return the pattern illuminated by the ray casting
108
function ray.cast_360(v0, domain, ray_length)
1✔
109
    assert(getmetatable(v0) == cell, "ray.cast_360 requires a cell as the first argument")
100✔
110
    assert(getmetatable(domain) == pattern, "ray.cast_360 requires a pattern as the second argument")
100✔
111
    assert(type(ray_length) == 'number', "ray.cast_360 requires a number as the third argument")
100✔
112
    local lit_pattern = pattern.new():insert(v0.x, v0.y)
100✔
113
    for ioct = 1, 8, 1 do
900✔
114
        local np = ray.cast_octant(v0, domain, ioct, ray_length)
800✔
115
        for x, y in np:cell_coordinates() do
9,994✔
116
            if not lit_pattern:has_cell(x, y) then
9,194✔
117
                lit_pattern:insert(x, y)
6,494✔
118
            end
119
        end
120
    end
121
    return lit_pattern
100✔
122
end
123

124
return ray
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc