Documentation
¶
Overview ¶
Package formula defines the per-unit deploy coordinate schema produced by the `design_attack` interactive tool and consumed by the deploy pipeline in `internal/attack`.
The schema is intentionally side-based (single endpoints along a chosen SIDE of the screen — top, bottom, left, right) rather than the legacy corner-based system in `precision_config.json`. The user can pinpoint exact tap coordinates for every unit type so the bot no longer has to guess based on red zone detection or random interpolation.
Schema (1 line per field, see examples in cmd/design_attack README):
{
"name": "EDrag side formula",
"screen": {"w": 860, "h": 732},
"units": {
"balloon": {"type":"line", "p1":{"x":60,"y":110}, "p2":{"x":60,"y":542}, "count":10, "jitter":3},
"stone slammer": {"type":"point", "p":{"x":400,"y":326}, "jitter":6},
"rage spell": {"type":"lines", "lines":[
{"p1":{"x":80,"y":200},"p2":{"x":130,"y":280},"count":3,"jitter":3},
{"p1":{"x":80,"y":280},"p2":{"x":130,"y":380},"count":2,"jitter":3}
]}
}
}
Index ¶
- type Formula
- func (f *Formula) ApplyScreenScale(srcW, srcH, dstW, dstH int)deprecated
- func (f *Formula) ClampY(minY, maxY int) int
- func (f *Formula) LookUp(unitName string) (UnitEntry, bool)
- func (f *Formula) MirrorForCorner(targetEdge string)
- func (f *Formula) ProjectUniform(k float64, srcW, srcH, dstW, dstH int)
- func (f *Formula) PushOutOfZone(blocked func(x, y int) bool, cx, cy, step, maxSteps int) int
- func (f *Formula) Save(path string) error
- type LinePoint
- type Point
- type ScreenSize
- type UnitEntry
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Formula ¶
type Formula struct {
Name string `json:"name"`
Screen ScreenSize `json:"screen"`
Units map[string]UnitEntry `json:"units"`
CornerOverrides map[string]map[string]UnitEntry `json:"corner_overrides,omitempty"`
}
func AutoPickFor ¶
func AutoPickFor(s *strategy.DynamicStrategy, screenW, screenH int, targetEdge string) *Formula
AutoPickFor builds a *Formula for every unit in the strategy using per-class pinned geometry. No manual clicks required - the side is chosen from targetEdge (Random falls back to Left for determinism).
Per-class rules (4-side aware):
- Hero (Point pattern, phase name contains "hero") → midpoint of the chosen side's deploy line, jitter 5.
- Siege (Point pattern, phase name contains "siege") → dead center of map, jitter 6.
- Line troop (Line pattern, "balloon"/"dragon"/etc) → full edge line endpoints, count=10, jitter 3.
- Rage spell (Line pattern, name contains "rage") → 2 sub-lines: first 15%-60% (3 taps), second 40%-85% (2 taps).
- Other spell (Line pattern, name contains "spell") → single edge line pulled slightly inward, count=5, jitter 3.
- FourSides → point cluster at map center, jitter 5.
The 20-80 percent rule guarantees auto-pick geometry mathematically never lands in a screen corner (the regression that motivated this helper).
func Load ¶
Load finds and reads a formula file. Strategy path is the YAML file the bot is loading — we look for `<stem>_formula.json` next to it, also probing `assets/strategies/<basename>` via paths.Resolve.
func LoadFile ¶
LoadFile reads a formula from a direct path. Use Load() instead when the path is a strategy YAML (Load auto-resolves <stem>_formula.json). Used by cmd/design_attack -verify to load a previously-saved formula for visual inspection of the 4-corner mirror.
func (*Formula) ApplyScreenScale
deprecated
ApplyScreenScale multiplies every coordinate in the formula by (dstW/srcW, dstH/srcH) so a JSON calibrated on one screen size still lands correctly on a different live resolution. Does NOT scale Count, Jitter, or Type. No-op when src dims are zero.
Deprecated: this is the legacy "stretch to fill the framebuffer" model, and it is wrong for the game these formulas are authored against. CoC renders at a single uniform display scale about the viewport centre (docs/RESOLUTION.md item 1, verified live for HUD chrome at k=1.325 with 720p icon positions matching mapped predictions within 3 px). Between 860x732 and 1280x720 this function's factors are 1.4884 horizontally and 0.9836 vertically — a 34% anisotropy that moves an authored deploy point by 19-76 px, one to two village tiles, onto the wrong building. Use ProjectUniform with the calibration's display scale for anything that lands on the game world; this entry point survives only for the corner-mirror path in cmd/design_attack, which scales between two buffers of the same device.
func (*Formula) ClampY ¶
ClampY pulls every coordinate's Y into [minY, maxY] and reports how many points moved. It exists because a formula authored on the reference geometry can project below the live deployable band even when the projection itself is correct: at 1280x720 the village view is shorter in reference terms, so an authored y of 580 lands at 645 on a 720px screen — under the troop bar at 624. Tapping there hits the HUD and the unit never deploys, which is the "it didn't use all the troops" failure; clamping keeps the unit in play at a slightly different spot instead. The count is returned so the caller can say so out loud, because the durable fix is re-authoring the point.
A degenerate band (maxY <= minY) is ignored rather than collapsing every point onto one row.
func (*Formula) LookUp ¶
LookUp returns the entry for a unit name, normalizing underscores to spaces (Strategy stores "stone_slammer"; formula stores "stone slammer").
func (*Formula) MirrorForCorner ¶
MirrorForCorner reflects every P / P1 / P2 / Lines[i].P1 / Lines[i].P2 across the screen axes as needed so a formula authored for ONE corner (BottomRight by convention) applies to any of the 4 corners. Used by the orchestrator with `target_edge: "Rotate"` so the same authored attack lands on a different side each run without the user having to pin all 4 corners in `precision_config.json`.
Reflection rules (mirror around the formula's authored screen center):
BottomRight: identity (formula as-authored) — no-op BottomLeft: reflect X (newX = W - X) — flip horizontally TopRight: reflect Y (newY = H - Y) — flip vertically TopLeft: reflect both (newX = W - X; newY = H - Y)
Accepts BOTH the full canonical names ("BottomLeft", "TopRight", etc. — used by the orchestrator via NextEdgeIndex) AND the abbreviated 2-letter forms ("BL", "TR", etc. — used by cmd/design_attack -verify for its 2x2 grid labels). Freeform values like "left" or "right" also work via the substring fallback so a user-authored strategy with `target_edge: "left"` still gets a reasonable mirror.
Designed to be called BEFORE ApplyScreenScale so the mirror uses the formula's authored 860x732 reference frame. After the mirror, ApplyScreenScale does the live-screen projection. The Grand Warden "always at screen center" hardcode in HeroManager is NOT mirrored — center stays center — so a pin authored at the formula's (430, 366) center mirrors to itself, which is the right behavior either way.
func (*Formula) ProjectUniform ¶
ProjectUniform projects every coordinate from a formula authored on the src frame onto a live dst frame using a single display scale k about the two frames' centres: dst_centre + (p - src_centre) * k.
This is the model the game actually renders with, which is why it takes k as an argument instead of deriving a ratio from the frame sizes: the display scale is a property of the device (device.display_scale, measured with cmd/resprobe), not of how many pixels the framebuffer happens to have. The same uniform scale applies on both axes, so a shape keeps its aspect, which is exactly what ApplyScreenScale's per-axis factors break.
Coordinates are NOT clamped: a point that projects off-screen is a deploy point the bot cannot tap, and the caller is expected to report that rather than silently pin it to the frame edge.
No-op when the formula is already in live coordinates (src == dst) or when any dimension or k is non-positive. The src == dst guard matters: formulas written by cmd/design_attack carry the live screen size, and re-scaling them by the device's k would multiply them for a second time.
func (*Formula) PushOutOfZone ¶
PushOutOfZone walks every coordinate radially away from (cx, cy) while `blocked` reports it sits in an area that cannot take a tap, and returns how many coordinates moved.
It exists for the red deployment line. The band clamp above answers "is this point under the HUD", which is a frame-edge question; the red line is a shape in the middle of the frame — the game refuses a tap inside it with "You cannot deploy troops on the Red area!" and the unit stays in the bar. A user-pinned formula bypasses the red-zone-aware dynamic deploy line entirely, so a pinned line drawn inside the red boundary deploys nothing, and nothing in the log says why: the taps succeed, the game ignores them.
Direction is radial from the zone's centre because that is the direction of the deployable strip on every edge, and stepping by whole tiles keeps the moved point on the geometry the user was aiming at rather than nudging it a few pixels inside the same rejected area. A point that cannot escape within maxSteps is left alone and counted as unmoved — silently relocating it across the village would be a worse lie than the refused tap.
type LinePoint ¶
type LinePoint struct {
P1 Point `json:"p1"`
P2 Point `json:"p2"`
Count int `json:"count"`
Jitter int `json:"jitter"`
}
LinePoint is one segment of a multi-line deploy (e.g. rage spell 3+2 split). Count = number of taps distributed along P1->P2 for this segment.
type ScreenSize ¶
Formula is the top-level deploy plan.
`Units` is the per-unit deploy geometry authored for one canonical corner (BottomRight by convention). The orchestrator mirrors Units to the active target edge at runtime (see MirrorForCorner).
`CornerOverrides` is an optional map of per-corner partial overrides. When present, the orchestrator uses the override for that corner INSTEAD OF mirroring Units — because the user has authored explicit coordinates for that side (e.g. with `cmd/design_attack -corner BL`) that better match the base's actual red-line position on that side.
Keys are the canonical corner names produced by NextEdgeIndex: "TopLeft" / "TopRight" / "BottomRight" / "BottomLeft" (case sensitive — the orchestrator's switch uses the raw value). The per-corner value is a per-unit map with the SAME schema as Units; typically a PARTIAL formula (only the units that differ from the mirrored BR default). The orchestrator merges the override with Units per-unit, with the override winning.
`corner_overrides` is omitted from JSON when nil, so old formulas (with only Units) continue to round-trip cleanly. ScreenSize is the reference frame the formula was authored on (typically 860x732). MirrorForCorner reflects around its center and ApplyScreenScale projects onto the live screen size.
type UnitEntry ¶
type UnitEntry struct {
Type string `json:"type,omitempty"`
// point
P *Point `json:"p,omitempty"`
Jitter int `json:"jitter,omitempty"`
// line
P1 *Point `json:"p1,omitempty"`
P2 *Point `json:"p2,omitempty"`
Count int `json:"count,omitempty"`
// lines (rage-style)
Lines []LinePoint `json:"lines,omitempty"`
}
UnitEntry is the per-unit deploy instruction.
Type discriminator (inferred from populated fields):
"point" - single tap target (heroes, siege) "line" - taps evenly distributed from P1 to P2 (balloons, EDrag) "lines" - rage-style split (each LinePoint is one sub-line) empty - no entry; caller falls back to pCfg.Edges