route1_candidates.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
-- Driver: propose and photograph several battle arenas for ONE map. -- -- data/battle_arenas.lua holds a single authored spot per area, chosen by -- arena_pick's nearest-to-the-middle search and then looked at. This is the -- other half of that job: when the shipped spot is up for review, it lays out -- the ALTERNATIVES -- every arena on the map both mons can be seen in, spread -- along the map so the shortlist is places rather than neighbours -- and -- stages a real battle in each so they can be compared by eye. -- -- SHOT_DIR=.scratchpad/route1_candidates CAND_MAP=ROUTE_1 CAND_N=5 \ -- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/route1_candidates.lua love . -- -- CAND_MAP is the map id (default ROUTE_1), CAND_N how many to photograph. -- One `CAND` line per shot, ready to paste into the data file, plus a PNG -- named for its corner and shape. return function(game) local U = dofile("tests/drivers/util.lua") local DIR = os.getenv("SHOT_DIR") or ".scratchpad/route1_candidates" local MAP = os.getenv("CAND_MAP") or "ROUTE_1" local WANT = tonumber(os.getenv("CAND_N") or "") or 5 local Pokemon = require("src.pokemon.Pokemon") local BattleState = require("src.battle.BattleState") game.save.party = { Pokemon.new(game.data, "CHARIZARD", 45) } game.save.player.name = "RED" local exports = game.mods and game.mods.exports local lib = exports and exports.DRAMATIC_SHAPE and exports.DRAMATIC_SHAPE.lib if not lib then U.log("DRAMATIC_SHAPE is not loaded -- enable it and run again") return end local Arena = lib.require("BattleArena") local Battles = lib.require("OverworldBattle") U.teleport(game, MAP, 1, 1, "down") local map = game.overworld.map U.log(("%s is %dx%d cells"):format(MAP, map.widthCells, map.heightCells)) -- ------- every arena the map can offer -- -- BattleArena.search answers "the nearest one", which is the wrong question -- for a shortlist -- it returns one spot and hides the rest. So walk the -- same grid ourselves and keep them all, tagged with whether the pair would -- actually be SEEN there (Arena.clearance), because an obstructed spot is -- not a candidate no matter how good the ground looks. -- -- The map's outermost cells are its CONNECTION BORDER -- the strip the -- neighbouring map is drawn into, walkable so the player can step across. -- Ground there passes every test and is still the wrong answer: a fight -- staged on it happens at the edge of the world with the border ring's tree -- wall at the mons' backs, and every spot in the strip looks like every -- other one. CAND_MARGIN keeps the shortlist on the route proper. local MARGIN = tonumber(os.getenv("CAND_MARGIN") or "") or 2 local cands = {} for _, shape in ipairs(Arena.SHAPES) do for y = MARGIN, map.heightCells - shape.h - MARGIN do for x = MARGIN, map.widthCells - shape.w - MARGIN do local fits = true for cy = y, y + shape.h - 1 do for cx = x, x + shape.w - 1 do if not Arena.openCell(map, cx, cy, false) then fits = false break end end if not fits then break end end if fits then local a = Arena.at(x, y, shape.id) if a and Arena.clearance(map, a) then cands[#cands + 1] = { x = x, y = y, shape = shape.id, mx = x + (shape.w - 1) / 2, my = y + (shape.h - 1) / 2 } end end end end end U.log(("%d clear arenas on %s"):format(#cands, MAP)) if #cands == 0 then U.log("done -- nothing to propose") return end for _, c in ipairs(cands) do U.log((" fit %d,%d %s"):format(c.x, c.y, c.shape)) end -- CAND_AT=x,y,shape;x,y,shape;... photographs an explicit shortlist instead -- of the spread one. The spread is a first pass over ground the clearance -- test approved, and that test measures terrain height along the sightline -- only -- it has no opinion on a hedge sitting in the apron row between the -- camera and the near mon, which is the failure that keeps turning up. So -- the loop is: spread, look, then re-shoot the survivors and the -- replacements by hand. local explicit = os.getenv("CAND_AT") if explicit and explicit ~= "" then local list = {} for spot in explicit:gmatch("[^;]+") do local x, y, s = spot:match("^%s*(%-?%d+)%s*,%s*(%-?%d+)%s*,%s*(%a+)%s*$") if x then list[#list + 1] = { x = tonumber(x), y = tonumber(y), shape = s } else U.log("BAD CAND_AT entry: " .. spot) end end cands = list WANT = #list U.log(("%d spots given explicitly"):format(#list)) end local function shapeOf(id) for _, s in ipairs(Arena.SHAPES) do if s.id == id then return s end end end for _, c in ipairs(cands) do local s = shapeOf(c.shape) c.mx = c.x + ((s and s.w or 1) - 1) / 2 c.my = c.y + ((s and s.h or 1) - 1) / 2 end -- ------- thin them down to a shortlist that is actually a CHOICE -- -- Adjacent corners are the same patch of ground shifted a cell, so a naive -- top-N is five photographs of one place, and pure farthest-point selection -- goes straight to the extremes -- which on a route means the ends, where -- the ground is emptiest and the shots are least distinguishable. -- -- So: spread along the route's LONG AXIS, one pick per band, and within a -- band take the spot nearest the middle of the road. The road is measured -- rather than assumed -- the median cross-axis position of everywhere a -- fight fits IS the lane, on a map whose walkable ground is mostly lane. local horizontal = map.widthCells > map.heightCells local function along(c) return horizontal and c.mx or c.my end local function across(c) return horizontal and c.my or c.mx end local xs = {} for _, c in ipairs(cands) do xs[#xs + 1] = across(c) end table.sort(xs) local road = xs[math.ceil(#xs / 2)] U.log(("road runs %s, centre of the lane is %s = %.1f") :format(horizontal and "east-west" or "north-south", horizontal and "y" or "x", road)) local lo, hi = math.huge, -math.huge for _, c in ipairs(cands) do lo, hi = math.min(lo, along(c)), math.max(hi, along(c)) end -- an explicit shortlist is already the answer; the spread below would only -- thin it, and its overlap rule would silently drop two spots deliberately -- asked for a cell apart local picked, taken = {}, {} for band = 1, (explicit and explicit ~= "") and 0 or WANT do -- band centres, not band edges: the first and last picks sit inside the -- route rather than on its two connection mouths local target = lo + (hi - lo) * (band - 0.5) / WANT local best, bestScore for _, c in ipairs(cands) do if not taken[c] then local da = along(c) - target local dr = across(c) - road -- distance from the band centre, plus a heavier penalty for being off -- the lane; wide arenas are worth a detour, being the shot this mode -- is framed for local score = da * da + 4 * dr * dr - (c.shape == "wide" and 100 or 0) if not bestScore or score < bestScore then best, bestScore = c, score end end end if best then picked[#picked + 1] = best -- everything overlapping the pick is off the table, so two bands whose -- best spots touch cannot return the same patch of ground twice for _, c in ipairs(cands) do if math.abs(along(c) - along(best)) < 3 and math.abs(across(c) - across(best)) < 3 then taken[c] = true end end end end if #picked == 0 then picked = cands end -- north to south (or west to east), so the filenames read along the route table.sort(picked, function(a, b) if along(a) ~= along(b) then return along(a) < along(b) end return across(a) < across(b) end) for i, c in ipairs(picked) do U.log(("CAND %d [%q] = { x = %d, y = %d, shape = %q },") :format(i, MAP, c.x, c.y, c.shape)) -- forced through the authored-entry seam, so what gets staged is exactly -- this spot rather than whatever the search would pick from the player's -- cell Arena.setOverride(MAP, { x = c.x, y = c.y, shape = c.shape }) local staged = Arena.find(map, 0, 0, false) if not staged then U.log(("SKIP %d -- override did not stage"):format(i)) else game.overworld.player.cellX = staged.playerCell[1] game.overworld.player.cellY = staged.playerCell[2] U.wait(90) -- let the meshes land local battle = BattleState.newWild(game, "NIDORINO", 20) battle.onFinish = function() end game.overworld:pushBattle(battle) U.wait(70) for _ = 1, 14 do U.tap(game, "a"); U.wait(8) end local got = Battles.arena() U.log(("SHOT %d staged at %s,%s"):format(i, tostring(got and got.x), tostring(got and got.y))) U.shot(game, ("%s/%d_%s_x%d_y%d_%s.png") :format(DIR, i, MAP:lower(), c.x, c.y, c.shape)) while game.stack:top() and game.stack:top() ~= game.overworld do game.stack:pop() end U.wait(6) end end Arena.setOverride(MAP, nil) U.log("done -- " .. DIR) end