-
Notifications
You must be signed in to change notification settings - Fork 2
feat(sage): cablea el sabio como NPC + leyenda de glifos (#11) #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ const { Field, Y_SCALE } = require('./field.js') | |
| const render = require('./render.js') | ||
| const { parse } = require('./script.js') | ||
| const portraits = require('./portraits.js') | ||
| const { SageSession } = require('./sage-npc.js') | ||
| const { ARENA } = require('./world.js') | ||
| const CONTENT = require('./content.js') | ||
|
|
||
|
|
@@ -55,6 +56,8 @@ const COMBAT_TURN_TICKS = 6 | |
|
|
||
| /** Where the player's strategy lives. */ | ||
| const SCRIPT_PATH = 'script.txt' | ||
| /** Sage input cap: mirrors the translator's own limit (issue #11). */ | ||
| const SAGE_MAX_INPUT = 120 | ||
|
|
||
| const DEFAULT_SCRIPT = [ | ||
| '// tu estrategia. se relee sola mientras peleas.', | ||
|
|
@@ -222,6 +225,10 @@ class Runa { | |
| this.scriptSource = '' | ||
| this.scriptMtime = 0 | ||
| this.scriptErrors = [] | ||
| /** Sage conversation state (issue #11). */ | ||
| this.sageSession = null | ||
| this.sageBuffer = '' | ||
| this.legendOpen = false | ||
|
|
||
| // Presence is built here and started in init(). A constructor that opens a | ||
| // socket is a class that cannot be built in a test, and every test in this | ||
|
|
@@ -264,6 +271,52 @@ class Runa { | |
| * @param {boolean} [force] | ||
| * @returns {boolean} whether it reloaded | ||
| */ | ||
| /** Open the sage conversation (used by the NPC action and tests). */ | ||
| startSage() { | ||
| this.sageSession = new SageSession( | ||
| null, | ||
| () => { | ||
| try { return fs.readFileSync(SCRIPT_PATH, 'utf8') } catch { return '' } | ||
| }, | ||
| (next) => fs.writeFileSync(SCRIPT_PATH, next), | ||
| (lines) => { | ||
| for (const line of lines || []) this.say(line) | ||
| if (this.activeSlot) this.loadScript(true) | ||
| this.sageSession = null | ||
| } | ||
| ).start() | ||
| for (const line of this.sageSession.say || []) this.say(line) | ||
| return this.sageSession | ||
| } | ||
|
|
||
| /** | ||
| * Sage mode collects one sentence via the shared text input. Keys arrive | ||
| * here from the main handler while `sageSession` is active; enter submits, | ||
| * escape closes (handled by the caller). | ||
| * @param {object} msg | ||
| * @returns {string|null} same contract as other handlers: null keeps playing | ||
| */ | ||
| sageKey(msg) { | ||
| const ch = typeof msg === 'string' ? msg : (msg && msg.key) || '' | ||
| if (!ch) return null | ||
| if (ch === 'enter' || ch === 'return') { | ||
| const sentence = this.sageBuffer.trim() | ||
| this.sageBuffer = '' | ||
| if (sentence) this.sageSession.ask(sentence) | ||
| else this.sageSession.close('el sabio espera una frase') | ||
| return null | ||
| } | ||
| if (ch === 'backspace' || ch === 'delete') { | ||
| this.sageBuffer = this.sageBuffer.slice(0, -1) | ||
| return render.sagePrompt(this.sageBuffer) | ||
| } | ||
| if (ch.length === 1) { | ||
|
Comment on lines
+299
to
+313
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if (this.sageBuffer.length < SAGE_MAX_INPUT) this.sageBuffer += ch | ||
| return render.sagePrompt(this.sageBuffer) | ||
| } | ||
| return render.sagePrompt(this.sageBuffer) | ||
| } | ||
|
|
||
| loadScript(force = false) { | ||
| let stat = null | ||
| try { | ||
|
|
@@ -1092,6 +1145,30 @@ class Runa { | |
| return null | ||
| } | ||
|
|
||
| // Map glyph legend (issue #11): the exported LEGEND finally has a reader. | ||
| if (key.matches(msg, 'l') && !this.field) { | ||
| this.legendOpen = true | ||
| return null | ||
| } | ||
| if (this.legendOpen) { | ||
| if (key.matches(msg, 'escape', 'l', 'enter')) { | ||
| this.legendOpen = false | ||
| this.say('cerraste la leyenda de glifos') | ||
| } else { | ||
| this.say('esc o l para cerrar la leyenda') | ||
| } | ||
| return null | ||
| } | ||
|
|
||
|
Comment on lines
+1148
to
+1162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| // The sage conversation swallows printable input while open (issue #11). | ||
| if (this.sageSession && this.sageSession.active) { | ||
| if (key.matches(msg, 'escape')) { | ||
| this.sageSession.close('el sabio asiente y espera') | ||
| return null | ||
| } | ||
| return this.sageKey(msg) | ||
| } | ||
|
|
||
| if (this.field && this.field.combat) { | ||
| if (key.matches(msg, 'space', 'enter', 'f')) this.advanceCombat() | ||
| else if (key.matches(msg, 't')) this.returnToCity() | ||
|
|
@@ -1224,6 +1301,21 @@ class Runa { | |
| this.restAtTavern() | ||
| break | ||
|
|
||
| case 'sage': | ||
| this.sageSession = new SageSession( | ||
| null, | ||
| () => { | ||
| try { return fs.readFileSync(SCRIPT_PATH, 'utf8') } catch { return '' } | ||
| }, | ||
| (next) => fs.writeFileSync(SCRIPT_PATH, next), | ||
| (lines) => { | ||
| for (const line of lines || []) this.say(line) | ||
| if (this.activeSlot) this.loadScript(true) | ||
| this.sageSession = null | ||
| } | ||
| ).start() | ||
| break | ||
|
Comment on lines
+1304
to
+1317
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚨 Bug: Sage NPC is unreachable — interactNpc ignores kind 'sage'The sabio is placed as a CITY_NPC with Route NPC 'sage' action to startSage() inside interactNpc.:
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎 |
||
|
|
||
| default: | ||
| this.say('no se que es esto') | ||
| } | ||
|
|
@@ -1574,6 +1666,23 @@ class Runa { | |
|
|
||
| const city = MAPS[this.walker.mapId] | ||
| const nearby = this.nearbyNpc(2) | ||
| // Legend overlay (issue #11): LEGEND was exported but never shown anywhere. | ||
| if (this.legendOpen) { | ||
| const { LEGEND } = require('./map.js') | ||
| const rows = Object.entries(LEGEND || {}).map(([glyph, meaning]) => `${glyph} ${meaning}`) | ||
|
Comment on lines
+1670
to
+1672
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const width = Math.min(this.width - 4, 46) | ||
| const height = Math.max(6, Math.min(rows.length + 4, this.height - 4)) | ||
| return render.compose({ | ||
| width: this.width, | ||
| height: this.height, | ||
| title: 'runa', | ||
| mainCaption: 'glifos del mapa', | ||
| main: render.box(`leyenda de glifos\n\n${rows.join('\n')}`, width, height), | ||
| stats: base.stats, | ||
| log: base.log, | ||
| footer: 'esc o l cerrar la leyenda' | ||
| }) | ||
| } | ||
| return render.mapScreen({ | ||
| ...base, | ||
| place: nearby | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /** | ||
| * The Sage NPC (issue #11): wires the orphaned `lib/sage.js` translator into | ||
| * the game so natural-language rules reach `script.txt` without a text editor. | ||
| * | ||
| * Flow: player talks to the sage (`e`) -> game enters sage mode -> | ||
| * bare-tui textinput collects one sentence -> Sage.ask() translates it -> | ||
| * on success the returned rule block is appended to script.txt and picked up | ||
| * by the normal script reload path. | ||
| */ | ||
|
|
||
| const fs = require('bare-fs') | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Quality: Unused fs import in sage-npc.js
Was this helpful? React with 👍 / 👎 |
||
|
|
||
| /** | ||
| * One conversation with the sage. The main loop feeds keystrokes in through | ||
| * handle() while active, and receives the closing lines via onDone. | ||
| */ | ||
| class SageSession { | ||
| constructor(createInput, readScript, writeScript, onDone) { | ||
| this.createInput = createInput | ||
| this.readScript = readScript | ||
| this.writeScript = writeScript | ||
| this.onDone = onDone | ||
| this.say = [] | ||
| this.closed = false | ||
| this.lines = [] | ||
| } | ||
|
|
||
| start() { | ||
| const { Sage } = require('./sage.js') | ||
| this.sage = new Sage() | ||
| this.say.push('el sabio te escucha. decile una regla en palabras.') | ||
| return this | ||
| } | ||
|
|
||
| get active() { | ||
| return !this.closed | ||
| } | ||
|
|
||
| /** | ||
| * Feed one sentence to the sage. | ||
| * @param {string} sentence | ||
| */ | ||
| ask(sentence) { | ||
| if (this.closed) return false | ||
| const result = this.sage.ask(String(sentence ?? '')) | ||
| for (const line of result.say || []) this.say.push(line) | ||
|
|
||
| if (result.ok && result.script) { | ||
| try { | ||
| const current = String(this.readScript() ?? '') | ||
| const next = | ||
| current.trimEnd() + (current.trim() ? '\n' : '') + result.script + '\n' | ||
| this.writeScript(next) | ||
| this.say.push('la regla quedo escrita en tu script') | ||
| } catch { | ||
| this.say.push('no pude escribir el script; intenta con ? y tu editor') | ||
| } | ||
| } else if (result.examples && result.examples.length) { | ||
| this.say.push(`probá: ${result.examples[0]}`) | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| close(message) { | ||
| if (message && this.say[this.say.length - 1] !== message) this.say.push(message) | ||
| this.closed = true | ||
| if (this.onDone) this.onDone(this.say) | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| module.exports = { SageSession } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Quality: NPC 'sage' path doesn't show the sage's opening lines
The
case 'sage'block inenter()builds and.start()s the SageSession but, unlikestartSage(), never iteratesthis.sageSession.sayto push the intro lines to the log. If the sage is entered through this path the player gets no on-screen prompt that the sage is listening. Prefer callingstartSage()from both entry points to avoid the duplicated (and divergent) session-construction logic.Was this helpful? React with 👍 / 👎