SCRMaps
/
Sign in

One data block instead of six arrays

If adding one hero means editing six arrays together, an index bug is a matter of time. Write the data once and let the compiler make the arrays.

8 min read

Most maps grow into the same shape: a HeroName array, a HeroUnit array, a HeroSlot array, a HeroInitRank array and two more for rank increments. Adding one hero means editing six places together, and nothing checks that the six stayed in step.

epScript can run Python at compile time, so the data can be written once as a list of dictionaries and the arrays generated from it.

const HERO_DATA = py_eval('[ \
    {"name": "손책", "unit": 48, "slot": 0, "ranks": [1321, 1322, 1323]}, \
    {"name": "황개", "unit": 23, "slot": 1, "ranks": [1304, 1307, 1310]}, \
]');

const HERO_COUNT    = py_len(HERO_DATA);
const HeroNames     = EUDArray(py_eval('[EPD(Db(x["name"])) for x in HERO_DATA]'));
const HERO_UNITS    = py_list(py_map(py_eval('lambda x: x["unit"]'), HERO_DATA));
const HeroUnitTypes = EUDArray(HERO_UNITS);
const HeroSlots     = EUDArray(py_eval('[x["slot"] for x in HERO_DATA]'));
Adding a hero is one line, and nothing can fall out of step because there is only one list.

And the part most worth taking even if you keep your arrays as they are comes next: a rule your data has to satisfy can be made into a compile error.

py_assert(HERO_COUNT == py_len(py_set(HERO_UNITS)), "영웅 유닛 중복");
py_assert(py_eval('all(len(x["ranks"]) == 1+MAX_RANK for x in HERO_DATA)'), "계급 개수 불일치");
py_assert(py_eval('all(0 <= x["slot"] < SLOT_COUNT for x in HERO_DATA)'), "슬롯 범위 초과");
Two heroes sharing a unit id, a row with only two ranks, a slot number past the end of the slot table - all three are silent in game, and all three fail the build here.

It is the counterpart of ep_assert, which proves a property of the code - “this construct emits no triggers”. py_assert checks that your data makes sense.

Packing several small values into one cell is also a compile-time job, and it removes two of the three arrays.

const HeroRankData = EUDArray(py_eval(
    '[(x["ranks"][0] - 1302) + ((x["ranks"][1] - 1302) << 8)'
    ' + ((x["ranks"][2] - 1302) << 16) for x in HERO_DATA]'));

const rank = bread_epd(HeroRankData + heroIndex, currentRank);   // currentRank번째 바이트
The shifting happens in Python, so it costs nothing in game. bread_epd takes a variable as its second argument, so choosing which of the three you want is one read - where the three-array version needed a branch per case.

When a whole data table has to be swapped - a game mode, say - give the variable an array type and repoint it.

var HeroRanksData: EUDArray = HeroRankDataMod0;
// ...
HeroRanksData = HeroRankDataMod1;                 // 표 전체를 교체 / swap the whole table
for (var i = 0; i < HERO_COUNT; i += 1) updateHeroRank(i);
The mode table is generated at compile time from a Python dict of only the rows that differ, merged over the base. What it replaced was a pair of hundred-line functions assigning array cells one at a time.

For an array of units, type the EUDVArray as CUnit. What comes out is a CUnit, the offset arithmetic disappears, and the death check reads like the sentence it is.

const HeroCUnit = EUDVArray(HERO_COUNT, CUnit)();

foreach(hero : HeroCUnit) {
    if (hero == 0) continue;
    if (hero.hp <= 0) hero.order = "Die";   // 먼저 / first
    if (hero.order == "Die") { /* ... */ } // 그 다음 / then
}
The order matters: set order from hp first and then test order, and one pass handles both “killed by damage” and “already dying”.
Types and scopeReading what a unit is doing
Sign in to track your progress