silver-tongue 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/courses/zh-china-en/course.json +1 -1
- package/dist/silver-tongue.mjs +152 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> You arrive speaking pidgin; you leave with a silver tongue.
|
|
4
4
|
|
|
5
|
-
A language-learning life game. Your phone is dead, your wallet is gone, and you're standing on a street in a Chinese city with ¥20 in your pocket. An old man on a bench teaches you your first words, and later explains how the language works when you ask; after that you earn your living by understanding people. Every job, purchase and conversation happens in the language you're learning.
|
|
5
|
+
A language-learning life game. Your phone is dead, your wallet is gone, and you're standing on a street in a Chinese city with ¥20 in your pocket. You start by telling the game your name. An old man on a bench teaches you your first words, and later explains how the language works when you ask; after that you earn your living by understanding people. Every job, purchase and conversation happens in the language you're learning.
|
|
6
6
|
|
|
7
7
|
The first course is Mandarin (HSK 1), in a Chinese city, explained in English.
|
|
8
8
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"zh-china-en","typing":false,"words":{"w0003":{"id":"w0003","w":"三","lv":"1","gloss":"three; 3","pron":"sān"},"w0009":{"id":"w0009","w":"不","lv":"1","gloss":"no; not","pron":"bù"},"w0010":{"id":"w0010","w":"不客气","lv":"1","gloss":"you're welcome; don't mention it","pron":"bùkèqi"},"w0020":{"id":"w0020","w":"五","lv":"1","gloss":"five; 5","pron":"wǔ"},"w0028":{"id":"w0028","w":"你","lv":"1","gloss":"you (informal)","pron":"nǐ"},"w0034":{"id":"w0034","w":"再见","lv":"1","gloss":"goodbye","pron":"zàijiàn"},"w0037":{"id":"w0037","w":"几","lv":"1","gloss":"how much; how many","pron":"jǐ"},"w0051":{"id":"w0051","w":"吗","lv":"1","gloss":"(question particle for \"yes-no\" questions)","pron":"ma"},"w0060":{"id":"w0060","w":"四","lv":"1","gloss":"four; 4","pron":"sì"},"w0072":{"id":"w0072","w":"好","lv":"1","gloss":"good; appropriate; proper","pron":"hǎo"},"w0079":{"id":"w0079","w":"对不起","lv":"1","gloss":"I'm sorry; excuse me; I beg your pardon","pron":"duìbuqǐ"},"w0084":{"id":"w0084","w":"工作","lv":"1","gloss":"to work; (of a machine) to operate","pron":"gōngzuò"},"w0099":{"id":"w0099","w":"是","lv":"1","gloss":"to be (followed by substantives only); correct; right; true","pron":"shì"},"w0108":{"id":"w0108","w":"水","lv":"1","gloss":"water; river","pron":"shuǐ"},"w0112":{"id":"w0112","w":"没关系","lv":"1","gloss":"it doesn't matter","pron":"méiguānxi"},"w0133":{"id":"w0133","w":"茶","lv":"1","gloss":"tea; tea plant","pron":"chá"},"w0141":{"id":"w0141","w":"谢谢","lv":"1","gloss":"to thank; thanks","pron":"xièxie"},"x0001":{"id":"x0001","w":"杯","lv":"1","gloss":"cup; glass (measure word for drinks)","pron":"bēi","bonus":true}},"concepts":{"tea":["w0133"],"water":["w0108"],"three":["w0003"],"four":["w0060"],"five":["w0020"],"hello":["w0028","w0072"],"thanks":["w0141"],"sorry":["w0079"],"bye":["w0034"]},"groups":{"courtesies":["hello","thanks","sorry"],"drinks":["tea","water"],"numbers_3_5":["three","four","five"]},"world":{"start":"street","currency":"¥","slotsPerDay":4,"startWallet":20,"foodPerDay":5,"rentPerWeek":50,"places":{"street":{"links":["noodle_shop"]},"noodle_shop":{"links":["street"]}},"npcs":{"wang":{"place":"street"},"cook":{"place":"noodle_shop"}},"mentor":{"npc":"wang","after":"street-hello"}},"scenes":[{"id":"noodle-intro","place":"noodle_shop","npc":"cook","stage":1,"after":["street-hello"],"requires":{},"repeatable":false,"trustGain":1,"exchanges":[{"id":"greet","slots":{},"expect":{"action":"greet"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}}}},{"id":"job","slots":{},"expect":{"action":"accept"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"工作,好吗?","tokens":[{"start":0,"end":2,"word":"w0084"},{"start":3,"end":4,"word":"w0072"},{"start":4,"end":5,"word":"w0051"}],"meaning":"Want to work here?"},"reply":{"text":"好。","tokens":[{"start":0,"end":1,"word":"w0072"}],"meaning":"OK."}}}}]},{"id":"noodle-shift","place":"noodle_shop","npc":"cook","stage":1,"after":["noodle-intro"],"requires":{"trust":{"cook":1}},"repeatable":true,"trustGain":1,"exchanges":[{"id":"drink","slots":{"item":"drinks"},"expect":{"action":"fetch","item":"$item"},"hinges":["$item"],"pay":1,"missCost":1,"variants":{"item=tea":{"npc":{"text":"茶。","tokens":[{"start":0,"end":1,"word":"w0133"}],"meaning":"Tea."},"reply":{"text":"好,茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0133"}],"meaning":"OK, tea."}},"item=water":{"npc":{"text":"水。","tokens":[{"start":0,"end":1,"word":"w0108"}],"meaning":"Water."},"reply":{"text":"好,水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0108"}],"meaning":"OK, water."}}}},{"id":"order","slots":{"item":"drinks","count":"numbers_3_5"},"expect":{"action":"serve","item":"$item","count":"$count"},"hinges":["$item","$count"],"pay":3,"missCost":2,"variants":{"count=three|item=tea":{"npc":{"text":"三杯茶。","tokens":[{"start":0,"end":1,"word":"w0003"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}],"meaning":"Three cups of tea."},"reply":{"text":"好,三杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}],"meaning":"OK, three cups of tea."},"rephrase":{"text":"茶。三杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Tea. Three cups."}},"count=three|item=water":{"npc":{"text":"三杯水。","tokens":[{"start":0,"end":1,"word":"w0003"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}],"meaning":"Three cups of water."},"reply":{"text":"好,三杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}],"meaning":"OK, three cups of water."},"rephrase":{"text":"水。三杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Water. Three cups."}},"count=four|item=tea":{"npc":{"text":"四杯茶。","tokens":[{"start":0,"end":1,"word":"w0060"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}],"meaning":"Four cups of tea."},"reply":{"text":"好,四杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}],"meaning":"OK, four cups of tea."},"rephrase":{"text":"茶。四杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Tea. Four cups."}},"count=four|item=water":{"npc":{"text":"四杯水。","tokens":[{"start":0,"end":1,"word":"w0060"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}],"meaning":"Four cups of water."},"reply":{"text":"好,四杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}],"meaning":"OK, four cups of water."},"rephrase":{"text":"水。四杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Water. Four cups."}},"count=five|item=tea":{"npc":{"text":"五杯茶。","tokens":[{"start":0,"end":1,"word":"w0020"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}],"meaning":"Five cups of tea."},"reply":{"text":"好,五杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}],"meaning":"OK, five cups of tea."},"rephrase":{"text":"茶。五杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Tea. Five cups."}},"count=five|item=water":{"npc":{"text":"五杯水。","tokens":[{"start":0,"end":1,"word":"w0020"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}],"meaning":"Five cups of water."},"reply":{"text":"好,五杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}],"meaning":"OK, five cups of water."},"rephrase":{"text":"水。五杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Water. Five cups."}}}}]},{"id":"street-hello","place":"street","npc":"wang","stage":1,"after":[],"requires":{},"repeatable":false,"trustGain":1,"exchanges":[{"id":"hello","slots":{},"expect":{"action":"greet"},"hinges":["hello"],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}}}},{"id":"polite","slots":{"said":"courtesies"},"expect":{"action":"answer","said":"$said"},"hinges":["$said"],"pay":0,"missCost":0,"variants":{"said=hello":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}},"said=thanks":{"npc":{"text":"谢谢!","tokens":[{"start":0,"end":2,"word":"w0141"}],"meaning":"Thank you!"},"reply":{"text":"不客气!","tokens":[{"start":0,"end":3,"word":"w0010"}],"meaning":"You're welcome!"}},"said=sorry":{"npc":{"text":"对不起!","tokens":[{"start":0,"end":3,"word":"w0079"}],"meaning":"Sorry!"},"reply":{"text":"没关系!","tokens":[{"start":0,"end":3,"word":"w0112"}],"meaning":"That's all right!"}}}},{"id":"polite-again","slots":{"said":"courtesies"},"expect":{"action":"answer","said":"$said"},"hinges":["$said"],"pay":0,"missCost":0,"variants":{"said=hello":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}},"said=thanks":{"npc":{"text":"谢谢!","tokens":[{"start":0,"end":2,"word":"w0141"}],"meaning":"Thank you!"},"reply":{"text":"不客气!","tokens":[{"start":0,"end":3,"word":"w0010"}],"meaning":"You're welcome!"}},"said=sorry":{"npc":{"text":"对不起!","tokens":[{"start":0,"end":3,"word":"w0079"}],"meaning":"Sorry!"},"reply":{"text":"没关系!","tokens":[{"start":0,"end":3,"word":"w0112"}],"meaning":"That's all right!"}}}},{"id":"bye","slots":{},"expect":{"action":"farewell"},"hinges":["bye"],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},"reply":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"}}}}]},{"id":"street-practice","place":"street","npc":"wang","stage":1,"after":["street-hello"],"requires":{},"repeatable":true,"trustGain":1,"exchanges":[{"id":"hello","slots":{},"expect":{"action":"greet"},"hinges":["hello"],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}}}},{"id":"polite-1","slots":{"said":"courtesies"},"expect":{"action":"answer","said":"$said"},"hinges":["$said"],"pay":0,"missCost":0,"variants":{"said=hello":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}},"said=thanks":{"npc":{"text":"谢谢!","tokens":[{"start":0,"end":2,"word":"w0141"}],"meaning":"Thank you!"},"reply":{"text":"不客气!","tokens":[{"start":0,"end":3,"word":"w0010"}],"meaning":"You're welcome!"}},"said=sorry":{"npc":{"text":"对不起!","tokens":[{"start":0,"end":3,"word":"w0079"}],"meaning":"Sorry!"},"reply":{"text":"没关系!","tokens":[{"start":0,"end":3,"word":"w0112"}],"meaning":"That's all right!"}}}},{"id":"polite-2","slots":{"said":"courtesies"},"expect":{"action":"answer","said":"$said"},"hinges":["$said"],"pay":0,"missCost":0,"variants":{"said=hello":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}},"said=thanks":{"npc":{"text":"谢谢!","tokens":[{"start":0,"end":2,"word":"w0141"}],"meaning":"Thank you!"},"reply":{"text":"不客气!","tokens":[{"start":0,"end":3,"word":"w0010"}],"meaning":"You're welcome!"}},"said=sorry":{"npc":{"text":"对不起!","tokens":[{"start":0,"end":3,"word":"w0079"}],"meaning":"Sorry!"},"reply":{"text":"没关系!","tokens":[{"start":0,"end":3,"word":"w0112"}],"meaning":"That's all right!"}}}},{"id":"bye","slots":{},"expect":{"action":"farewell"},"hinges":["bye"],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},"reply":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"}}}}]}],"reactions":{"wrong-generic":{"text":"不是。","tokens":[{"start":0,"end":1,"word":"w0009"},{"start":1,"end":2,"word":"w0099"}],"meaning":"No."},"wrong-count":{"text":"几杯?","tokens":[{"start":0,"end":1,"word":"w0037"},{"start":1,"end":2,"word":"x0001"}],"meaning":"How many cups?"}},"learnerFtl":"# Text-game UI. Every id here is required by packages/tui (UI_KEYS).\n\nhud = Day { $day } · slot { $slot }/{ $slots } · { $currency }{ $wallet } · { $rank }{ $rentLate ->\n [yes] { \" · rent due\" }\n *[no] {\"\"}\n}\n\nrank-0 = Pidgin\nrank-1 = Getting By\nrank-2 = Conversational\nrank-3 = Fluent\nrank-4 = Silver Tongue\n\nmenu-title = What now?\nmenu-talk = Talk to { $npc }: { $scene }\nmenu-go = Go to { $place }\nmenu-mentor = Ask { $npc } about the language\ncost-slot = { \" · 1 slot\" }\nmenu-sleep = Sleep (end the day)\nmenu-quit = Save and quit\n\nkeys-explore = [{ $keys }] choose · [n] notebook · [q] quit\nkeys-pick = [{ $keys }] reply · [w] word help · [n] notebook\nkeys-tiles = [{ $keys }] add · [⌫] undo · [enter] say · [w] help · [n] notebook\nkeys-help = [{ $keys }] look up · [esc] back\nkeys-help-sentence = [{ $keys }] look up · [s] whole sentence · [esc] back\n\nhelp-title = Which word?\nhelp-sentence = The whole sentence\nhelp-in-replies = In the replies:\nreply-title = Your reply:\ntiles-title = Your reply: tap the words in order, then press [enter].\ntiles-answer = You say:\nyou = You\n\nmismatch = That's not what they asked for.\nrephrased = (slower)\nwallet-change = { $sign }{ $currency }{ $amount } ({ $reason })\nreason-wages = wages\nreason-mixup = mix-up\nreason-food = food\nreason-rent = rent\ntrust-up = { $npc } trusts you a little more ({ $trust }).\nscene-done = { $earned ->\n [0] Done.\n *[other] Done. You earned { $currency }{ $earned }.\n}\nunlocked = New: { $scene }\nrank-up = You're now: { $rank }\nday-ended = Day { $day } is over. You sleep.\nreject-unknown-scene = There's nobody here for that.\nreject-in-scene = Finish the conversation first.\nreject-wrong-place = They're not here.\nreject-locked = They're not ready to talk about that yet.\nreject-no-slots = You're out of time today. Sleep first.\nreject-stale-run = That conversation can't continue. Start it again.\nreject-no-pick = Choose a reply with the number keys.\nreject-bad-choice = There's no reply with that number.\nreject-no-tiles = Build your reply from the tiles.\nreject-bad-tile = There's no tile with that number.\nreject-not-linked = You can't get there from here.\nreject-unknown-word = That word isn't in the dictionary.\nnotice-read-only = Your progress can't be saved on this computer, so this session won't be kept.\nnotice-bad-save = Your save couldn't be read. It was kept as a backup and a new game started.\n\nresume-title = Your games:\nresume-item = Day { $day } · { $place } · { $currency }{ $wallet } · { $done ->\n [one] 1 scene done\n *[other] { $done } scenes done\n} · last played { $date }\nresume-ask = Which one? (number, or enter to cancel)\nresume-none = No saved games yet, so here's a new one.\nreject-no-mentor = There's nobody here to explain things.\nnote-hint = { $npc } seems to have something to tell you.\nmentor-nothing = { $npc } has nothing new to explain today.\n\nkeys-notebook = [↑↓] scroll · [esc] back\nnotebook-progress = Stage { $stage }: { $known } of { $total } words known · { $heard } heard\nnotebook-empty = Nothing yet. Words you hear are written down here.\nnotebook-elsewhere = Heard elsewhere\nnotebook-notes = Notes\nexport-none = There's no saved game to export yet.\nimport-bad = That line isn't a saved game for this course ({ $reason }).\nimport-done = Added: { $game }. Run the game to continue it.\n\n# Browser page controls (packages/tui-web).\nweb-new = New game\nweb-games = Games\nweb-games-none = No saved games yet.\nweb-export = Export\nweb-export-hint = This line is your whole game. Paste it into Import on another device, or into the terminal game with: npx silver-tongue --import -\nweb-copy = Copy\nweb-copied = Copied\nweb-import = Import\nweb-import-hint = Paste a line from Export, here or from the terminal game. It is added as a new game; nothing is overwritten.\nweb-import-go = Add game\nweb-close = Close\nweb-saved = Your game is saved. You can close this tab, or keep playing.\n\n# The story. intro-1, intro-2, … open a new game ($currency, $wallet, $rent are available).\nintro-1 = Your phone is dead, your wallet is gone, and the bus that brought you here left an hour ago.\nintro-2 = You're standing on a street in a Chinese city with { $currency }{ $wallet } in your pocket. You know nobody, and nobody here speaks your language.\nintro-3 = Food costs money. Rent is { $currency }{ $rent }, due at the end of the week. To earn anything you'll need work, and for work you'll need words.\nintro-4 = On a bench by the road, an old man is watching you with open curiosity. Maybe start there.\n\nplace-street = Main Street\nplace-street-desc = Bikes, steam and shouting. An old man sits on a bench by the road. Across it, a noodle shop glows.\nplace-noodle_shop = Noodle Shop\nplace-noodle_shop-desc = Steam everywhere. The cook is shouting orders at nobody in particular.\n\nnpc-wang = Old Wang\nnpc-cook = Cook\n\n# Scene names for the menu; scene-<id>-start and scene-<id>-end are optional narration.\nscene-street-hello = Say hello\nscene-street-hello-start = The old man pats the bench beside him. \"Wang,\" he says, pointing at himself. He seems to have decided you need lessons: he says something, you answer. Use [w] when you're stuck.\nscene-street-hello-end = Old Wang beams. He points across the road at the noodle shop, mimes slurping noodles, then rubs his fingers together. Work. Money.\n\nscene-street-practice = Practise greetings\nscene-street-practice-start = Old Wang shuffles over to make room. Another lesson, then.\nscene-street-practice-end = Old Wang nods, satisfied, and goes back to watching the street.\n\nscene-noodle-intro = Ask about work\nscene-noodle-intro-start = The cook looks you up and down, wiping her hands on her apron.\nscene-noodle-intro-end = She throws you an apron. You have a job, sort of.\n\nscene-noodle-shift = Work a shift\nscene-noodle-shift-start = Orders fly across the counter. Keep up.\nscene-noodle-shift-end = The rush dies down. The cook counts coins into your hand.\n\n# What a reply does (action-<name>), and on a mix-up what was asked (asked-<name>).\n# Concept values arrive as learner names: $item = \"tea\", $count = \"three\".\naction-fetch = You bring { $item }.\nasked-fetch = They wanted { $item }.\naction-serve = You set down { $count } cups of { $item }.\nasked-serve = They wanted { $count } cups of { $item }.\n\n# Usage notes Old Wang explains (languages/zh/notes.json). note-<id>-title and note-<id>.\n\nnote-bukeqi-title = 不客气, \"you're welcome\"\nnote-bukeqi = 不客气 (bùkèqi) literally means \"don't be polite\": no need to stand on ceremony. It's the usual answer to 谢谢. Between close friends, people often skip 谢谢 altogether; thanking a friend too much can sound distant.\n\nnote-hao-ma-title = 好 and 吗\nnote-hao-ma = 好 (hǎo) means \"good\", and on its own it's how you agree: \"OK\". Put 吗 (ma) at the end of a sentence and it becomes a yes-or-no question: 工作,好吗? is \"Work, OK?\" To say yes, answer with the key word: 好。\n\nnote-bei-title = 杯, a measure word\nnote-bei = Chinese doesn't say \"three teas\". A measure word goes between a number and a thing: 三杯茶 is \"three cups of tea\". 杯 (bēi) is the one for cups and glasses. There are many measure words; 个 (gè) is the everyday all-purpose one.\n","conceptNames":{"tea":"tea","water":"water","three":"three","four":"four","five":"five","hello":"Hello","thanks":"Thank you","sorry":"Sorry","bye":"Goodbye"},"stageWords":{"1":["w0001","w0002","w0003","w0004","w0005","w0006","w0007","w0008","w0009","w0010","w0011","w0012","w0013","w0014","w0015","w0016","w0017","w0018","w0019","w0020","w0021","w0022","w0023","w0024","w0025","w0026","w0027","w0028","w0029","w0030","w0031","w0032","w0033","w0034","w0035","w0036","w0037","w0038","w0039","w0040","w0041","w0042","w0043","w0044","w0045","w0046","w0047","w0048","w0049","w0050","w0051","w0052","w0053","w0054","w0055","w0056","w0057","w0058","w0059","w0060","w0061","w0062","w0063","w0064","w0065","w0066","w0067","w0068","w0069","w0070","w0071","w0072","w0073","w0074","w0075","w0076","w0077","w0078","w0079","w0080","w0081","w0082","w0083","w0084","w0085","w0086","w0087","w0088","w0089","w0090","w0091","w0092","w0093","w0094","w0095","w0096","w0097","w0098","w0099","w0100","w0101","w0102","w0103","w0104","w0105","w0106","w0107","w0108","w0109","w0110","w0111","w0112","w0113","w0114","w0115","w0116","w0117","w0118","w0119","w0120","w0121","w0122","w0123","w0124","w0125","w0126","w0127","w0128","w0129","w0130","w0131","w0132","w0133","w0134","w0135","w0136","w0137","w0138","w0139","w0140","w0141","w0142","w0143","w0144","w0145","w0146","w0147","w0148","w0149","w0150"],"2":["w0151","w0152","w0153","w0154","w0155","w0156","w0157","w0158","w0159","w0160","w0161","w0162","w0163","w0164","w0165","w0166","w0167","w0168","w0169","w0170","w0171","w0172","w0173","w0174","w0175","w0176","w0177","w0178","w0179","w0180","w0181","w0182","w0183","w0184","w0185","w0186","w0187","w0188","w0189","w0190","w0191","w0192","w0193","w0194","w0195","w0196","w0197","w0198","w0199","w0200","w0201","w0202","w0203","w0204","w0205","w0206","w0207","w0208","w0209","w0210","w0211","w0212","w0213","w0214","w0215","w0216","w0217","w0218","w0219","w0220","w0221","w0222","w0223","w0224","w0225","w0226","w0227","w0228","w0229","w0230","w0231","w0232","w0233","w0234","w0235","w0236","w0237","w0238","w0239","w0240","w0241","w0242","w0243","w0244","w0245","w0246","w0247","w0248","w0249","w0250","w0251","w0252","w0253","w0254","w0255","w0256","w0257","w0258","w0259","w0260","w0261","w0262","w0263","w0264","w0265","w0266","w0267","w0268","w0269","w0270","w0271","w0272","w0273","w0274","w0275","w0276","w0277","w0278","w0279","w0280","w0281","w0282","w0283","w0284","w0285","w0286","w0287","w0288","w0289","w0290","w0291","w0292","w0293","w0294","w0295","w0296","w0297"],"3":["w0298","w0299","w0300","w0301","w0302","w0303","w0304","w0305","w0306","w0307","w0308","w0309","w0310","w0311","w0312","w0313","w0314","w0315","w0316","w0317","w0318","w0319","w0320","w0321","w0322","w0323","w0324","w0325","w0326","w0327","w0328","w0329","w0330","w0331","w0332","w0333","w0334","w0335","w0336","w0337","w0338","w0339","w0340","w0341","w0342","w0343","w0344","w0345","w0346","w0347","w0348","w0349","w0350","w0351","w0352","w0353","w0354","w0355","w0356","w0357","w0358","w0359","w0360","w0361","w0362","w0363","w0364","w0365","w0366","w0367","w0368","w0369","w0370","w0371","w0372","w0373","w0374","w0375","w0376","w0377","w0378","w0379","w0380","w0381","w0382","w0383","w0384","w0385","w0386","w0387","w0388","w0389","w0390","w0391","w0392","w0393","w0394","w0395","w0396","w0397","w0398","w0399","w0400","w0401","w0402","w0403","w0404","w0405","w0406","w0407","w0408","w0409","w0410","w0411","w0412","w0413","w0414","w0415","w0416","w0417","w0418","w0419","w0420","w0421","w0422","w0423","w0424","w0425","w0426","w0427","w0428","w0429","w0430","w0431","w0432","w0433","w0434","w0435","w0436","w0437","w0438","w0439","w0440","w0441","w0442","w0443","w0444","w0445","w0446","w0447","w0448","w0449","w0450","w0451","w0452","w0453","w0454","w0455","w0456","w0457","w0458","w0459","w0460","w0461","w0462","w0463","w0464","w0465","w0466","w0467","w0468","w0469","w0470","w0471","w0472","w0473","w0474","w0475","w0476","w0477","w0478","w0479","w0480","w0481","w0482","w0483","w0484","w0485","w0486","w0487","w0488","w0489","w0490","w0491","w0492","w0493","w0494","w0495","w0496","w0497","w0498","w0499","w0500","w0501","w0502","w0503","w0504","w0505","w0506","w0507","w0508","w0509","w0510","w0511","w0512","w0513","w0514","w0515","w0516","w0517","w0518","w0519","w0520","w0521","w0522","w0523","w0524","w0525","w0526","w0527","w0528","w0529","w0530","w0531","w0532","w0533","w0534","w0535","w0536","w0537","w0538","w0539","w0540","w0541","w0542","w0543","w0544","w0545","w0546","w0547","w0548","w0549","w0550","w0551","w0552","w0553","w0554","w0555","w0556","w0557","w0558","w0559","w0560","w0561","w0562","w0563","w0564","w0565","w0566","w0567","w0568","w0569","w0570","w0571","w0572","w0573","w0574","w0575","w0576","w0577","w0578","w0579","w0580","w0581","w0582","w0583","w0584","w0585","w0586","w0587","w0588","w0589","w0590","w0591","w0592","w0593","w0594","w0595"],"4":["w0596","w0597","w0598","w0599","w0600","w0601","w0602","w0603","w0604","w0605","w0606","w0607","w0608","w0609","w0610","w0611","w0612","w0613","w0614","w0615","w0616","w0617","w0618","w0619","w0620","w0621","w0622","w0623","w0624","w0625","w0626","w0627","w0628","w0629","w0630","w0631","w0632","w0633","w0634","w0635","w0636","w0637","w0638","w0639","w0640","w0641","w0642","w0643","w0644","w0645","w0646","w0647","w0648","w0649","w0650","w0651","w0652","w0653","w0654","w0655","w0656","w0657","w0658","w0659","w0660","w0661","w0662","w0663","w0664","w0665","w0666","w0667","w0668","w0669","w0670","w0671","w0672","w0673","w0674","w0675","w0676","w0677","w0678","w0679","w0680","w0681","w0682","w0683","w0684","w0685","w0686","w0687","w0688","w0689","w0690","w0691","w0692","w0693","w0694","w0695","w0696","w0697","w0698","w0699","w0700","w0701","w0702","w0703","w0704","w0705","w0706","w0707","w0708","w0709","w0710","w0711","w0712","w0713","w0714","w0715","w0716","w0717","w0718","w0719","w0720","w0721","w0722","w0723","w0724","w0725","w0726","w0727","w0728","w0729","w0730","w0731","w0732","w0733","w0734","w0735","w0736","w0737","w0738","w0739","w0740","w0741","w0742","w0743","w0744","w0745","w0746","w0747","w0748","w0749","w0750","w0751","w0752","w0753","w0754","w0755","w0756","w0757","w0758","w0759","w0760","w0761","w0762","w0763","w0764","w0765","w0766","w0767","w0768","w0769","w0770","w0771","w0772","w0773","w0774","w0775","w0776","w0777","w0778","w0779","w0780","w0781","w0782","w0783","w0784","w0785","w0786","w0787","w0788","w0789","w0790","w0791","w0792","w0793","w0794","w0795","w0796","w0797","w0798","w0799","w0800","w0801","w0802","w0803","w0804","w0805","w0806","w0807","w0808","w0809","w0810","w0811","w0812","w0813","w0814","w0815","w0816","w0817","w0818","w0819","w0820","w0821","w0822","w0823","w0824","w0825","w0826","w0827","w0828","w0829","w0830","w0831","w0832","w0833","w0834","w0835","w0836","w0837","w0838","w0839","w0840","w0841","w0842","w0843","w0844","w0845","w0846","w0847","w0848","w0849","w0850","w0851","w0852","w0853","w0854","w0855","w0856","w0857","w0858","w0859","w0860","w0861","w0862","w0863","w0864","w0865","w0866","w0867","w0868","w0869","w0870","w0871","w0872","w0873","w0874","w0875","w0876","w0877","w0878","w0879","w0880","w0881","w0882","w0883","w0884","w0885","w0886","w0887","w0888","w0889","w0890","w0891","w0892","w0893","w0894","w0895","w0896","w0897","w0898","w0899","w0900","w0901","w0902","w0903","w0904","w0905","w0906","w0907","w0908","w0909","w0910","w0911","w0912","w0913","w0914","w0915","w0916","w0917","w0918","w0919","w0920","w0921","w0922","w0923","w0924","w0925","w0926","w0927","w0928","w0929","w0930","w0931","w0932","w0933","w0934","w0935","w0936","w0937","w0938","w0939","w0940","w0941","w0942","w0943","w0944","w0945","w0946","w0947","w0948","w0949","w0950","w0951","w0952","w0953","w0954","w0955","w0956","w0957","w0958","w0959","w0960","w0961","w0962","w0963","w0964","w0965","w0966","w0967","w0968","w0969","w0970","w0971","w0972","w0973","w0974","w0975","w0976","w0977","w0978","w0979","w0980","w0981","w0982","w0983","w0984","w0985","w0986","w0987","w0988","w0989","w0990","w0991","w0992","w0993","w0994","w0995","w0996","w0997","w0998","w0999","w1000","w1001","w1002","w1003","w1004","w1005","w1006","w1007","w1008","w1009","w1010","w1011","w1012","w1013","w1014","w1015","w1016","w1017","w1018","w1019","w1020","w1021","w1022","w1023","w1024","w1025","w1026","w1027","w1028","w1029","w1030","w1031","w1032","w1033","w1034","w1035","w1036","w1037","w1038","w1039","w1040","w1041","w1042","w1043","w1044","w1045","w1046","w1047","w1048","w1049","w1050","w1051","w1052","w1053","w1054","w1055","w1056","w1057","w1058","w1059","w1060","w1061","w1062","w1063","w1064","w1065","w1066","w1067","w1068","w1069","w1070","w1071","w1072","w1073","w1074","w1075","w1076","w1077","w1078","w1079","w1080","w1081","w1082","w1083","w1084","w1085","w1086","w1087","w1088","w1089","w1090","w1091","w1092","w1093","w1094","w1095","w1096","w1097","w1098","w1099","w1100","w1101","w1102","w1103","w1104","w1105","w1106","w1107","w1108","w1109","w1110","w1111","w1112","w1113","w1114","w1115","w1116","w1117","w1118","w1119","w1120","w1121","w1122","w1123","w1124","w1125","w1126","w1127","w1128","w1129","w1130","w1131","w1132","w1133","w1134","w1135","w1136","w1137","w1138","w1139","w1140","w1141","w1142","w1143","w1144","w1145","w1146","w1147","w1148","w1149","w1150","w1151","w1152","w1153","w1154","w1155","w1156","w1157","w1158","w1159","w1160","w1161","w1162","w1163","w1164","w1165","w1166","w1167","w1168","w1169","w1170","w1171","w1172","w1173","w1174","w1175","w1176","w1177","w1178","w1179","w1180","w1181","w1182","w1183","w1184","w1185","w1186","w1187","w1188","w1189","w1190","w1191","w1192","w1193"]},"notes":[{"id":"bukeqi","trigger":{"scene":"street-hello"}},{"id":"hao-ma","trigger":{"scene":"noodle-intro"}},{"id":"bei","trigger":{"word":"x0001"}}]}
|
|
1
|
+
{"id":"zh-china-en","typing":false,"words":{"w0003":{"id":"w0003","w":"三","lv":"1","gloss":"three; 3","pron":"sān"},"w0009":{"id":"w0009","w":"不","lv":"1","gloss":"no; not","pron":"bù"},"w0010":{"id":"w0010","w":"不客气","lv":"1","gloss":"you're welcome; don't mention it","pron":"bùkèqi"},"w0018":{"id":"w0018","w":"了","lv":"1","gloss":"(completed action marker)","pron":"le"},"w0020":{"id":"w0020","w":"五","lv":"1","gloss":"five; 5","pron":"wǔ"},"w0023":{"id":"w0023","w":"什么","lv":"1","gloss":"what?; something","pron":"shénme"},"w0028":{"id":"w0028","w":"你","lv":"1","gloss":"you (informal)","pron":"nǐ"},"w0034":{"id":"w0034","w":"再见","lv":"1","gloss":"goodbye","pron":"zàijiàn"},"w0037":{"id":"w0037","w":"几","lv":"1","gloss":"how much; how many","pron":"jǐ"},"w0046":{"id":"w0046","w":"叫","lv":"1","gloss":"to shout; to call","pron":"jiào"},"w0047":{"id":"w0047","w":"吃","lv":"1","gloss":"to eat; to consume; to eat at (a cafeteria etc)","pron":"chī"},"w0049":{"id":"w0049","w":"名字","lv":"1","gloss":"name (of a person or thing)","pron":"míngzi"},"w0051":{"id":"w0051","w":"吗","lv":"1","gloss":"(question particle for \"yes-no\" questions)","pron":"ma"},"w0053":{"id":"w0053","w":"呢","lv":"1","gloss":"particle indicating that a previously asked question is…","pron":"ne"},"w0058":{"id":"w0058","w":"喜欢","lv":"1","gloss":"to like; to be fond of","pron":"xǐhuan"},"w0060":{"id":"w0060","w":"四","lv":"1","gloss":"four; 4","pron":"sì"},"w0072":{"id":"w0072","w":"好","lv":"1","gloss":"good; appropriate; proper","pron":"hǎo"},"w0079":{"id":"w0079","w":"对不起","lv":"1","gloss":"I'm sorry; excuse me; I beg your pardon","pron":"duìbuqǐ"},"w0084":{"id":"w0084","w":"工作","lv":"1","gloss":"to work; (of a machine) to operate","pron":"gōngzuò"},"w0087":{"id":"w0087","w":"很","lv":"1","gloss":"(adverb of degree); quite","pron":"hěn"},"w0091":{"id":"w0091","w":"我","lv":"1","gloss":"I; me; my","pron":"wǒ"},"w0099":{"id":"w0099","w":"是","lv":"1","gloss":"to be (followed by substantives only); correct; right; true","pron":"shì"},"w0101":{"id":"w0101","w":"有","lv":"1","gloss":"to have; there is","pron":"yǒu"},"w0102":{"id":"w0102","w":"朋友","lv":"1","gloss":"friend","pron":"péngyou"},"w0108":{"id":"w0108","w":"水","lv":"1","gloss":"water; river","pron":"shuǐ"},"w0112":{"id":"w0112","w":"没关系","lv":"1","gloss":"it doesn't matter","pron":"méiguānxi"},"w0133":{"id":"w0133","w":"茶","lv":"1","gloss":"tea; tea plant","pron":"chá"},"w0141":{"id":"w0141","w":"谢谢","lv":"1","gloss":"to thank; thanks","pron":"xièxie"},"w0143":{"id":"w0143","w":"那","lv":"1","gloss":"(specifier) that; the; those (colloquial pr. [nèi])","pron":"nà"},"w0145":{"id":"w0145","w":"里","lv":"1","gloss":"lining; interior","pron":"lǐ"},"w0149":{"id":"w0149","w":"饭馆","lv":"1","gloss":"restaurant","pron":"fànguǎn"},"w0583":{"id":"w0583","w":"面条","lv":"3","gloss":"noodles","pron":"miàntiáo","bonus":true},"w0587":{"id":"w0587","w":"饿","lv":"3","gloss":"to be hungry; hungry","pron":"è","bonus":true},"x0001":{"id":"x0001","w":"杯","lv":"1","gloss":"cup; glass (measure word for drinks)","pron":"bēi","bonus":true},"x0002":{"id":"x0002","w":"老王","lv":"1","gloss":"Old Wang (a name: 老 \"old\" + the surname Wang)","pron":"Lǎo Wáng","bonus":true},"x0004":{"id":"x0004","w":"小张","lv":"1","gloss":"Xiao Zhang, the cook (小 \"little\" + the surname Zhang)","pron":"Xiǎo Zhāng","bonus":true}},"concepts":{"tea":["w0133"],"water":["w0108"],"three":["w0003"],"four":["w0060"],"five":["w0020"],"hello":["w0028","w0072"],"thanks":["w0141"],"sorry":["w0079"],"bye":["w0034"]},"groups":{"courtesies":["hello","thanks","sorry"],"drinks":["tea","water"],"numbers_3_5":["three","four","five"]},"world":{"start":"street","currency":"¥","slotsPerDay":4,"startWallet":20,"foodPerDay":5,"rentPerWeek":50,"places":{"street":{"links":["noodle_shop"]},"noodle_shop":{"links":["street"]}},"npcs":{"wang":{"place":"street"},"cook":{"place":"noodle_shop"}},"mentor":{"npc":"wang","after":"street-hello"}},"scenes":[{"id":"noodle-intro","place":"noodle_shop","npc":"cook","stage":1,"after":["street-hungry"],"requires":{},"repeatable":false,"trustGain":1,"exchanges":[{"id":"greet","slots":{},"expect":{"action":"greet"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!你叫什么名字?","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"},{"start":3,"end":4,"word":"w0028"},{"start":4,"end":5,"word":"w0046"},{"start":5,"end":7,"word":"w0023"},{"start":7,"end":9,"word":"w0049"}],"meaning":"Hello! What's your name?"},"alts":[{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},{"text":"你好吗?","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"},{"start":2,"end":3,"word":"w0051"}],"meaning":"How are you?"}]}}},{"id":"names","slots":{},"expect":{"action":"names"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"我叫小张。你呢?","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"},{"start":2,"end":4,"word":"x0004"},{"start":5,"end":6,"word":"w0028"},{"start":6,"end":7,"word":"w0053"}],"meaning":"My name is Xiao Zhang. And you?"},"reply":{"text":"我叫。老王是我朋友。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"},{"start":4,"end":6,"word":"x0002"},{"start":6,"end":7,"word":"w0099"},{"start":7,"end":8,"word":"w0091"},{"start":8,"end":10,"word":"w0102"}],"meaning":"My name is . Old Wang is my friend."},"alts":[{"text":"我叫小张。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"},{"start":2,"end":4,"word":"x0004"}],"meaning":"My name is Xiao Zhang."},{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}]}}},{"id":"job","slots":{},"expect":{"action":"job"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"工作,好吗?","tokens":[{"start":0,"end":2,"word":"w0084"},{"start":3,"end":4,"word":"w0072"},{"start":4,"end":5,"word":"w0051"}],"meaning":"Want to work here?"},"reply":{"text":"好。","tokens":[{"start":0,"end":1,"word":"w0072"}],"meaning":"OK."},"alts":[{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},{"text":"我叫。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"}],"meaning":"My name is ."}]}}}]},{"id":"noodle-shift","place":"noodle_shop","npc":"cook","stage":1,"after":["noodle-intro"],"requires":{"trust":{"cook":1}},"repeatable":true,"trustGain":1,"exchanges":[{"id":"drink","slots":{"item":"drinks"},"expect":{"action":"fetch","item":"$item"},"hinges":["$item"],"pay":1,"missCost":1,"variants":{"item=tea":{"npc":{"text":"茶。","tokens":[{"start":0,"end":1,"word":"w0133"}],"meaning":"Tea."},"reply":{"text":"好,茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0133"}],"meaning":"OK, tea."}},"item=water":{"npc":{"text":"水。","tokens":[{"start":0,"end":1,"word":"w0108"}],"meaning":"Water."},"reply":{"text":"好,水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0108"}],"meaning":"OK, water."}}}},{"id":"order","slots":{"item":"drinks","count":"numbers_3_5"},"expect":{"action":"serve","item":"$item","count":"$count"},"hinges":["$item","$count"],"pay":3,"missCost":2,"variants":{"count=three|item=tea":{"npc":{"text":"三杯茶。","tokens":[{"start":0,"end":1,"word":"w0003"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}],"meaning":"Three cups of tea."},"reply":{"text":"好,三杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}],"meaning":"OK, three cups of tea."},"rephrase":{"text":"茶。三杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Tea. Three cups."}},"count=three|item=water":{"npc":{"text":"三杯水。","tokens":[{"start":0,"end":1,"word":"w0003"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}],"meaning":"Three cups of water."},"reply":{"text":"好,三杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}],"meaning":"OK, three cups of water."},"rephrase":{"text":"水。三杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0003"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Water. Three cups."}},"count=four|item=tea":{"npc":{"text":"四杯茶。","tokens":[{"start":0,"end":1,"word":"w0060"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}],"meaning":"Four cups of tea."},"reply":{"text":"好,四杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}],"meaning":"OK, four cups of tea."},"rephrase":{"text":"茶。四杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Tea. Four cups."}},"count=four|item=water":{"npc":{"text":"四杯水。","tokens":[{"start":0,"end":1,"word":"w0060"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}],"meaning":"Four cups of water."},"reply":{"text":"好,四杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}],"meaning":"OK, four cups of water."},"rephrase":{"text":"水。四杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0060"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Water. Four cups."}},"count=five|item=tea":{"npc":{"text":"五杯茶。","tokens":[{"start":0,"end":1,"word":"w0020"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0133"}],"meaning":"Five cups of tea."},"reply":{"text":"好,五杯茶。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0133"}],"meaning":"OK, five cups of tea."},"rephrase":{"text":"茶。五杯。","tokens":[{"start":0,"end":1,"word":"w0133"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Tea. Five cups."}},"count=five|item=water":{"npc":{"text":"五杯水。","tokens":[{"start":0,"end":1,"word":"w0020"},{"start":1,"end":2,"word":"x0001"},{"start":2,"end":3,"word":"w0108"}],"meaning":"Five cups of water."},"reply":{"text":"好,五杯水。","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"},{"start":4,"end":5,"word":"w0108"}],"meaning":"OK, five cups of water."},"rephrase":{"text":"水。五杯。","tokens":[{"start":0,"end":1,"word":"w0108"},{"start":2,"end":3,"word":"w0020"},{"start":3,"end":4,"word":"x0001"}],"meaning":"Water. Five cups."}}}}]},{"id":"street-hello","place":"street","npc":"wang","stage":1,"after":[],"requires":{},"repeatable":false,"trustGain":1,"exchanges":[{"id":"hello","slots":{},"expect":{"action":"hello"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"alts":[{"text":"好!","tokens":[{"start":0,"end":1,"word":"w0072"}],"meaning":"Good!"}]}}},{"id":"wang","slots":{},"expect":{"action":"wang"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"老王!","tokens":[{"start":0,"end":2,"word":"x0002"}],"meaning":"Old Wang!"},"reply":{"text":"你好,老王!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"},{"start":3,"end":5,"word":"x0002"}],"meaning":"Hello, Old Wang!"},"alts":[{"text":"老王!","tokens":[{"start":0,"end":2,"word":"x0002"}],"meaning":"Old Wang!"},{"text":"好,老王!","tokens":[{"start":0,"end":1,"word":"w0072"},{"start":2,"end":4,"word":"x0002"}],"meaning":"OK, Old Wang!"}]}}},{"id":"how","slots":{},"expect":{"action":"how"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你好吗?","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"},{"start":2,"end":3,"word":"w0051"}],"meaning":"How are you?"},"reply":{"text":"很好!你好吗?","tokens":[{"start":0,"end":1,"word":"w0087"},{"start":1,"end":2,"word":"w0072"},{"start":3,"end":4,"word":"w0028"},{"start":4,"end":5,"word":"w0072"},{"start":5,"end":6,"word":"w0051"}],"meaning":"Very well! How are you?"},"alts":[{"text":"老王!","tokens":[{"start":0,"end":2,"word":"x0002"}],"meaning":"Old Wang!"},{"text":"你好,老王!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"},{"start":3,"end":5,"word":"x0002"}],"meaning":"Hello, Old Wang!"}]}}},{"id":"name","slots":{},"expect":{"action":"name"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"很好!叫我老王。","tokens":[{"start":0,"end":1,"word":"w0087"},{"start":1,"end":2,"word":"w0072"},{"start":3,"end":4,"word":"w0046"},{"start":4,"end":5,"word":"w0091"},{"start":5,"end":7,"word":"x0002"}],"meaning":"Very well! Call me Old Wang."},"reply":{"text":"你好,老王!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"},{"start":3,"end":5,"word":"x0002"}],"meaning":"Hello, Old Wang!"},"alts":[{"text":"我叫老王。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"},{"start":2,"end":4,"word":"x0002"}],"meaning":"My name is Old Wang."},{"text":"很好!","tokens":[{"start":0,"end":1,"word":"w0087"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Very well!"}]}}},{"id":"ask","slots":{},"expect":{"action":"ask"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你叫什么名字?","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0046"},{"start":2,"end":4,"word":"w0023"},{"start":4,"end":6,"word":"w0049"}],"meaning":"What's your name?"},"reply":{"text":"我叫。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"}],"meaning":"My name is ."},"alts":[{"text":"我叫老王。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"},{"start":2,"end":4,"word":"x0002"}],"meaning":"My name is Old Wang."},{"text":"你好吗?","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"},{"start":2,"end":3,"word":"w0051"}],"meaning":"How are you?"},{"text":"很好!","tokens":[{"start":0,"end":1,"word":"w0087"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Very well!"}]}}},{"id":"bye","slots":{},"expect":{"action":"bye"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"再见,!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye, !"},"reply":{"text":"再见,老王!","tokens":[{"start":0,"end":2,"word":"w0034"},{"start":3,"end":5,"word":"x0002"}],"meaning":"Goodbye, Old Wang!"},"alts":[{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},{"text":"很好!","tokens":[{"start":0,"end":1,"word":"w0087"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Very well!"}]}}}]},{"id":"street-hungry","place":"street","npc":"wang","stage":1,"after":["street-hello"],"requires":{},"repeatable":false,"trustGain":1,"exchanges":[{"id":"hungry","slots":{},"expect":{"action":"hungry"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":",你好吗?","tokens":[{"start":2,"end":3,"word":"w0028"},{"start":3,"end":4,"word":"w0072"},{"start":4,"end":5,"word":"w0051"}],"meaning":", how are you?"},"reply":{"text":"我饿了。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0587"},{"start":2,"end":3,"word":"w0018"}],"meaning":"I'm hungry."},"alts":[{"text":"我叫。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"}],"meaning":"My name is ."},{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"}]}}},{"id":"noodles","slots":{},"expect":{"action":"noodles"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"吃面条吗?","tokens":[{"start":0,"end":1,"word":"w0047"},{"start":1,"end":3,"word":"w0583"},{"start":3,"end":4,"word":"w0051"}],"meaning":"Want some noodles?"},"reply":{"text":"好!","tokens":[{"start":0,"end":1,"word":"w0072"}],"meaning":"Yes, please!"},"alts":[{"text":"我叫。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"}],"meaning":"My name is ."},{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"}]}}},{"id":"like","slots":{},"expect":{"action":"like"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你喜欢面条吗?","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":3,"word":"w0058"},{"start":3,"end":5,"word":"w0583"},{"start":5,"end":6,"word":"w0051"}],"meaning":"Do you like noodles?"},"reply":{"text":"喜欢!我喜欢面条。","tokens":[{"start":0,"end":2,"word":"w0058"},{"start":3,"end":4,"word":"w0091"},{"start":4,"end":6,"word":"w0058"},{"start":6,"end":8,"word":"w0583"}],"meaning":"I do! I like noodles."},"alts":[{"text":"我叫。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"}],"meaning":"My name is ."},{"text":"再见,老王!","tokens":[{"start":0,"end":2,"word":"w0034"},{"start":3,"end":5,"word":"x0002"}],"meaning":"Goodbye, Old Wang!"}]}}},{"id":"there","slots":{},"expect":{"action":"there"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"那里!","tokens":[{"start":0,"end":1,"word":"w0143"},{"start":1,"end":2,"word":"w0145"}],"meaning":"There!"},"reply":{"text":"那里?","tokens":[{"start":0,"end":1,"word":"w0143"},{"start":1,"end":2,"word":"w0145"}],"meaning":"There?"},"alts":[{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},{"text":"我叫。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"}],"meaning":"My name is ."}]}}},{"id":"shop","slots":{},"expect":{"action":"shop"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"那里有饭馆。","tokens":[{"start":0,"end":1,"word":"w0143"},{"start":1,"end":2,"word":"w0145"},{"start":2,"end":3,"word":"w0101"},{"start":3,"end":5,"word":"w0149"}],"meaning":"There's a restaurant there."},"reply":{"text":"好!","tokens":[{"start":0,"end":1,"word":"w0072"}],"meaning":"Good!"},"alts":[{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},{"text":"你好吗?","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"},{"start":2,"end":3,"word":"w0051"}],"meaning":"How are you?"}]}}},{"id":"zhang","slots":{},"expect":{"action":"zhang"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"小张!","tokens":[{"start":0,"end":2,"word":"x0004"}],"meaning":"Xiao Zhang!"},"reply":{"text":"小张?","tokens":[{"start":0,"end":2,"word":"x0004"}],"meaning":"Xiao Zhang?"},"alts":[{"text":"老王?","tokens":[{"start":0,"end":2,"word":"x0002"}],"meaning":"Old Wang?"},{"text":"?","tokens":[],"meaning":"?"}]}}},{"id":"friend","slots":{},"expect":{"action":"friend"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"小张是我朋友。","tokens":[{"start":0,"end":2,"word":"x0004"},{"start":2,"end":3,"word":"w0099"},{"start":3,"end":4,"word":"w0091"},{"start":4,"end":6,"word":"w0102"}],"meaning":"Xiao Zhang is my friend."},"reply":{"text":"好!","tokens":[{"start":0,"end":1,"word":"w0072"}],"meaning":"Good!"},"alts":[{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},{"text":"你好吗?","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"},{"start":2,"end":3,"word":"w0051"}],"meaning":"How are you?"}]}}},{"id":"bye","slots":{},"expect":{"action":"bye"},"hinges":[],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"再见,!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye, !"},"reply":{"text":"谢谢!再见!","tokens":[{"start":0,"end":2,"word":"w0141"},{"start":3,"end":5,"word":"w0034"}],"meaning":"Thank you! Goodbye!"},"alts":[{"text":"你好,老王!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"},{"start":3,"end":5,"word":"x0002"}],"meaning":"Hello, Old Wang!"},{"text":"我叫。","tokens":[{"start":0,"end":1,"word":"w0091"},{"start":1,"end":2,"word":"w0046"}],"meaning":"My name is ."}]}}}]},{"id":"street-practice","place":"street","npc":"wang","stage":1,"after":["street-hello"],"requires":{},"repeatable":true,"trustGain":1,"exchanges":[{"id":"hello","slots":{},"expect":{"action":"greet"},"hinges":["hello"],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}}}},{"id":"polite-1","slots":{"said":"courtesies"},"expect":{"action":"answer","said":"$said"},"hinges":["$said"],"pay":0,"missCost":0,"variants":{"said=hello":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}},"said=thanks":{"npc":{"text":"谢谢!","tokens":[{"start":0,"end":2,"word":"w0141"}],"meaning":"Thank you!"},"reply":{"text":"不客气!","tokens":[{"start":0,"end":3,"word":"w0010"}],"meaning":"You're welcome!"}},"said=sorry":{"npc":{"text":"对不起!","tokens":[{"start":0,"end":3,"word":"w0079"}],"meaning":"Sorry!"},"reply":{"text":"没关系!","tokens":[{"start":0,"end":3,"word":"w0112"}],"meaning":"That's all right!"}}}},{"id":"polite-2","slots":{"said":"courtesies"},"expect":{"action":"answer","said":"$said"},"hinges":["$said"],"pay":0,"missCost":0,"variants":{"said=hello":{"npc":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"},"reply":{"text":"你好!","tokens":[{"start":0,"end":1,"word":"w0028"},{"start":1,"end":2,"word":"w0072"}],"meaning":"Hello!"}},"said=thanks":{"npc":{"text":"谢谢!","tokens":[{"start":0,"end":2,"word":"w0141"}],"meaning":"Thank you!"},"reply":{"text":"不客气!","tokens":[{"start":0,"end":3,"word":"w0010"}],"meaning":"You're welcome!"}},"said=sorry":{"npc":{"text":"对不起!","tokens":[{"start":0,"end":3,"word":"w0079"}],"meaning":"Sorry!"},"reply":{"text":"没关系!","tokens":[{"start":0,"end":3,"word":"w0112"}],"meaning":"That's all right!"}}}},{"id":"bye","slots":{},"expect":{"action":"farewell"},"hinges":["bye"],"pay":0,"missCost":0,"variants":{"":{"npc":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"},"reply":{"text":"再见!","tokens":[{"start":0,"end":2,"word":"w0034"}],"meaning":"Goodbye!"}}}}]}],"reactions":{"wrong-generic":{"text":"不是。","tokens":[{"start":0,"end":1,"word":"w0009"},{"start":1,"end":2,"word":"w0099"}],"meaning":"No."},"wrong-count":{"text":"几杯?","tokens":[{"start":0,"end":1,"word":"w0037"},{"start":1,"end":2,"word":"x0001"}],"meaning":"How many cups?"}},"learnerFtl":"# Text-game UI. Every id here is required by packages/tui (UI_KEYS).\n\nhud = Day { $day } · slot { $slot }/{ $slots } · { $currency }{ $wallet } · { $rank }{ $rentLate ->\n [yes] { \" · rent due\" }\n *[no] {\"\"}\n}\n\nrank-0 = Pidgin\nrank-1 = Getting By\nrank-2 = Conversational\nrank-3 = Fluent\nrank-4 = Silver Tongue\n\nmenu-title = What now?\nmenu-talk = Talk to { $npc }: { $scene }\nmenu-go = Go to { $place }\nmenu-mentor = Ask { $npc } about the language\ncost-slot = { \" · 1 slot\" }\nmenu-sleep = Sleep (end the day)\nmenu-quit = Save and quit\n\nkeys-explore = [{ $keys }] choose · [n] notebook · [q] quit\nkeys-pick = [{ $keys }] reply · [w] word help · [n] notebook\nkeys-tiles = [{ $keys }] add · [⌫] undo · [enter] say · [w] help · [n] notebook\nkeys-help = [{ $keys }] look up · [esc] back\nkeys-help-sentence = [{ $keys }] look up · [s] whole sentence · [esc] back\n\nhelp-title = Which word?\nhelp-sentence = The whole sentence\nhelp-in-replies = In the replies:\nreply-title = Your reply:\ntiles-title = Your reply: tap the words in order, then press [enter].\ntiles-answer = You say:\nyou = You\n\nmismatch = That's not what they asked for.\nrephrased = (slower)\nwallet-change = { $sign }{ $currency }{ $amount } ({ $reason })\nreason-wages = wages\nreason-mixup = mix-up\nreason-food = food\nreason-rent = rent\ntrust-up = { $npc } trusts you a little more ({ $trust }).\nscene-done = { $earned ->\n [0] Done.\n *[other] Done. You earned { $currency }{ $earned }.\n}\nunlocked = New: { $scene }\nrank-up = You're now: { $rank }\nday-ended = Day { $day } is over. You sleep.\nreject-unknown-scene = There's nobody here for that.\nreject-in-scene = Finish the conversation first.\nreject-wrong-place = They're not here.\nreject-locked = They're not ready to talk about that yet.\nreject-no-slots = You're out of time today. Sleep first.\nreject-stale-run = That conversation can't continue. Start it again.\nreject-no-pick = Choose a reply with the number keys.\nreject-bad-choice = There's no reply with that number.\nreject-no-tiles = Build your reply from the tiles.\nreject-bad-tile = There's no tile with that number.\nreject-not-linked = You can't get there from here.\nreject-unknown-word = That word isn't in the dictionary.\nnotice-read-only = Your progress can't be saved on this computer, so this session won't be kept.\nnotice-bad-save = Your save couldn't be read. It was kept as a backup and a new game started.\n\nresume-title = Your games:\nresume-item = { $name }Day { $day } · { $place } · { $currency }{ $wallet } · { $done ->\n [one] 1 scene done\n *[other] { $done } scenes done\n} · last played { $date }\nresume-ask = Which one? (number, or enter to cancel)\nresume-none = No saved games yet, so here's a new one.\nreject-no-mentor = There's nobody here to explain things.\nnote-hint = { $npc } seems to have something to tell you.\nmentor-nothing = { $npc } has nothing new to explain today.\n\nkeys-notebook = [↑↓] scroll · [esc] back\nnotebook-progress = Stage { $stage }: { $known } of { $total } words known · { $heard } heard\nnotebook-empty = Nothing yet. Words you hear are written down here.\nnotebook-elsewhere = Heard elsewhere\nnotebook-notes = Notes\nexport-none = There's no saved game to export yet.\nimport-bad = That line isn't a saved game for this course ({ $reason }).\nimport-done = Added: { $game }. Run the game to continue it.\n\n# Browser page controls (packages/tui-web).\nweb-new = New game\nweb-games = Games\nweb-games-none = No saved games yet.\nweb-export = Export\nweb-export-hint = This line is your whole game. Paste it into Import on another device, or into the terminal game with: npx silver-tongue --import -\nweb-copy = Copy\nweb-copied = Copied\nweb-import = Import\nweb-import-hint = Paste a line from Export, here or from the terminal game. It is added as a new game; nothing is overwritten.\nweb-import-go = Add game\nweb-close = Close\nweb-saved = Your game is saved. You can close this tab, or keep playing.\nreject-bad-name = That name won't work. Use 1 to 20 letters.\nreject-no-name = Tell us your name first.\nname-prompt = Before anything else: what's your name?\nkeys-name = type your name · [enter] done · [⌫] delete\n\n# The story. intro-1, intro-2, … open a new game ($currency, $wallet, $rent are available).\nintro-1 = Your phone is dead, your wallet is gone, and the bus that brought you here left an hour ago.\nintro-2 = You're standing on a street in a Chinese city with { $currency }{ $wallet } in your pocket. You know nobody, and nobody here speaks your language.\nintro-3 = Food costs money. Rent is { $currency }{ $rent }, due at the end of the week. To earn anything you'll need work, and for work you'll need words.\nintro-4 = On a bench by the road, an old man is watching you with open curiosity. Maybe start there.\n\nplace-street = Main Street\nplace-street-desc = Bikes, steam and shouting. An old man sits on a bench by the road. Across it, a noodle shop glows.\nplace-noodle_shop = Noodle Shop\nplace-noodle_shop-desc = Steam everywhere. The cook is shouting orders at nobody in particular.\n\nnpc-wang = Old Wang\nnpc-cook = Cook\n\n# Scene names for the menu; scene-<id>-start and scene-<id>-end are optional narration.\nscene-street-hello = Meet Old Wang\nscene-street-hello-start = The old man pats the bench beside him and points at himself. He seems to have decided you need lessons: he says something, you answer. Stuck? Press [w] to look words up, and [s] there to see what the whole sentence means.\nscene-street-hello-end = Old Wang looks delighted with his new student. He repeats your name a few times, getting it a little wrong each time, and seems very pleased about it.\n\nscene-street-hungry = Talk about food\nscene-street-hungry-start = Your stomach growls, loudly. Old Wang raises an eyebrow.\nscene-street-hungry-end = Old Wang points at the noodle shop, then at you, then mimes rolling up his sleeves. Work first, noodles after.\n\nscene-street-practice = Practise greetings\nscene-street-practice-start = Old Wang shuffles over to make room. Another lesson, then.\nscene-street-practice-end = Old Wang nods, satisfied, and goes back to watching the street.\n\nscene-noodle-intro = Ask about work\nscene-noodle-intro-start = The cook looks you up and down, wiping her hands on her apron. So this is Old Wang's latest project.\nscene-noodle-intro-end = She throws you an apron. You have a job, sort of.\n\nscene-noodle-shift = Work a shift\nscene-noodle-shift-start = Orders fly across the counter. Keep up.\nscene-noodle-shift-end = The rush dies down. The cook counts coins into your hand.\n\n# What a reply does (action-<name>), and on a mix-up what was asked (asked-<name>).\n# Concept values arrive as learner names: $item = \"tea\", $count = \"three\".\naction-fetch = You bring { $item }.\nasked-fetch = They wanted { $item }.\naction-serve = You set down { $count } cups of { $item }.\nasked-serve = They wanted { $count } cups of { $item }.\n\n# Usage notes Old Wang explains (languages/zh/notes.json). note-<id>-title and note-<id>.\n\nnote-hao-ma-title = 好 and 吗\nnote-hao-ma = 好 (hǎo) means \"good\", and on its own it's how you agree: \"OK\". Put 吗 (ma) at the end of a sentence and it becomes a yes-or-no question: 你好吗? is \"How are you?\", literally \"You good?\" The usual answer is 很好 (hěn hǎo), \"very good\"; the 很 is there mostly for rhythm.\n\nnote-lao-xiao-title = 老王 and 小张\nnote-lao-xiao = Friends and neighbours your own age are often called by surname with 老 (lǎo, \"old\") or 小 (xiǎo, \"little\") in front: 老王 for someone a bit older, 小张 for someone younger. From a young stranger to an old man it would be too familiar; you'd say 王大爷, \"Grandpa Wang\". Old Wang told you to call him 老王, which is his way of saying you're friends now.\n\nnote-le-title = 了 for a change\nnote-le = 了 (le) after a word often marks a change: 我饿了 is \"I've got hungry\", \"I'm hungry now\". You'll also hear it in 太好了!, \"great!\", literally \"too good!\"\n\nnote-bukeqi-title = 谢谢 and 不客气\nnote-bukeqi = 谢谢 (xièxie) is \"thank you\". The usual answer is 不客气 (bùkèqi), \"you're welcome\"; it literally means \"don't be polite\": no need to stand on ceremony. Between close friends, people often skip 谢谢 altogether; thanking a friend too much can sound distant.\n\nnote-bei-title = 杯, a measure word\nnote-bei = Chinese doesn't say \"three teas\". A measure word goes between a number and a thing: 三杯茶 is \"three cups of tea\". 杯 (bēi) is the one for cups and glasses. There are many measure words; 个 (gè) is the everyday all-purpose one.\n","conceptNames":{"tea":"tea","water":"water","three":"three","four":"four","five":"five","hello":"Hello","thanks":"Thank you","sorry":"Sorry","bye":"Goodbye"},"stageWords":{"1":["w0001","w0002","w0003","w0004","w0005","w0006","w0007","w0008","w0009","w0010","w0011","w0012","w0013","w0014","w0015","w0016","w0017","w0018","w0019","w0020","w0021","w0022","w0023","w0024","w0025","w0026","w0027","w0028","w0029","w0030","w0031","w0032","w0033","w0034","w0035","w0036","w0037","w0038","w0039","w0040","w0041","w0042","w0043","w0044","w0045","w0046","w0047","w0048","w0049","w0050","w0051","w0052","w0053","w0054","w0055","w0056","w0057","w0058","w0059","w0060","w0061","w0062","w0063","w0064","w0065","w0066","w0067","w0068","w0069","w0070","w0071","w0072","w0073","w0074","w0075","w0076","w0077","w0078","w0079","w0080","w0081","w0082","w0083","w0084","w0085","w0086","w0087","w0088","w0089","w0090","w0091","w0092","w0093","w0094","w0095","w0096","w0097","w0098","w0099","w0100","w0101","w0102","w0103","w0104","w0105","w0106","w0107","w0108","w0109","w0110","w0111","w0112","w0113","w0114","w0115","w0116","w0117","w0118","w0119","w0120","w0121","w0122","w0123","w0124","w0125","w0126","w0127","w0128","w0129","w0130","w0131","w0132","w0133","w0134","w0135","w0136","w0137","w0138","w0139","w0140","w0141","w0142","w0143","w0144","w0145","w0146","w0147","w0148","w0149","w0150"],"2":["w0151","w0152","w0153","w0154","w0155","w0156","w0157","w0158","w0159","w0160","w0161","w0162","w0163","w0164","w0165","w0166","w0167","w0168","w0169","w0170","w0171","w0172","w0173","w0174","w0175","w0176","w0177","w0178","w0179","w0180","w0181","w0182","w0183","w0184","w0185","w0186","w0187","w0188","w0189","w0190","w0191","w0192","w0193","w0194","w0195","w0196","w0197","w0198","w0199","w0200","w0201","w0202","w0203","w0204","w0205","w0206","w0207","w0208","w0209","w0210","w0211","w0212","w0213","w0214","w0215","w0216","w0217","w0218","w0219","w0220","w0221","w0222","w0223","w0224","w0225","w0226","w0227","w0228","w0229","w0230","w0231","w0232","w0233","w0234","w0235","w0236","w0237","w0238","w0239","w0240","w0241","w0242","w0243","w0244","w0245","w0246","w0247","w0248","w0249","w0250","w0251","w0252","w0253","w0254","w0255","w0256","w0257","w0258","w0259","w0260","w0261","w0262","w0263","w0264","w0265","w0266","w0267","w0268","w0269","w0270","w0271","w0272","w0273","w0274","w0275","w0276","w0277","w0278","w0279","w0280","w0281","w0282","w0283","w0284","w0285","w0286","w0287","w0288","w0289","w0290","w0291","w0292","w0293","w0294","w0295","w0296","w0297"],"3":["w0298","w0299","w0300","w0301","w0302","w0303","w0304","w0305","w0306","w0307","w0308","w0309","w0310","w0311","w0312","w0313","w0314","w0315","w0316","w0317","w0318","w0319","w0320","w0321","w0322","w0323","w0324","w0325","w0326","w0327","w0328","w0329","w0330","w0331","w0332","w0333","w0334","w0335","w0336","w0337","w0338","w0339","w0340","w0341","w0342","w0343","w0344","w0345","w0346","w0347","w0348","w0349","w0350","w0351","w0352","w0353","w0354","w0355","w0356","w0357","w0358","w0359","w0360","w0361","w0362","w0363","w0364","w0365","w0366","w0367","w0368","w0369","w0370","w0371","w0372","w0373","w0374","w0375","w0376","w0377","w0378","w0379","w0380","w0381","w0382","w0383","w0384","w0385","w0386","w0387","w0388","w0389","w0390","w0391","w0392","w0393","w0394","w0395","w0396","w0397","w0398","w0399","w0400","w0401","w0402","w0403","w0404","w0405","w0406","w0407","w0408","w0409","w0410","w0411","w0412","w0413","w0414","w0415","w0416","w0417","w0418","w0419","w0420","w0421","w0422","w0423","w0424","w0425","w0426","w0427","w0428","w0429","w0430","w0431","w0432","w0433","w0434","w0435","w0436","w0437","w0438","w0439","w0440","w0441","w0442","w0443","w0444","w0445","w0446","w0447","w0448","w0449","w0450","w0451","w0452","w0453","w0454","w0455","w0456","w0457","w0458","w0459","w0460","w0461","w0462","w0463","w0464","w0465","w0466","w0467","w0468","w0469","w0470","w0471","w0472","w0473","w0474","w0475","w0476","w0477","w0478","w0479","w0480","w0481","w0482","w0483","w0484","w0485","w0486","w0487","w0488","w0489","w0490","w0491","w0492","w0493","w0494","w0495","w0496","w0497","w0498","w0499","w0500","w0501","w0502","w0503","w0504","w0505","w0506","w0507","w0508","w0509","w0510","w0511","w0512","w0513","w0514","w0515","w0516","w0517","w0518","w0519","w0520","w0521","w0522","w0523","w0524","w0525","w0526","w0527","w0528","w0529","w0530","w0531","w0532","w0533","w0534","w0535","w0536","w0537","w0538","w0539","w0540","w0541","w0542","w0543","w0544","w0545","w0546","w0547","w0548","w0549","w0550","w0551","w0552","w0553","w0554","w0555","w0556","w0557","w0558","w0559","w0560","w0561","w0562","w0563","w0564","w0565","w0566","w0567","w0568","w0569","w0570","w0571","w0572","w0573","w0574","w0575","w0576","w0577","w0578","w0579","w0580","w0581","w0582","w0584","w0585","w0586","w0588","w0589","w0590","w0591","w0592","w0593","w0594","w0595"],"4":["w0596","w0597","w0598","w0599","w0600","w0601","w0602","w0603","w0604","w0605","w0606","w0607","w0608","w0609","w0610","w0611","w0612","w0613","w0614","w0615","w0616","w0617","w0618","w0619","w0620","w0621","w0622","w0623","w0624","w0625","w0626","w0627","w0628","w0629","w0630","w0631","w0632","w0633","w0634","w0635","w0636","w0637","w0638","w0639","w0640","w0641","w0642","w0643","w0644","w0645","w0646","w0647","w0648","w0649","w0650","w0651","w0652","w0653","w0654","w0655","w0656","w0657","w0658","w0659","w0660","w0661","w0662","w0663","w0664","w0665","w0666","w0667","w0668","w0669","w0670","w0671","w0672","w0673","w0674","w0675","w0676","w0677","w0678","w0679","w0680","w0681","w0682","w0683","w0684","w0685","w0686","w0687","w0688","w0689","w0690","w0691","w0692","w0693","w0694","w0695","w0696","w0697","w0698","w0699","w0700","w0701","w0702","w0703","w0704","w0705","w0706","w0707","w0708","w0709","w0710","w0711","w0712","w0713","w0714","w0715","w0716","w0717","w0718","w0719","w0720","w0721","w0722","w0723","w0724","w0725","w0726","w0727","w0728","w0729","w0730","w0731","w0732","w0733","w0734","w0735","w0736","w0737","w0738","w0739","w0740","w0741","w0742","w0743","w0744","w0745","w0746","w0747","w0748","w0749","w0750","w0751","w0752","w0753","w0754","w0755","w0756","w0757","w0758","w0759","w0760","w0761","w0762","w0763","w0764","w0765","w0766","w0767","w0768","w0769","w0770","w0771","w0772","w0773","w0774","w0775","w0776","w0777","w0778","w0779","w0780","w0781","w0782","w0783","w0784","w0785","w0786","w0787","w0788","w0789","w0790","w0791","w0792","w0793","w0794","w0795","w0796","w0797","w0798","w0799","w0800","w0801","w0802","w0803","w0804","w0805","w0806","w0807","w0808","w0809","w0810","w0811","w0812","w0813","w0814","w0815","w0816","w0817","w0818","w0819","w0820","w0821","w0822","w0823","w0824","w0825","w0826","w0827","w0828","w0829","w0830","w0831","w0832","w0833","w0834","w0835","w0836","w0837","w0838","w0839","w0840","w0841","w0842","w0843","w0844","w0845","w0846","w0847","w0848","w0849","w0850","w0851","w0852","w0853","w0854","w0855","w0856","w0857","w0858","w0859","w0860","w0861","w0862","w0863","w0864","w0865","w0866","w0867","w0868","w0869","w0870","w0871","w0872","w0873","w0874","w0875","w0876","w0877","w0878","w0879","w0880","w0881","w0882","w0883","w0884","w0885","w0886","w0887","w0888","w0889","w0890","w0891","w0892","w0893","w0894","w0895","w0896","w0897","w0898","w0899","w0900","w0901","w0902","w0903","w0904","w0905","w0906","w0907","w0908","w0909","w0910","w0911","w0912","w0913","w0914","w0915","w0916","w0917","w0918","w0919","w0920","w0921","w0922","w0923","w0924","w0925","w0926","w0927","w0928","w0929","w0930","w0931","w0932","w0933","w0934","w0935","w0936","w0937","w0938","w0939","w0940","w0941","w0942","w0943","w0944","w0945","w0946","w0947","w0948","w0949","w0950","w0951","w0952","w0953","w0954","w0955","w0956","w0957","w0958","w0959","w0960","w0961","w0962","w0963","w0964","w0965","w0966","w0967","w0968","w0969","w0970","w0971","w0972","w0973","w0974","w0975","w0976","w0977","w0978","w0979","w0980","w0981","w0982","w0983","w0984","w0985","w0986","w0987","w0988","w0989","w0990","w0991","w0992","w0993","w0994","w0995","w0996","w0997","w0998","w0999","w1000","w1001","w1002","w1003","w1004","w1005","w1006","w1007","w1008","w1009","w1010","w1011","w1012","w1013","w1014","w1015","w1016","w1017","w1018","w1019","w1020","w1021","w1022","w1023","w1024","w1025","w1026","w1027","w1028","w1029","w1030","w1031","w1032","w1033","w1034","w1035","w1036","w1037","w1038","w1039","w1040","w1041","w1042","w1043","w1044","w1045","w1046","w1047","w1048","w1049","w1050","w1051","w1052","w1053","w1054","w1055","w1056","w1057","w1058","w1059","w1060","w1061","w1062","w1063","w1064","w1065","w1066","w1067","w1068","w1069","w1070","w1071","w1072","w1073","w1074","w1075","w1076","w1077","w1078","w1079","w1080","w1081","w1082","w1083","w1084","w1085","w1086","w1087","w1088","w1089","w1090","w1091","w1092","w1093","w1094","w1095","w1096","w1097","w1098","w1099","w1100","w1101","w1102","w1103","w1104","w1105","w1106","w1107","w1108","w1109","w1110","w1111","w1112","w1113","w1114","w1115","w1116","w1117","w1118","w1119","w1120","w1121","w1122","w1123","w1124","w1125","w1126","w1127","w1128","w1129","w1130","w1131","w1132","w1133","w1134","w1135","w1136","w1137","w1138","w1139","w1140","w1141","w1142","w1143","w1144","w1145","w1146","w1147","w1148","w1149","w1150","w1151","w1152","w1153","w1154","w1155","w1156","w1157","w1158","w1159","w1160","w1161","w1162","w1163","w1164","w1165","w1166","w1167","w1168","w1169","w1170","w1171","w1172","w1173","w1174","w1175","w1176","w1177","w1178","w1179","w1180","w1181","w1182","w1183","w1184","w1185","w1186","w1187","w1188","w1189","w1190","w1191","w1192","w1193"]},"notes":[{"id":"hao-ma","trigger":{"scene":"street-hello"}},{"id":"lao-xiao","trigger":{"word":"x0004"}},{"id":"le","trigger":{"scene":"street-hungry"}},{"id":"bukeqi","trigger":{"word":"w0141"}},{"id":"bei","trigger":{"word":"x0001"}}],"needsName":true}
|
package/dist/silver-tongue.mjs
CHANGED
|
@@ -933,6 +933,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
933
933
|
import { fileURLToPath } from "node:url";
|
|
934
934
|
|
|
935
935
|
// ../core/src/types.ts
|
|
936
|
+
var PLAYER_MARK = "\uE000";
|
|
936
937
|
var REJECT_REASONS = [
|
|
937
938
|
"unknown-scene",
|
|
938
939
|
"in-scene",
|
|
@@ -946,7 +947,9 @@ var REJECT_REASONS = [
|
|
|
946
947
|
"bad-tile",
|
|
947
948
|
"not-linked",
|
|
948
949
|
"unknown-word",
|
|
949
|
-
"no-mentor"
|
|
950
|
+
"no-mentor",
|
|
951
|
+
"bad-name",
|
|
952
|
+
"no-name"
|
|
950
953
|
];
|
|
951
954
|
var WALLET_REASONS = ["wages", "mixup", "food", "rent"];
|
|
952
955
|
|
|
@@ -1120,6 +1123,25 @@ function shuffle(xs, rng) {
|
|
|
1120
1123
|
return a;
|
|
1121
1124
|
}
|
|
1122
1125
|
|
|
1126
|
+
// ../core/src/player.ts
|
|
1127
|
+
var MAX_NAME_LENGTH = 20;
|
|
1128
|
+
function cleanName(name) {
|
|
1129
|
+
const n = name.trim();
|
|
1130
|
+
if (!n || [...n].length > MAX_NAME_LENGTH || /[\p{Cc}\p{Cf}]/u.test(n) || n.includes(PLAYER_MARK)) return void 0;
|
|
1131
|
+
return n;
|
|
1132
|
+
}
|
|
1133
|
+
function personalize(line, name) {
|
|
1134
|
+
if (!line.text.includes(PLAYER_MARK) && !line.meaning?.includes(PLAYER_MARK)) return line;
|
|
1135
|
+
const grow = name.length - PLAYER_MARK.length;
|
|
1136
|
+
const shiftAt = (pos) => pos + grow * [...line.text.slice(0, pos).matchAll(new RegExp(PLAYER_MARK, "g"))].length;
|
|
1137
|
+
return {
|
|
1138
|
+
...line,
|
|
1139
|
+
text: line.text.split(PLAYER_MARK).join(name),
|
|
1140
|
+
tokens: line.tokens.map((t) => ({ ...t, start: shiftAt(t.start), end: shiftAt(t.end) })),
|
|
1141
|
+
...line.meaning !== void 0 ? { meaning: line.meaning.split(PLAYER_MARK).join(name) } : {}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1123
1145
|
// ../core/src/dialogue.ts
|
|
1124
1146
|
function reject(ctx, reason) {
|
|
1125
1147
|
ctx.ev.push({ type: "inputRejected", reason });
|
|
@@ -1155,10 +1177,14 @@ function hear(ctx, line) {
|
|
|
1155
1177
|
if (!rec.first) rec.first = { line: line.text, place: ctx.state.place };
|
|
1156
1178
|
}
|
|
1157
1179
|
}
|
|
1180
|
+
var nameOf = (state2) => state2.player ?? "?";
|
|
1158
1181
|
function speak(ctx, npc, line) {
|
|
1159
|
-
|
|
1160
|
-
|
|
1182
|
+
const said = personalize(line, nameOf(ctx.state));
|
|
1183
|
+
ctx.ev.push({ type: "lineSpoken", npc, line: said });
|
|
1184
|
+
hear(ctx, said);
|
|
1161
1185
|
}
|
|
1186
|
+
var altKey = (n) => `alt:${n}`;
|
|
1187
|
+
var altIndex = (key) => /^alt:\d+$/.test(key) ? Number(key.slice(4)) : void 0;
|
|
1162
1188
|
function pickOptions(ctx, ex, combo) {
|
|
1163
1189
|
const rightKey = comboKey(combo);
|
|
1164
1190
|
const texts = /* @__PURE__ */ new Set([ex.variants[rightKey].reply.text]);
|
|
@@ -1173,24 +1199,36 @@ function pickOptions(ctx, ex, combo) {
|
|
|
1173
1199
|
wrong.push(key);
|
|
1174
1200
|
}
|
|
1175
1201
|
}
|
|
1202
|
+
const alts = ex.variants[rightKey].alts ?? [];
|
|
1203
|
+
for (const n of shuffle(alts.map((_, i) => i), ctx.rng)) {
|
|
1204
|
+
if (wrong.length === 3) break;
|
|
1205
|
+
if (!texts.has(alts[n].text)) {
|
|
1206
|
+
texts.add(alts[n].text);
|
|
1207
|
+
wrong.push(altKey(n));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1176
1210
|
return shuffle([rightKey, ...wrong], ctx.rng);
|
|
1177
1211
|
}
|
|
1178
1212
|
function buildTiles(ctx, ex, combo) {
|
|
1179
1213
|
const rightKey = comboKey(combo);
|
|
1180
1214
|
const pieces2 = tilePieces(ex.variants[rightKey].reply);
|
|
1181
1215
|
const extra = /* @__PURE__ */ new Set();
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
for (const p of tilePieces(
|
|
1216
|
+
const others = shuffle(Object.keys(ex.variants), ctx.rng).filter((k) => k !== rightKey).map((k) => ex.variants[k].reply);
|
|
1217
|
+
for (const l of [...others, ...shuffle(ex.variants[rightKey].alts ?? [], ctx.rng)]) {
|
|
1218
|
+
for (const p of tilePieces(l)) if (!pieces2.includes(p)) extra.add(p);
|
|
1185
1219
|
if (extra.size >= 2) break;
|
|
1186
1220
|
}
|
|
1187
1221
|
return shuffle([...pieces2, ...[...extra].slice(0, 2)], ctx.rng);
|
|
1188
1222
|
}
|
|
1189
|
-
function
|
|
1190
|
-
|
|
1223
|
+
function optionLine(ex, run, key) {
|
|
1224
|
+
const n = altIndex(key);
|
|
1225
|
+
return n === void 0 ? ex.variants[key].reply : ex.variants[comboKey(run.combo)].alts[n];
|
|
1226
|
+
}
|
|
1227
|
+
function optionsEvent(ex, run, name) {
|
|
1228
|
+
return run.mode === "pick" ? { type: "replyOptions", mode: "pick", options: run.options.map((k) => personalize(optionLine(ex, run, k), name)) } : { type: "replyOptions", mode: "tiles", tiles: run.tiles };
|
|
1191
1229
|
}
|
|
1192
1230
|
function emitOptions(ctx, ex) {
|
|
1193
|
-
ctx.ev.push(optionsEvent(ex, ctx.state.run));
|
|
1231
|
+
ctx.ev.push(optionsEvent(ex, ctx.state.run, nameOf(ctx.state)));
|
|
1194
1232
|
}
|
|
1195
1233
|
function describeRun(course2, state2) {
|
|
1196
1234
|
const run = state2.run;
|
|
@@ -1200,8 +1238,8 @@ function describeRun(course2, state2) {
|
|
|
1200
1238
|
if (!run || !scene || !ex || !v) return [];
|
|
1201
1239
|
return [
|
|
1202
1240
|
{ type: "sceneStarted", scene: scene.id, npc: scene.npc },
|
|
1203
|
-
{ type: "lineSpoken", npc: scene.npc, line: v.npc },
|
|
1204
|
-
optionsEvent(ex, run)
|
|
1241
|
+
{ type: "lineSpoken", npc: scene.npc, line: personalize(v.npc, nameOf(state2)) },
|
|
1242
|
+
optionsEvent(ex, run, nameOf(state2))
|
|
1205
1243
|
];
|
|
1206
1244
|
}
|
|
1207
1245
|
function beginExchange(ctx, scene, index) {
|
|
@@ -1209,7 +1247,9 @@ function beginExchange(ctx, scene, index) {
|
|
|
1209
1247
|
const ex = scene.exchanges[index];
|
|
1210
1248
|
const combo = chooseCombo(ctx, ex);
|
|
1211
1249
|
speak(ctx, scene.npc, ex.variants[comboKey(combo)].npc);
|
|
1212
|
-
const
|
|
1250
|
+
const v = ex.variants[comboKey(combo)];
|
|
1251
|
+
const modeWords = [.../* @__PURE__ */ new Set([...hingeWords(ctx, ex, combo), ...v.reply.tokens.map((t) => t.word)])];
|
|
1252
|
+
const states = modeWords.map((w) => {
|
|
1213
1253
|
const rec = ctx.state.words[w];
|
|
1214
1254
|
const st = wordState(rec, ctx.now);
|
|
1215
1255
|
return st === "shaky" && rec && rec.right === 0 ? "met" : st;
|
|
@@ -1231,12 +1271,12 @@ function finishScene(ctx, scene) {
|
|
|
1231
1271
|
ctx.ev.push(...changeWallet(ctx.state, run.earned, "wages"));
|
|
1232
1272
|
ctx.ev.push(...addTrust(ctx.state, scene.npc, scene.trustGain + (run.mixups === 0 ? 1 : 0)));
|
|
1233
1273
|
}
|
|
1234
|
-
function resolve(ctx, scene, ex, chosen, diff, tilesWrong = false) {
|
|
1274
|
+
function resolve(ctx, scene, ex, chosen, diff, tilesWrong = false, other = false, saidWords = []) {
|
|
1235
1275
|
const run = ctx.state.run;
|
|
1236
|
-
const matched = diff.length === 0 && !tilesWrong;
|
|
1276
|
+
const matched = diff.length === 0 && !tilesWrong && !other;
|
|
1237
1277
|
ctx.ev.push({
|
|
1238
1278
|
type: "actionPerformed",
|
|
1239
|
-
action: resolveParams(ex.expect, chosen),
|
|
1279
|
+
action: other ? { action: "other" } : resolveParams(ex.expect, chosen),
|
|
1240
1280
|
expected: resolveParams(ex.expect, run.combo),
|
|
1241
1281
|
matched,
|
|
1242
1282
|
diff,
|
|
@@ -1244,13 +1284,21 @@ function resolve(ctx, scene, ex, chosen, diff, tilesWrong = false) {
|
|
|
1244
1284
|
});
|
|
1245
1285
|
const hinges = hingeWords(ctx, ex, run.combo);
|
|
1246
1286
|
if (matched) {
|
|
1247
|
-
|
|
1287
|
+
const reply3 = ex.variants[comboKey(run.combo)].reply;
|
|
1288
|
+
for (const w of /* @__PURE__ */ new Set([...hinges, ...reply3.tokens.map((t) => t.word)])) setWord(ctx, w, recordRight);
|
|
1289
|
+
for (const t of reply3.tokens) {
|
|
1290
|
+
const rec = ctx.state.words[t.word];
|
|
1291
|
+
if (!rec.first) rec.first = { line: reply3.text, place: ctx.state.place };
|
|
1292
|
+
}
|
|
1248
1293
|
run.earned += ex.pay;
|
|
1249
1294
|
if (run.exchange + 1 < scene.exchanges.length) beginExchange(ctx, scene, run.exchange + 1);
|
|
1250
1295
|
else finishScene(ctx, scene);
|
|
1251
1296
|
return;
|
|
1252
1297
|
}
|
|
1253
|
-
|
|
1298
|
+
const said = new Set(saidWords);
|
|
1299
|
+
const reply2 = ex.variants[comboKey(run.combo)].reply;
|
|
1300
|
+
const missed = reply2.tokens.map((t) => t.word).filter((w) => !said.has(w));
|
|
1301
|
+
for (const w of /* @__PURE__ */ new Set([...hinges, ...missed])) setWord(ctx, w, recordWrong);
|
|
1254
1302
|
run.misses += 1;
|
|
1255
1303
|
run.mixups += 1;
|
|
1256
1304
|
ctx.ev.push(...changeWallet(ctx.state, -ex.missCost, "mixup"));
|
|
@@ -1263,16 +1311,20 @@ function resolve(ctx, scene, ex, chosen, diff, tilesWrong = false) {
|
|
|
1263
1311
|
}
|
|
1264
1312
|
if (run.misses >= 2) {
|
|
1265
1313
|
const v = ex.variants[comboKey(run.combo)];
|
|
1266
|
-
const line = v.rephrase ?? v.npc;
|
|
1314
|
+
const line = personalize(v.rephrase ?? v.npc, nameOf(ctx.state));
|
|
1267
1315
|
ctx.ev.push({ type: "lineRephrased", npc: scene.npc, line, slow: !v.rephrase });
|
|
1268
1316
|
hear(ctx, line);
|
|
1269
1317
|
}
|
|
1270
1318
|
emitOptions(ctx, ex);
|
|
1271
1319
|
}
|
|
1320
|
+
function resolveOther(ctx, scene, ex, said) {
|
|
1321
|
+
resolve(ctx, scene, ex, ctx.state.run.combo, [], false, true, said.tokens.map((t) => t.word));
|
|
1322
|
+
}
|
|
1272
1323
|
function startScene(ctx, id) {
|
|
1273
1324
|
const scene = sceneById(ctx, id);
|
|
1274
1325
|
if (!scene) return reject(ctx, "unknown-scene");
|
|
1275
1326
|
if (ctx.state.run) return reject(ctx, "in-scene");
|
|
1327
|
+
if (ctx.course.needsName && !ctx.state.player) return reject(ctx, "no-name");
|
|
1276
1328
|
if (scene.place !== ctx.state.place) return reject(ctx, "wrong-place");
|
|
1277
1329
|
if (!isAvailable(scene, ctx.state)) return reject(ctx, "locked");
|
|
1278
1330
|
if (ctx.state.slot >= ctx.course.world.slotsPerDay) return reject(ctx, "no-slots");
|
|
@@ -1311,8 +1363,10 @@ function reply(ctx, choice) {
|
|
|
1311
1363
|
if (!cur || !run || run.mode !== "pick") return reject(ctx, "no-pick");
|
|
1312
1364
|
const key = run.options[choice];
|
|
1313
1365
|
if (key === void 0) return reject(ctx, "bad-choice");
|
|
1366
|
+
if (altIndex(key) !== void 0) return resolveOther(ctx, cur.scene, cur.ex, optionLine(cur.ex, run, key));
|
|
1314
1367
|
const chosen = parseComboKey(key);
|
|
1315
|
-
|
|
1368
|
+
const said = cur.ex.variants[key].reply.tokens.map((t) => t.word);
|
|
1369
|
+
resolve(ctx, cur.scene, cur.ex, chosen, actionDiff(cur.ex, chosen, run.combo), false, false, said);
|
|
1316
1370
|
}
|
|
1317
1371
|
function replyTiles(ctx, tiles) {
|
|
1318
1372
|
const run = ctx.state.run;
|
|
@@ -1320,9 +1374,11 @@ function replyTiles(ctx, tiles) {
|
|
|
1320
1374
|
const cur = current(ctx);
|
|
1321
1375
|
if (!cur || !run || run.mode !== "tiles") return reject(ctx, "no-tiles");
|
|
1322
1376
|
if (tiles.some((i) => run.tiles[i] === void 0)) return reject(ctx, "bad-tile");
|
|
1323
|
-
const
|
|
1324
|
-
const
|
|
1325
|
-
|
|
1377
|
+
const chosen = tiles.map((i) => run.tiles[i]);
|
|
1378
|
+
const reply2 = cur.ex.variants[comboKey(run.combo)].reply;
|
|
1379
|
+
const target = tilePieces(reply2).join("");
|
|
1380
|
+
const said = reply2.tokens.filter((t) => chosen.includes(reply2.text.slice(t.start, t.end))).map((t) => t.word);
|
|
1381
|
+
resolve(ctx, cur.scene, cur.ex, run.combo, [], chosen.join("") !== target, false, said);
|
|
1326
1382
|
}
|
|
1327
1383
|
|
|
1328
1384
|
// ../core/src/save.ts
|
|
@@ -1345,7 +1401,7 @@ function isRunShape(x) {
|
|
|
1345
1401
|
if (!["pick", "tiles", "type"].includes(x.mode) || !isStrings(x.options) || !isStrings(x.tiles)) return false;
|
|
1346
1402
|
return ["exchange", "misses", "earned", "mixups"].every((k) => isCount(x[k]));
|
|
1347
1403
|
}
|
|
1348
|
-
var INPUT_TYPES = /* @__PURE__ */ new Set(["goTo", "startScene", "reply", "replyTiles", "helpWord", "visitMentor", "sleep"]);
|
|
1404
|
+
var INPUT_TYPES = /* @__PURE__ */ new Set(["goTo", "startScene", "reply", "replyTiles", "helpWord", "visitMentor", "setName", "sleep"]);
|
|
1349
1405
|
function isLogEntry(x) {
|
|
1350
1406
|
return isObj(x) && isCount(x.t) && isCount(x.day) && isCount(x.slot) && isObj(x.input) && INPUT_TYPES.has(x.input.type);
|
|
1351
1407
|
}
|
|
@@ -1360,7 +1416,12 @@ function runFits(run, course2) {
|
|
|
1360
1416
|
const ex = course2.scenes.find((s) => s.id === run.scene)?.exchanges[run.exchange];
|
|
1361
1417
|
const v = ex?.variants[comboKey(run.combo)];
|
|
1362
1418
|
if (!ex || !v) return false;
|
|
1363
|
-
if (run.mode === "pick")
|
|
1419
|
+
if (run.mode === "pick") {
|
|
1420
|
+
return run.options.every((k) => {
|
|
1421
|
+
const n = altIndex(k);
|
|
1422
|
+
return n === void 0 ? !!ex.variants[k] : !!v.alts?.[n];
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1364
1425
|
return tilesCover(run.tiles, tilePieces(v.reply));
|
|
1365
1426
|
}
|
|
1366
1427
|
function parseSave(raw, course2) {
|
|
@@ -1383,6 +1444,9 @@ function parseSave(raw, course2) {
|
|
|
1383
1444
|
if (!isObj(data.scenesDone) || !allValues(data.scenesDone, isCount)) return { ok: false, reason: "bad-scenesDone" };
|
|
1384
1445
|
if (!isObj(data.words) || !allValues(data.words, isWordRecord)) return { ok: false, reason: "bad-words" };
|
|
1385
1446
|
if (data.run !== null && !isRunShape(data.run)) return { ok: false, reason: "bad-run" };
|
|
1447
|
+
if (data.player !== void 0 && (typeof data.player !== "string" || cleanName(data.player) !== data.player)) {
|
|
1448
|
+
return { ok: false, reason: "bad-player" };
|
|
1449
|
+
}
|
|
1386
1450
|
if (!("notes" in data)) data.notes = { ready: [], read: [] };
|
|
1387
1451
|
if (!("log" in data)) data.log = [];
|
|
1388
1452
|
if (!isObj(data.notes) || !isStrings(data.notes.ready) || !isStrings(data.notes.read)) return { ok: false, reason: "bad-notes" };
|
|
@@ -1440,6 +1504,13 @@ function handle(ctx, input) {
|
|
|
1440
1504
|
case "helpWord":
|
|
1441
1505
|
if (!course2.words[input.word]) return reject(ctx, "unknown-word");
|
|
1442
1506
|
return setWord(ctx, input.word, recordHelp);
|
|
1507
|
+
case "setName": {
|
|
1508
|
+
const name = cleanName(input.name);
|
|
1509
|
+
if (!name) return reject(ctx, "bad-name");
|
|
1510
|
+
state2.player = name;
|
|
1511
|
+
ctx.ev.push({ type: "playerNamed", name });
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1443
1514
|
case "visitMentor":
|
|
1444
1515
|
return visitMentor(ctx);
|
|
1445
1516
|
case "sleep":
|
|
@@ -1607,6 +1678,8 @@ var UI_KEYS = {
|
|
|
1607
1678
|
"keys-tiles": ["keys"],
|
|
1608
1679
|
"keys-help": ["keys"],
|
|
1609
1680
|
"keys-notebook": [],
|
|
1681
|
+
"keys-name": [],
|
|
1682
|
+
"name-prompt": [],
|
|
1610
1683
|
"notebook-progress": ["stage", "known", "total", "heard"],
|
|
1611
1684
|
"notebook-empty": [],
|
|
1612
1685
|
"notebook-elsewhere": [],
|
|
@@ -1631,7 +1704,7 @@ var UI_KEYS = {
|
|
|
1631
1704
|
"notice-bad-save": [],
|
|
1632
1705
|
"notice-read-only": [],
|
|
1633
1706
|
"resume-title": [],
|
|
1634
|
-
"resume-item": ["day", "place", "currency", "wallet", "done", "date"],
|
|
1707
|
+
"resume-item": ["name", "day", "place", "currency", "wallet", "done", "date"],
|
|
1635
1708
|
"resume-ask": [],
|
|
1636
1709
|
"resume-none": [],
|
|
1637
1710
|
"export-none": [],
|
|
@@ -1696,7 +1769,7 @@ function renderScreen(m, cols, rows) {
|
|
|
1696
1769
|
return [
|
|
1697
1770
|
border("\u250C", m.title, "\u2510", "\u2500", cols, m.hud),
|
|
1698
1771
|
...body.map((l) => [{ text: "\u2502 ", dim: true }, ...fitLine(l, inner), { text: " \u2502", dim: true }]),
|
|
1699
|
-
border("\u2514", m.footer, "\u2518", "\u2500", cols)
|
|
1772
|
+
border("\u2514", m.footer, "\u2518", "\u2500", cols, m.footerRight)
|
|
1700
1773
|
];
|
|
1701
1774
|
}
|
|
1702
1775
|
|
|
@@ -1762,6 +1835,7 @@ function startApp(opts) {
|
|
|
1762
1835
|
let notebookFrom = "explore";
|
|
1763
1836
|
let notebookTop = 0;
|
|
1764
1837
|
let resuming = false;
|
|
1838
|
+
let nameInput = "";
|
|
1765
1839
|
const push = (...lines) => {
|
|
1766
1840
|
log = [...log, ...lines].slice(-LOG_LIMIT2);
|
|
1767
1841
|
};
|
|
@@ -1915,6 +1989,9 @@ function startApp(opts) {
|
|
|
1915
1989
|
return [...said, ...extra].slice(0, 9);
|
|
1916
1990
|
}
|
|
1917
1991
|
function prompt(width) {
|
|
1992
|
+
if (mode === "name") {
|
|
1993
|
+
return [[{ text: t("name-prompt"), bold: true }], [{ text: "> " }, { text: nameInput, bold: true }, { text: "_", dim: true }]];
|
|
1994
|
+
}
|
|
1918
1995
|
if (mode === "explore") {
|
|
1919
1996
|
return [[{ text: t("menu-title"), dim: true }], ...menu().map((m, i) => [{ text: `${i + 1}) ${m.label}` }])];
|
|
1920
1997
|
}
|
|
@@ -1942,6 +2019,7 @@ function startApp(opts) {
|
|
|
1942
2019
|
[{ text: `${t("tiles-answer")} `, dim: true }, { text: tileInput.map((i) => tiles[i]).join(""), bold: true }]
|
|
1943
2020
|
];
|
|
1944
2021
|
}
|
|
2022
|
+
const footerRight = opts.version ? `Silver Tongue v${opts.version}` : void 0;
|
|
1945
2023
|
function render() {
|
|
1946
2024
|
const s = core2.state;
|
|
1947
2025
|
const { cols, rows } = term2.size();
|
|
@@ -1960,15 +2038,26 @@ function startApp(opts) {
|
|
|
1960
2038
|
notebookTop = Math.max(0, Math.min(notebookTop, lines.length - bodyRows));
|
|
1961
2039
|
const page = lines.slice(notebookTop, notebookTop + bodyRows);
|
|
1962
2040
|
const prompt2 = [...page, ...Array(bodyRows - page.length).fill([])];
|
|
1963
|
-
term2.write(renderScreen({ title: t(`place-${s.place}`), hud, log: [], prompt: prompt2, footer: t("keys-notebook") }, cols, rows));
|
|
2041
|
+
term2.write(renderScreen({ title: t(`place-${s.place}`), hud, log: [], prompt: prompt2, footer: t("keys-notebook"), footerRight }, cols, rows));
|
|
1964
2042
|
return;
|
|
1965
2043
|
}
|
|
1966
|
-
const [footerId, count] = mode === "explore" ? ["keys-explore", menu().length] : mode === "help" ? [lastLine?.meaning ? "keys-help-sentence" : "keys-help", helpWords().length] : replyMode === "pick" ? ["keys-pick", pickOptions2.length] : ["keys-tiles", tiles.length];
|
|
2044
|
+
const [footerId, count] = mode === "name" ? ["keys-name", 0] : mode === "explore" ? ["keys-explore", menu().length] : mode === "help" ? [lastLine?.meaning ? "keys-help-sentence" : "keys-help", helpWords().length] : replyMode === "pick" ? ["keys-pick", pickOptions2.length] : ["keys-tiles", tiles.length];
|
|
1967
2045
|
const footer = t(footerId, { keys: keyRange(count) });
|
|
1968
|
-
term2.write(renderScreen({ title: t(`place-${s.place}`), hud, log, prompt: prompt(cols - 4), footer }, cols, rows));
|
|
2046
|
+
term2.write(renderScreen({ title: t(`place-${s.place}`), hud, log, prompt: prompt(cols - 4), footer, footerRight }, cols, rows));
|
|
1969
2047
|
}
|
|
1970
2048
|
function press(key) {
|
|
1971
2049
|
if (key.name === "ctrl-c") return opts.quit();
|
|
2050
|
+
if (mode === "name") {
|
|
2051
|
+
const ch = key.text ?? ([...key.name].length === 1 ? key.name : void 0);
|
|
2052
|
+
if (key.name === "return") {
|
|
2053
|
+
if (!core2.send({ type: "setName", name: nameInput }).some((e) => e.type === "inputRejected")) {
|
|
2054
|
+
persist();
|
|
2055
|
+
mode = core2.state.run ? "scene" : "explore";
|
|
2056
|
+
} else push([{ text: t("reject-bad-name"), color: "red" }]);
|
|
2057
|
+
} else if (key.name === "backspace") nameInput = [...nameInput].slice(0, -1).join("");
|
|
2058
|
+
else if (ch && ch >= " " && [...nameInput].length < MAX_NAME_LENGTH) nameInput += ch;
|
|
2059
|
+
return render();
|
|
2060
|
+
}
|
|
1972
2061
|
if (mode === "notebook") {
|
|
1973
2062
|
if (key.name === "escape" || key.name === "n") mode = notebookFrom;
|
|
1974
2063
|
else if (key.name === "down") notebookTop += 1;
|
|
@@ -2029,6 +2118,7 @@ function startApp(opts) {
|
|
|
2029
2118
|
resuming = true;
|
|
2030
2119
|
apply(describeRun(course2, core2.state));
|
|
2031
2120
|
resuming = false;
|
|
2121
|
+
if (course2.needsName && !core2.state.player) mode = "name";
|
|
2032
2122
|
term2.onKey(press);
|
|
2033
2123
|
term2.onResize(render);
|
|
2034
2124
|
render();
|
|
@@ -2068,6 +2158,7 @@ async function decodeSave(line, course2) {
|
|
|
2068
2158
|
function sessionLines(sessions, course2, t, date) {
|
|
2069
2159
|
return sessions.map((s, i) => {
|
|
2070
2160
|
const text = t("resume-item", {
|
|
2161
|
+
name: s.state.player ? `${s.state.player} \xB7 ` : "",
|
|
2071
2162
|
day: s.state.day,
|
|
2072
2163
|
place: t(`place-${s.state.place}`),
|
|
2073
2164
|
currency: course2.world.currency,
|
|
@@ -2089,6 +2180,7 @@ var USAGE = `Usage: silver-tongue [--new | --resume | --export | --import <line>
|
|
|
2089
2180
|
--export print the game you played last as one line of text
|
|
2090
2181
|
--import <line> add a game exported with --export (nothing is overwritten);
|
|
2091
2182
|
use --import - to paste the line on standard input
|
|
2183
|
+
--version print the version
|
|
2092
2184
|
--help show this help`;
|
|
2093
2185
|
function parseFlags(args) {
|
|
2094
2186
|
let parsed;
|
|
@@ -2101,7 +2193,8 @@ function parseFlags(args) {
|
|
|
2101
2193
|
resume: { type: "boolean" },
|
|
2102
2194
|
export: { type: "boolean" },
|
|
2103
2195
|
import: { type: "string" },
|
|
2104
|
-
help: { type: "boolean", short: "h" }
|
|
2196
|
+
help: { type: "boolean", short: "h" },
|
|
2197
|
+
version: { type: "boolean", short: "v" }
|
|
2105
2198
|
}
|
|
2106
2199
|
});
|
|
2107
2200
|
} catch (e) {
|
|
@@ -2110,6 +2203,7 @@ function parseFlags(args) {
|
|
|
2110
2203
|
}
|
|
2111
2204
|
const { values, positionals } = parsed;
|
|
2112
2205
|
if (values.help) return { mode: "help" };
|
|
2206
|
+
if (values.version) return { mode: "version" };
|
|
2113
2207
|
const chosen = ["new", "resume", "export", "import"].filter((k) => values[k] !== void 0 && values[k] !== false);
|
|
2114
2208
|
if (chosen.length > 1) return { mode: "error", message: `use one of ${chosen.map((k) => `--${k}`).join(", ")}, not several` };
|
|
2115
2209
|
const course2 = positionals[0] ? { coursePath: positionals[0] } : {};
|
|
@@ -2138,7 +2232,8 @@ function createNodeTerminal(input = process.stdin, output = process.stdout) {
|
|
|
2138
2232
|
const handlers = [];
|
|
2139
2233
|
input.on("keypress", (str, key) => {
|
|
2140
2234
|
const name = keyName(str, key);
|
|
2141
|
-
|
|
2235
|
+
const text = str && [...str].length === 1 && str >= " " && !key?.ctrl && !key?.meta ? str : void 0;
|
|
2236
|
+
if (name) for (const h of handlers) h(text ? { name, text } : { name });
|
|
2142
2237
|
});
|
|
2143
2238
|
input.on("end", () => {
|
|
2144
2239
|
for (const h of handlers) h({ name: "ctrl-c" });
|
|
@@ -2170,6 +2265,27 @@ function createNodeTerminal(input = process.stdin, output = process.stdout) {
|
|
|
2170
2265
|
};
|
|
2171
2266
|
}
|
|
2172
2267
|
|
|
2268
|
+
// package.json
|
|
2269
|
+
var package_default = {
|
|
2270
|
+
name: "silver-tongue",
|
|
2271
|
+
version: "0.6.0",
|
|
2272
|
+
description: "Learn a language by living in it: a text game for the terminal.",
|
|
2273
|
+
type: "module",
|
|
2274
|
+
repository: { type: "git", url: "git+https://github.com/jamil314/silver-tongue.git", directory: "packages/tui-node" },
|
|
2275
|
+
license: "MIT",
|
|
2276
|
+
engines: { node: ">=22" },
|
|
2277
|
+
bin: { "silver-tongue": "dist/silver-tongue.mjs" },
|
|
2278
|
+
files: ["dist"],
|
|
2279
|
+
scripts: {
|
|
2280
|
+
bundle: 'esbuild src/main.ts --bundle --platform=node --format=esm --outfile=dist/silver-tongue.mjs --banner:js="#!/usr/bin/env node" && mkdir -p dist/courses/zh-china-en && cp ../../dist/courses/zh-china-en/course.json dist/courses/zh-china-en/'
|
|
2281
|
+
},
|
|
2282
|
+
devDependencies: {
|
|
2283
|
+
"@silver-tongue/core": "*",
|
|
2284
|
+
"@silver-tongue/tui": "*",
|
|
2285
|
+
esbuild: "^0.28.2"
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
|
|
2173
2289
|
// src/sessions.ts
|
|
2174
2290
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
2175
2291
|
import { join } from "node:path";
|
|
@@ -2271,6 +2387,10 @@ if (major < 22) {
|
|
|
2271
2387
|
process.exit(1);
|
|
2272
2388
|
}
|
|
2273
2389
|
var flags = parseFlags(process.argv.slice(2));
|
|
2390
|
+
if (flags.mode === "version") {
|
|
2391
|
+
console.log(package_default.version);
|
|
2392
|
+
process.exit(0);
|
|
2393
|
+
}
|
|
2274
2394
|
if (flags.mode === "help") {
|
|
2275
2395
|
console.log(USAGE);
|
|
2276
2396
|
process.exit(0);
|
|
@@ -2363,6 +2483,7 @@ startApp({
|
|
|
2363
2483
|
term,
|
|
2364
2484
|
now: Date.now,
|
|
2365
2485
|
notice,
|
|
2486
|
+
version: package_default.version,
|
|
2366
2487
|
save: readOnly ? void 0 : (s) => writeSave(savePath, s),
|
|
2367
2488
|
quit: () => bail(0)
|
|
2368
2489
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "silver-tongue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Learn a language by living in it: a text game for the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": { "type": "git", "url": "git+https://github.com/jamil314/silver-tongue.git", "directory": "packages/tui-node" },
|