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

nhartland / forma / 23412288202

22 Mar 2026 08:54PM UTC coverage: 98.169% (-0.04%) from 98.213%
23412288202

push

github

web-flow
Merge pull request #32 from nhartland/mkdocs

MKdocs rather than Ldoc for documentation generation

9 of 12 new or added lines in 4 files covered. (75.0%)

36 existing lines in 4 files now uncovered.

2091 of 2130 relevant lines covered (98.17%)

8571.78 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
---@class forma.raycasting
10
local ray = {}
1✔
11

12
local cell    = require('forma.cell')
1✔
13
local pattern = require('forma.pattern')
1✔
14

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

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

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

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

127
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