| """The mentor's voice for each spread of the grimoire. |
| |
| MENTOR_TEXT[lang_code][step_index] -> markdown-like string. |
| |
| Spreads 1 and onward may contain ``{hero_name}`` and ``{village_name}`` |
| placeholders that get filled in at render time. |
| """ |
|
|
| from __future__ import annotations |
|
|
|
|
| |
| |
| |
| |
| |
| |
|
|
|
|
| MENTOR_TEXT: dict[str, list[str]] = { |
| "en": [ |
| |
| """The candle is lit. The grimoire is open. The pen is sharpened |
| to a fine bead of black ink. |
| |
| But first, before I write a word of any of this — in what tongue |
| shall I write? My apprentice will need to read every parchment |
| through tears and through dragon-light. Whatever I choose now, I |
| will keep to. The mentor who switches languages mid-spell loses |
| the spell. |
| |
| *Choose for me. I shall write in —*""", |
|
|
| |
| """Seven generations the Iron Peak has cast its shadow. Seven |
| generations of burned villages and silenced kings. I have |
| watched, I have waited, and tonight I write. |
| |
| The Pact binds me — no mentor with magic in his blood may strike |
| at **Vraskar** directly. The dragon's curse can see the spell in |
| the hand that casts it, and burn it back tenfold. So the spell |
| must walk in on someone smaller. Someone the curse will not look |
| for. |
| |
| He must be young. He must be unknown. He must have a name a |
| dragon would forget the moment after hearing it. |
| |
| *I will name him —*""", |
|
|
| |
| """**{hero_name}**, then. Common enough; the dragon will look past |
| it. I write it twice, as a charm, and the ink dries quickly. |
| |
| Now — his place of beginning. Somewhere far enough from the Iron |
| Peak that no smoke has yet darkened his rooftiles. Somewhere with |
| a well and a bell and a road that does not look like it leads |
| anywhere. Somewhere a boy can leave without breaking too many |
| hearts. |
| |
| *His village will be —*""", |
|
|
| |
| """**{hero_name} of {village_name}**. Written, sealed in my |
| memory. Good. |
| |
| He kneels by the well at dawn. I am no longer present in his |
| sight; I am no longer present in any room he has ever entered. |
| But the parchments will speak to him, and he will understand |
| them. |
| |
| Now — the words themselves. |
| |
| I do not know what {hero_name} will face. Only the shape of the |
| dangers. A creature with appetite. A barrier of water. A |
| guardian who asks a question. A force without a face. The dragon |
| himself. |
| |
| I cannot guess which trial will draw which parchment. So I write |
| what I have, and I trust the road to choose. |
| |
| *The first parchment, then. What word does he most need?*""", |
|
|
| |
| """The first is sealed. I press the wax. I do not feel any wiser |
| for having written it. Perhaps that is the point — perhaps |
| wisdom dressed as wisdom is no use at all. |
| |
| The candle bends in a draft I cannot find the source of. |
| |
| *The second parchment. Something different. Something I would |
| never think to say twice.*""", |
|
|
| |
| """Two parchments sealed. Three remain. The candle is low and the |
| night is loud beyond the shutters. |
| |
| The third. **{hero_name}** will read this one only in an hour where |
| he is no longer the boy I have chosen — only after the road has |
| asked something of him that I cannot ask for him. |
| |
| *What shall I leave him for that hour?*""", |
|
|
| |
| """The fourth. Three sealed and two to go. He will not know which |
| parchment he opens first, nor which he saves for last. He will |
| only know the order in which his life narrows. |
| |
| Some of these I write because I know they save him. This one I |
| write because I do not know what else to give. |
| |
| *The fourth parchment.*""", |
|
|
| |
| """The fifth and last. The ink hesitates. |
| |
| This one he will open at the threshold of **Vraskar** — I know |
| this because there will be no others left in his pack. |
| |
| If anything I write may save him from a dragon, let it be this. |
| |
| *The fifth and final parchment.*""", |
|
|
| |
| """All five are sealed. The candle is out. |
| |
| I leave the parchments in his pack as he sleeps. He will not see |
| my face. He will not know my name. He will know only that they |
| are there, and that I have whispered, before sleep: |
| |
| > *"Open one only when your life is in peril. Trust the words I |
| > have written, however strange they seem."* |
| |
| I have done what I can. The road takes him from here.""", |
| ], |
|
|
| "zh": [ |
| |
| """烛火已燃。卷书已开。笔尖磨得极细,墨珠饱满如黑曜石。 |
| |
| 但在我落笔之前——我当以何种文字书写?我的使者必须在泪水 |
| 中、在龙焰之下,依然能读懂这五张羊皮卷上的每一个字。我此 |
| 刻所选的语言,便要从一而终。中途换语的导师,咒语必失。 |
| |
| *请代我抉择。我将以——*""", |
|
|
| |
| """艾恩峰的阴影已笼罩这片大地七代之久。七代之久的焚毁村 |
| 庄,七代之久的沉默王者。我一直凝视,一直等待,而今夜 |
| ——我落笔。 |
| |
| 古老的契约束缚着我——血脉中怀有法力之人,不可亲手对 |
| **瓦斯卡尔**出手。巨龙的诅咒能看见施法之手,并十倍奉 |
| 还。所以法术必须借由更小的人走进去。诅咒所看不见的人。 |
| |
| 他必须年轻。他必须无名。他必须有一个名字——一条龙听过 |
| 后转瞬便会忘记的名字。 |
| |
| *我将为他起名——*""", |
|
|
| |
| """**{hero_name}**。这个名字够寻常,龙的目光会越过它。我提笔 |
| 重写了一遍,作为护持,墨迹很快便干。 |
| |
| 现在——他的起点。一个离艾恩峰足够远的地方,远到他屋顶 |
| 上的瓦片还没被烟熏黑。一个有水井、有钟声的小地方。一 |
| 个弟子离开时,不至让太多人心碎的地方。 |
| |
| *他的村庄将是——*""", |
|
|
| |
| """**{village_name}的{hero_name}**。已写,已封于我的记忆中。 |
| 好。 |
| |
| 他在黎明时分跪于井边。我已不在他的视野中;我已不在他踏 |
| 足过的任何房间里。但羊皮卷会向他说话,而他能听懂。 |
| |
| 现在——便是文字本身了。 |
| |
| 我不知道{hero_name}将面对什么。只知道凶险的形状:一只 |
| 怀着饥饿的生灵,一道水的阻隔,一个发问的守门者,一种 |
| 没有面容的力量,还有那条龙本身。 |
| |
| 我无从猜测哪一重劫难会牵出哪一卷羊皮。所以我写下我所 |
| 能写的,余下的,便交给道路来抉择。 |
| |
| *第一卷羊皮,那么。他此刻最需要的词,是什么?*""", |
|
|
| |
| """第一卷已封缄。我按下蜡印。落笔之后,我并未感到自己变 |
| 得更睿智。或许这正是真意——也许装作智慧的智慧,本就 |
| 毫无用处。 |
| |
| 烛火朝一个我寻不见源头的方向歪去。 |
| |
| *第二卷羊皮。换些别的。换些我永远不会重说一遍的话。*""", |
|
|
| |
| """两卷已封。三卷在前。烛火将尽,窗外的夜声喧响。 |
| |
| 第三卷。**{hero_name}**只会在他不再是我所选的那个弟子 |
| 的那一刻才会开启它——只会在道路向他索取我无法替他承 |
| 受之事时。 |
| |
| *那一刻,我该留给他什么?*""", |
|
|
| |
| """第四卷。三卷已封,两卷在前。他不会知道他最先开启的是 |
| 哪一卷,也不会知道最后留下的是哪一卷。他只会知道,他 |
| 的生命在以何种顺序,渐渐收窄。 |
| |
| 有些卷我写下,是因为我知道它们能救他。这一卷,我写 |
| 下,只因我不知道还能给他什么。 |
| |
| *第四卷。*""", |
|
|
| |
| """第五卷,也是最后一卷。笔尖犹疑。 |
| |
| 这一卷他将在**瓦斯卡尔**的门槛上开启——我知道,因为 |
| 那时他行囊中再无其他可供他选择。 |
| |
| 若我所书的任何一字仍可救他于龙焰之下,便让此言成为那 |
| 一字。 |
| |
| *第五卷,也是最后一卷。*""", |
|
|
| |
| """五卷皆已封缄。烛火已灭。 |
| |
| 我将羊皮卷放入他熟睡的行囊中。他不会见我的容貌。他不 |
| 会知我的名姓。他只会知道:它们在那里,而我,在他入眠 |
| 之前,于他梦中低语过: |
| |
| > *"惟有性命攸关之时方可启封。无论我所书之言多么奇 |
| > 异,亦当信之。"* |
| |
| 我已尽我所能。前路,从此处起,便是他的了。""", |
| ], |
| } |
|
|
|
|
| def text_for(lang_code: str, step: int) -> str: |
| """Return the mentor's text for ``(lang_code, step)``. |
| |
| Falls back to English if ``lang_code`` isn't supported, and clamps |
| ``step`` to the valid range. |
| """ |
| block = MENTOR_TEXT.get(lang_code) or MENTOR_TEXT["en"] |
| if step < 0: |
| step = 0 |
| if step >= len(block): |
| step = len(block) - 1 |
| return block[step] |
|
|
|
|
| |
| |
| |
| |
| |
|
|
| GRIMOIRE_SIGILS: list[list[str]] = [ |
| |
| [ |
| ".......#", |
| "......##", |
| ".....##.", |
| "....##..", |
| "...##.#.", |
| "..###...", |
| "..####..", |
| "##......", |
| ], |
| |
| [ |
| "...##...", |
| "...##...", |
| "########", |
| ".######.", |
| ".##..##.", |
| "##....##", |
| "........", |
| "........", |
| ], |
| |
| [ |
| "...##...", |
| "..####..", |
| ".######.", |
| "########", |
| ".##.#.##", |
| ".##.#.##", |
| ".##.#.##", |
| "........", |
| ], |
| |
| [ |
| ".#....#.", |
| "..#..#..", |
| "...##...", |
| ".######.", |
| ".######.", |
| "...##...", |
| "..#..#..", |
| ".#....#.", |
| ], |
| |
| [ |
| "..####..", |
| ".######.", |
| "###.....", |
| "###.....", |
| "###.....", |
| "###.....", |
| ".######.", |
| "..####..", |
| ], |
| |
| [ |
| "########", |
| "########", |
| ".##..##.", |
| "..####..", |
| "..####..", |
| ".##..##.", |
| "########", |
| "########", |
| ], |
| |
| [ |
| ".######.", |
| "########", |
| "##.##.##", |
| "##.##.##", |
| "##.##.##", |
| "########", |
| ".######.", |
| "........", |
| ], |
| |
| [ |
| "...#....", |
| "..##....", |
| ".###.#..", |
| "####.#..", |
| "#####...", |
| "######..", |
| ".####...", |
| "..##....", |
| ], |
| |
| [ |
| ".####...", |
| "######..", |
| ".####...", |
| "..##....", |
| "..##....", |
| "..####..", |
| "..#..#..", |
| "..####..", |
| ], |
| ] |
|
|
|
|
| def sigil_svg(step: int, color: str = "#f0c060", render_px: int = 36) -> str: |
| """Render the pixel-art sigil for ``step`` as inline SVG. |
| |
| Uses ``shape-rendering="crispEdges"`` so the pixels stay crisp at any |
| rendered size. Returns the bare ``<svg>`` element (no wrapper).""" |
| if step < 0: |
| step = 0 |
| if step >= len(GRIMOIRE_SIGILS): |
| step = len(GRIMOIRE_SIGILS) - 1 |
| grid = GRIMOIRE_SIGILS[step] |
| h = len(grid) |
| w = max(len(row) for row in grid) if grid else 0 |
| rects: list[str] = [] |
| for y, row in enumerate(grid): |
| for x, c in enumerate(row): |
| if c == "#": |
| rects.append( |
| f'<rect x="{x}" y="{y}" width="1" height="1" fill="{color}"/>' |
| ) |
| return ( |
| f'<svg viewBox="0 0 {w} {h}" ' |
| f'width="{render_px}" height="{render_px}" ' |
| f'shape-rendering="crispEdges" ' |
| f'style="display:block;image-rendering:pixelated">' |
| + "".join(rects) |
| + "</svg>" |
| ) |
|
|