packages: the registry text a game fills is keys (Maroon Lake's phase 26.4) - ThingKind.name, GearKind blurb/howto/names, HintCard's words, JobDef title/text/how, NpcLine and NpcChoice text, WeatherKind.name are @Text Key fields (a compiled row that leaves one out takes its derived key, the English is the game's en.po); what a package hands back as text is the key's marked text, "" for none; NpcName.name, NpcChoice.line and SettingDef.text are not text and lose @Text; ludic.i18n's tr/trf/trn/key_text say @alloc_ok (tr is a cast); the packages' tests with key literals (built, not run); changes/text-keys-data.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-30 00:29:03 +03:00
parent 86146782f7
commit 86644e6533
21 changed files with 81 additions and 54 deletions

13
changes/text-keys-data.md Normal file
View file

@ -0,0 +1,13 @@
bump: minor
type: change
**The packages' registry text is keys (phase 26.4).** The fields a player reads in the open registries
a game fills are `@Text ... : Key` now, so a compiled row that leaves one out takes its derived key
(`<registry>.<row>.<field>`) and the English lives in the game's `en.po`: ludic.things' `ThingKind.name`,
ludic.gear's `GearKind.blurb` / `howto` / `names` (`[]Key`), ludic.hints' `HintCard` line, title, steps
and `later`, ludic.jobs' `JobDef.title` / `text` / `how`, ludic.npc's `NpcLine.text` and
`NpcChoice.text`, and ludic.weather's `WeatherKind.name`. What a package hands back as text
(`thing_name`, `gear_level_name`, `hints_step_text`, `npc_words`, `weather_name`) is the key's marked
text, "" for none, made into words where it is drawn. Not text, so no longer `@Text`: ludic.npc's
`NpcName.name` (a person's name, the same in every language), `NpcChoice.line` (a line's key) and
ludic.settings' `SettingDef.text` (a text setting's default value). ludic.i18n's `tr`, `trf`, `trn`
and `key_text` say `@alloc_ok`: `tr` is a cast, and what `trf` / `trn` make is kept by what shows it.

View file

@ -7,9 +7,9 @@ export property GearKind {
@Ref(Vendors) vendor: int = 0
icon: string = ""
tiers: int = 0
@Text @Multiline blurb: string = ""
@Text @Multiline howto: string = ""
names: []string = null
@Text @Multiline blurb: Key = null
@Text @Multiline howto: Key = null
@Text names: []Key = null
cost: []int = null
rep: []int = null
@Ref(Items) item: int = -1

View file

@ -8,9 +8,9 @@ program GearTest {
const IT_TORCH: int = 5
const IT_LAMP: int = 6
def GearKinds rod { tiers: 3, names: ["Rod", "Reel", "Guide's rod"], cost: [30, 100, 300], rep: [0, 10, 40], item: IT_ROD }
def GearKinds camera { tiers: 2, names: ["Camera", "Zoom"], cost: [0, 150], rep: [0, 20], start: 1 }
def GearKinds pack { tiers: 3, names: ["Pack", "Frame", "Hauler"], cost: [20, 60, 120], rep: [0, 0, 0], mult: [1.0, 1.3, 1.7, 2.2] }
def GearKinds rod { tiers: 3, names: [k"gear.rod", k"gear.reel", k"gear.guide_rod"], cost: [30, 100, 300], rep: [0, 10, 40], item: IT_ROD }
def GearKinds camera { tiers: 2, names: [k"gear.camera", k"gear.zoom"], cost: [0, 150], rep: [0, 20], start: 1 }
def GearKinds pack { tiers: 3, names: [k"gear.pack", k"gear.frame", k"gear.hauler"], cost: [20, 60, 120], rep: [0, 0, 0], mult: [1.0, 1.3, 1.7, 2.2] }
def GearValues bite { gear: GEAR_ROD, at: [1.0, 1.0, 0.75, 0.55] }
def GearValues drain { gear: GEAR_CAMERA, at: [14.0, 14.0, 8.0] }
def GearCharges torch { item: IT_TORCH, full: 60.0, per_hour: 60.0 }
@ -62,7 +62,7 @@ program GearTest {
fresh(gear_st, gear_test_st)
expect_eq(gear_level(gear_st, GEAR_ROD), 0)
expect_eq(gear_level(gear_st, GEAR_CAMERA), 1)
expect(gear_next_name(gear_st, GEAR_CAMERA) == "Zoom")
expect(gear_next_name(gear_st, GEAR_CAMERA) == string(k"gear.zoom"))
expect_eq(gear_next_cost(gear_st, GEAR_ROD), 30)
}

View file

@ -17,7 +17,8 @@ export function gear_best(gear_st: GearState, k: int) -> bool { return gear_leve
# a tier's name, "" for none
export function gear_level_name(k: int, lv: int) -> string {
if lv <= 0 or lv > GearKinds[k].tiers { return "" }
return GearKinds[k].names[lv - 1]
@alloc_ok("a cast: a key's marked text, nothing is made")
return string(GearKinds[k].names[lv - 1])
}
# the tier on offer next: its name, cost and the standing it wants

View file

@ -44,9 +44,12 @@ export function hints_mute_card(hints_st: mut HintsState) -> void {
# a step's words and its picture (n 1..3): the glyph's name, or the item (-1 when it is a glyph)
export function hints_step_text(id: int, n: int) -> string {
if n == 1 { return Hints[id].s1 }
if n == 2 { return Hints[id].s2 }
return Hints[id].s3
var k = Hints[id].s3
if n == 1 { k = Hints[id].s1 }
if n == 2 { k = Hints[id].s2 }
if k == null { return "" }
@alloc_ok("a cast: a key's marked text, nothing is made")
return string(k)
}
export function hints_step_icon(id: int, n: int) -> string {

View file

@ -11,18 +11,18 @@ export property HintCard {
@OneOf(HINTS_TIP, HINTS_NOTE, HINTS_WARN, HINTS_URGENT) urgency: int = HINTS_TIP
icon: string = ""
@Ref(Items) item: int = -1
@Text line: string = "" # the chip on the rail: five or six words
@Text title: string = "" # the card's heading
@Text s1: string = "" # three steps, each with a picture
@Text s2: string = ""
@Text s3: string = ""
@Text line: Key = null # the chip on the rail: five or six words
@Text title: Key = null # the card's heading
@Text s1: Key = null # three steps, each with a picture
@Text s2: Key = null
@Text s3: Key = null
pic1: string = ""
pic2: string = ""
pic3: string = ""
@Ref(Items) item1: int = -1
@Ref(Items) item2: int = -1
@Ref(Items) item3: int = -1
@Text @Multiline later: string = "" # how not to be here next time
@Text @Multiline later: Key = null # how not to be here next time
}
# the game's cards; a card's mute is kept by its key, so their order is free

View file

@ -1,3 +1,3 @@
# cards.lres - two cards from a file, after the test's own defs
bear { urgency: HINTS_URGENT, line: "Bear near", title: "A bear", icon: "bear" }
fire { urgency: HINTS_NOTE, line: "Light the fire", later: "Carry three logs." }
bear { urgency: HINTS_URGENT, line: k"hints.bear_near", title: k"hints.a_bear", icon: "bear" }
fire { urgency: HINTS_NOTE, line: k"hints.light_fire", later: k"hints.carry_logs" }

View file

@ -4,11 +4,11 @@ import "ludic.hints"
import "ludic.base"
program HintsTest {
numbers float
def Hints thirsty { urgency: HINTS_NOTE, line: "Thirsty - drink", title: "Water", s1: "Drink at a shore", pic1: "water", item2: 7 }
def Hints parched { urgency: HINTS_URGENT, line: "You must drink now", title: "Water, now" }
def Hints rain { urgency: HINTS_TIP, line: "Rain coming" }
def Hints cold { urgency: HINTS_WARN, line: "Cold" }
def Hints night { urgency: HINTS_TIP, line: "Night soon" }
def Hints thirsty { urgency: HINTS_NOTE, line: k"hints.thirsty", title: k"hints.water", s1: k"hints.drink_shore", pic1: "water", item2: 7 }
def Hints parched { urgency: HINTS_URGENT, line: k"hints.drink_now", title: k"hints.water_now" }
def Hints rain { urgency: HINTS_TIP, line: k"hints.rain" }
def Hints cold { urgency: HINTS_WARN, line: k"hints.cold" }
def Hints night { urgency: HINTS_TIP, line: k"hints.night" }
def Hints from "cards.lres"
state HintsTestState {
@ -40,10 +40,10 @@ program HintsTest {
expect_eq(HINT_COUNT, 7)
expect_eq(hints_find("parched"), HINT_PARCHED)
expect_eq(Hints[HINT_BEAR].urgency, HINTS_URGENT)
expect(Hints[HINT_FIRE].later == "Carry three logs.")
expect(Hints[HINT_FIRE].later == k"hints.carry_logs")
expect(hints_step_icon(HINT_THIRSTY, 1) == "water")
expect_eq(hints_step_item(HINT_THIRSTY, 2), 7)
expect(hints_step_text(HINT_THIRSTY, 1) == "Drink at a shore")
expect(hints_step_text(HINT_THIRSTY, 1) == string(k"hints.drink_shore"))
}
test "the most urgent true cards, capped, ties in registry order" (hints_st: mut HintsState, hints_test_st: mut HintsTestState) {

View file

@ -15,6 +15,7 @@ export function i18n_key(name: string) -> string { return kx_join(I18N_KEY, name
export function i18n_key_plural(name: string) -> string { return kx_join(I18N_PLURAL, name, "", "", "", "", 0) }
# the key's name without its marker ("pause.resume"), or the string as it is
@alloc_ok("a key's name: a slice of text already made, kept by whoever holds the key")
export function key_text(k: string) -> string {
if not i18n_is_key(k) { return k }
var e = 1
@ -23,13 +24,16 @@ export function key_text(k: string) -> string {
}
# the text of a key: drawn translated where it is drawn (its marked text, a free cast)
@alloc_ok("a cast: the key's own marked text, nothing is made")
export function tr(k: Key) -> string { return string(k) }
# a key's text with its holes, {1} .. {4}, filled by strings already made (a number, a name, another key)
@alloc_ok("a line with its holes, made when something happens and kept by what shows it")
export function trf(k: Key, a: string, b: string, c: string, d: string) -> string {
let s = string(k)
return kx_join(I18N_KEY, key_text(s), a, b, c, d, kx_count(a, b, c, d))
}
# a plural key's text by n: the count is {1}, a .. c are {2} .. {4}
@alloc_ok("a count's line, made when something happens and kept by what shows it")
export function trn(k: Key, n: int, a: string, b: string, c: string) -> string { return kx_join(I18N_PLURAL, key_text(string(k)), `{n}`, a, b, c, 1 + kx_count(a, b, c, "")) }
function kx_count(a: string, b: string, c: string, d: string) -> int {

View file

@ -23,9 +23,9 @@ export property JobDef {
minrep: int = 0 # standing in its scope before it is offered
minstage: int = 0 # how far the game's story must be
@OneOf(JOBS_SELF, JOBS_PARTY, JOBS_OFFERED, JOBS_ACTIVE, JOBS_DONE, JOBS_READY, JOBS_GROUPS) scope: int = JOBS_SELF
@Text title: string = ""
@Text @Multiline text: string = ""
@Text @Multiline how: string = ""
@Text title: Key = null
@Text @Multiline text: Key = null
@Text @Multiline how: Key = null
icon: string = ""
}

View file

@ -128,7 +128,7 @@ def NpcActs from "acts.lres"
# a name somebody can have; `body` the game's (a pool per model), so a Nora is drawn as a woman
export property NpcName {
key: string = ""
@Text name: string = ""
name: string = "" # a person's name, the same in every language
body: int = 0
}

View file

@ -2,8 +2,8 @@
# anything to say winning, each line in its turn given its `chance`; a line's words are its `text`,
# or the story's (NpcTalk.say) when it `fill`s; a line may offer choices, each answered by a line
export property NpcChoice {
@Text text: string = ""
@Text line: string = "" # the key of the line that answers it ("" ends the talk)
@Text text: Key = null # its words (the derived key npc_lines.<row>.choices.<i>.text)
line: string = "" # the key of the line that answers it ("" ends the talk)
code: int = -1 # the game's, carried by the Talked fact
}
@ -13,7 +13,7 @@ export property NpcLine {
@Ref(NpcActs) act: int = -1 # NPCA_* (-1: whatever they are doing)
prio: int = 0
chance: int = 100 # percent: how often this line is even considered
@Text text: string = ""
@Text text: Key = null # its words, a text key (null when it `fill`s)
fill: bool = false
choices: []NpcChoice = null
}
@ -30,7 +30,9 @@ function np_line_fits(p: NpcPerson, l: NpcLine) -> bool {
export function npc_words(p: NpcPerson, line: int) -> string {
if line < 0 or line >= NPCL_COUNT { return "" }
if NpcLines[line].fill { return NpcTalk.say(p, line) }
return NpcLines[line].text
if NpcLines[line].text == null { return "" }
@alloc_ok("a cast: a key's marked text, nothing is made")
return string(NpcLines[line].text)
}
# the line it would say now (-1: none), its words kept for npc_said()

View file

@ -1,6 +1,6 @@
# lines.lres - the test's lines: a fisher's hush, the story's news when it has any, a greeting with
# two choices, and the answer to the first
hush { act: NPCA_FISH, prio: 3, text: "Shh." }
hush { act: NPCA_FISH, prio: 3, text: k"npc.shh" }
news { prio: 2, fill: true }
hello { prio: 1, text: "Morning.", choices: [{ text: "Any fish?", line: "fish_no", code: 7 }, { text: "Bye.", line: "" }] }
fish_no { prio: -1, kind: NPCK_CAMPER, text: "Not today." }
hello { prio: 1, text: k"npc.morning", choices: [{ text: k"npc.any_fish", line: "fish_no", code: 7 }, { text: k"npc.bye", line: "" }] }
fish_no { prio: -1, kind: NPCK_CAMPER, text: k"npc.not_today" }

View file

@ -203,7 +203,7 @@ program NpcTest {
let p = one(npc_st, npc_tests_fake_st, NPCK_CAMPER)
npc_do(npc_st, p, NPCA_FISH, 100.0)
expect_eq(npc_talk(npc_st, p, 0), NPCL_HUSH)
expect(npc_said(npc_st) == "Shh.")
expect(npc_said(npc_st) == string(k"npc.shh"))
expect(npc_talked_today(p))
npc_do(npc_st, p, NPCA_IDLE, 100.0)
expect_eq(npc_line_for(npc_st, p), NPCL_HELLO)
@ -212,7 +212,7 @@ program NpcTest {
expect(npc_said(npc_st) == `{p.name} saw a bear.`)
npc_test_st.news_on = false
expect_eq(npc_choose(npc_st, p, NPCL_HELLO, 0, 0), NPCL_FISH_NO)
expect(npc_said(npc_st) == "Not today.")
expect(npc_said(npc_st) == string(k"npc.not_today"))
expect_eq(npc_choose(npc_st, p, NPCL_HELLO, 1, 0), -1)
let fs = q_drain(npc_facts(npc_st))
expect_eq(len(fs), 3)

View file

@ -9,7 +9,7 @@ export property SettingDef {
key: string = "" # its name in the file; a registry fills it with the entry's own name
@OneOf(SETTING_) kind: int = 0 # SETTING_*
num: float = 0.0 # the default: 0 or 1 for a switch, a whole number, a float
@Text text: string = "" # the default of a text setting
text: string = "" # the default of a text setting (a value, not words)
lo: float = 0.0 # the range, inclusive; a value outside it is the default again
hi: float = -1.0 # hi below lo: any value
rescue: bool = false # a rescue start puts it back to its default

View file

@ -10,7 +10,9 @@ export function thing_name(t: Thing) -> string {
let s = k.name_of(t)
if s != "" { return s }
}
return k.name
if k.name == null { return "" }
@alloc_ok("a cast: a key's marked text, nothing is made")
return string(k.name)
}
export function thing_prompt(t: Thing) -> string {

View file

@ -29,7 +29,7 @@ export property Thing {
export property ThingKind {
key: string = "" # the entry's own name: what the save calls the kind
@Text name: string = "" # what it is called when nothing about the one in front of you changes it
@Text name: Key = null # what it is called when nothing about the one in front of you changes it
reach: float = 2.2 # how close it is used from, when the Thing does not say
personal: bool = false # one belongs to whoever put it down
saved: bool = false # the save's section keeps a placed one (the port has the last word)

View file

@ -32,11 +32,11 @@ program ThingsTest {
}
function far_reach(t: Thing) -> float { return 9.0 }
def ThingKinds tent { name: "A tent", name_of: fn tent_name }
def ThingKinds fire { name: "The fire", prompt: fn lit_prompt, use: fn lit_use, keeps: true }
def ThingKinds branch { name: "Firewood", reach: 1.5, use: fn branch_use }
def ThingKinds lure { name: "A lure", tick: fn lure_tick, put_down: true, saved: true }
def ThingKinds sign { name: "A sign", reach_of: fn far_reach }
def ThingKinds tent { name: k"things.tent", name_of: fn tent_name }
def ThingKinds fire { name: k"things.fire", prompt: fn lit_prompt, use: fn lit_use, keeps: true }
def ThingKinds branch { name: k"things.firewood", reach: 1.5, use: fn branch_use }
def ThingKinds lure { name: k"things.lure", tick: fn lure_tick, put_down: true, saved: true }
def ThingKinds sign { name: k"things.sign", reach_of: fn far_reach }
function hill(x: float, z: float) -> float { return x * 0.5 }
function on_placed(things_test_st: mut ThingsTestState, t: Thing) -> void { things_test_st.drawn += 1 }
@ -102,7 +102,7 @@ program ThingsTest {
test "the kind answers: name, prompt, use, reach" (things_st: mut ThingsState) {
things_clear(things_st)
let t = put(things_st, TH_TENT, 0.0, 0.0)
expect(thing_name(t) == "A tent")
expect(thing_name(t) == string(k"things.tent"))
t.owner = 0
expect(thing_name(t) == "Your tent")
let f = put(things_st, TH_FIRE, 0.0, 0.0)

View file

@ -28,7 +28,9 @@ export function weather_cold(weather_st: WeatherState) -> float {
export function weather_name(weather_st: WeatherState, k: int) -> string {
if k < 0 or k >= weather_kind_count(weather_st) { return "" }
return weather_st.wx_kinds[k].name
if weather_st.wx_kinds[k].name == null { return "" }
@alloc_ok("a cast: a key's marked text, nothing is made")
return string(weather_st.wx_kinds[k].name)
}
# the half hour after a front goes through, when the air is glass-clear

View file

@ -28,7 +28,7 @@ export const WEATHER_WANT_CLEAR: int = 3 # clear the sky and keep it clear
# one kind of weather; its place in the list the game hands in is its number
export property WeatherKind {
@Text name: string = ""
@Text name: Key = null
clear: bool = false # the kind a sky eases out to: nothing falls, amount goes to 0
wet: bool = false # rain or snow is falling (weather_wet)
@Range(0, 1) soak: float = 0.0 # how wet the ground gets under it, 0..1

View file

@ -30,7 +30,7 @@ program WeatherTest {
return w
}
function kind(name: string, wet: bool, thunder: bool, odds: []float) -> WeatherKind {
function kind(name: Key, wet: bool, thunder: bool, odds: []float) -> WeatherKind {
let k = new WeatherKind
k.name = name
k.clear = not wet
@ -65,9 +65,9 @@ program WeatherTest {
weather_test_st.bias = WEATHER_NO_BIAS
weather_test_st.lean = null
let ks = new []WeatherKind
push(ks, kind("clear", false, false, odds3(0.0, 1.0, 0.0)))
push(ks, kind("rain", true, false, odds3(0.0, 0.0, 1.0)))
push(ks, kind("storm", true, true, odds3(1.0, 0.0, 0.0)))
push(ks, kind(k"weather.clear", false, false, odds3(0.0, 1.0, 0.0)))
push(ks, kind(k"weather.rain", true, false, odds3(0.0, 0.0, 1.0)))
push(ks, kind(k"weather.storm", true, true, odds3(1.0, 0.0, 0.0)))
weather_kinds(weather_st, ks)
weather_config_wind(weather_st, 0.5, 2.5, 10.0, 19.0)
weather_reset(weather_st)