~cadence/PE-DIA

01c95c9f010241c60dfac153051d8e1b6cc1c210 — Cadence Ember 1 year, 9 months ago c4389bd
Implement text effects
2 files changed, 167 insertions(+), 9 deletions(-)

M parser.js
M processor.js
M parser.js => parser.js +24 -0
@@ 66,6 66,30 @@ class Parser {
	}

	/**
	 * Get the text between the next matching pair of brackets. It is recommended to put the cursor just before the open bracket.
	 * @param {string} open
	 * @param {string} close
	 * @param {number} level Number of brackets that are currently open
	 */
	getBrackets(open, close, level = 0) {
		let start, end
		while (true) {
			const char = this.slice(1, true)
			if (char === open) {
				level++
				if (!start) start = this.cursor
			} else if (char === close) {
				level--
				if (level === 0) {
					end = this.cursor - 1
					break
				}
			}
		}
		return this.string.slice(start, end)
	}

	/**
	 * Get a number of chars from the buffer.
	 * @param {number} length Number of chars to get
	 * @param {boolean} [move] Whether to update the cursor

M processor.js => processor.js +143 -9
@@ 13,10 13,10 @@ const interchanges = []
	(int) number of interchange text lines
	[ for each interchange text line: ]
		(string) text line
	(int) number of interchange text commands
	[ for each interchange text command: ]
		(string) text command type
		(int) text command position
	(int) number of interchange text effects
	[ for each interchange text effect: ]
		(string) text effect type
		(int) text effect position
	(int) number of interchange replies
	[ for each interchange reply: ]
		(string) reply text


@@ 69,15 69,84 @@ class Interchange {
	 * @param {number} index
	 * @param {string} label
	 * @param {string} text
	 * @param {{effect: string, start: number, end: number}[]} effects
	 */
	constructor(index, label, text) {
	constructor(index, label, text, effects) {
		this.index = index
		this.label = label
		this.text = text
		this.effects = effects
		/** @type {Choice[]} */
		this.choices
	}

	static parseTextEffects(text) {
		const formalEffects = new Map([
			["r", "CRED"],
			["o", "CORA"],
			["y", "CYEL"],
			["g", "CGRE"],
			["b", "CCYA"],
			["i", "CBLU"],
			["v", "CPUR"],
			["w", "CWHI"],
			["grey", "CGRY"],
			["gray", "CGRY"],
			["rainbow", "CRAI"],
			["flash", "CBLI"],
			["warn", "CWAR"],

			["shiver", "ESHI"],
			["jitter", "EJIT"],
			["wave", "EWAV"],
			["bounce", "EBOU"],
			["zoom", "EZOO"],
			["zoomshake", "EZSH"],
			["circle", "ECIR"],
			["8", "EEIG"],
			["eight", "EEIG"],
			["updown", "EUPD"],
			["leftright", "ELER"]
		])

		/** @type {{effect: string, start: number}[]} */
		const openEffects = []
		/** @type {{effect: string, start: number, end: number}[]} */
		const closedEffects = []
		const parser = new Parser(text)
		let noEffectText = ""
		while (true) {
			// get the bit before the next opening bracket
			noEffectText += parser.get({split: "["})
			// console.error("no effect text:", noEffectText)
			// test if we've collected everything yet
			if (!parser.hasRemaining()) {
				if (parser.substore.length) {
					parser.popSubstore()
					closedEffects.push({...openEffects.pop(), end: noEffectText.length})
					continue
				} else {
					break
				}
			}
			// nope, there is actually a bracket here. collect its contents.
			parser.cursor--
			let inside = parser.getBrackets("[", "]")
			// console.error("new substore data found:", inside)
			parser.pushSubstore(inside)
			// the next word is the command word for this bracket sequence. it's the name of the effect
			const myEffectName = parser.get({split: " "})
			const formalEffectName = formalEffects.get(myEffectName)
			if (!formalEffectName) {
				throw new Error(`No text effect named ${myEffectName}`)
			}
			openEffects.push({effect: formalEffectName, start: noEffectText.length})
			// console.error(`effect ${effect} started. open effects:`, openEffects)
		}
		// console.error(closedEffects)
		return {text: noEffectText, effects: closedEffects}
	}

	static CreateFromParser(index, parser) {
		let label = null
		let text = ""


@@ 92,6 161,9 @@ class Interchange {
				break
			}
		}

		let effects
		;({text, effects} = Interchange.parseTextEffects(text))
		text = text.trimEnd()
		let lines = text.split("\n")
		lines = lines.map(line => wordwrap(line, 38))


@@ 106,21 178,83 @@ class Interchange {
			}
		}

		const interchange = new Interchange(index, label, text)
		const interchange = new Interchange(index, label, text, effects)
		let structuredChoices = choices.map((choice, index) => Choice.CreateFromLine(interchange, index, choice))
		interchange.choices = structuredChoices
		return interchange
	}

	serialiseEffects() {
		const effectGroups = new Map([
			["C", {
				prefix: "C",
				revert: "CWHI",
				effects: []
			}],
			["E", {
				prefix: "E",
				revert: "ENON",
				effects: []
			}]
		])

		// split the effects into start and end, and put them into the groups
		for (const effect of this.effects) {
			const prefix = effect.effect.slice(0, 1)
			const group = effectGroups.get(prefix)
			group.effects.push({type: "open", effect: effect.effect, position: effect.start})
			group.effects.push({type: "close", effect: effect.effect, position: effect.end})
		}

		// serialise each group by actually applying the effects stack. remember to go back to the previous effects when the current effect expires.
		/** @type {{effect: string, position: number}[]} */
		let result = []
		for (const group of effectGroups.values()) {
			const stack = []
			group.effects.sort((a, b) => a.position - b.position)
			for (const effect of group.effects) {
				// adjust position due to 38 character internal lines
				while (this.text[effect.position] === "\n") effect.position += 1
				effect.position += this.text.slice(0, effect.position).split("\n").slice(0, -1).reduce((result, line) => result + (37 - line.length), 0)

				if (effect.type === "open") {
					stack.push(effect)
					result.push({effect: effect.effect, position: effect.position})
					// console.error("effect opened", effect)
				} else {
					stack.pop()
					if (stack.length) {
						result.push({effect: stack[stack.length-1].effect, position: effect.position})
						// console.error("current effect closed, new effect is", stack[stack.length-1].effect)
					} else {
						result.push({effect: group.revert, position: effect.position})
						// console.error("current effect closed, reverted to original", group.revert)
					}
				}
			}
		}
		result.sort((a, b) => a.position - b.position)
		// console.error(result)

		return result
	}

	/** @param {MSStream} msstream */
	writeTo(msstream) {
		// text
		const lines = this.text.split("\n")
		msstream.writeInt(lines.length)
		for (const line of lines) {
			msstream.writeString(line)
		}
		// later: text commands
		msstream.writeInt(0)
		// text effects
		const serialEffects = this.serialiseEffects()
		msstream.writeInt(serialEffects.length)
		for (const effect of serialEffects) {
			msstream.writeString(effect.effect)
			msstream.writeInt(effect.position)
		}
		// choices
		msstream.writeInt(this.choices.length)
		for (const choice of this.choices) {
			choice.writeTo(msstream)


@@ 146,7 280,7 @@ class Choice {
		let text = line
		const endingMatch = line.match(/^(.*) \/([^/]+)$/)
		/** @type {{gotoType: string, gotoLocation: any, end: boolean, command: [number, number, number, number, number]}} */
		const attributes = {gotoType: "relative", gotoLocation: "next", end: false, command: [0, 0, 0, 0, 0]}
		const attributes = {gotoType: "relative", gotoLocation: "next", end: false, command: [0, -1, -1, -1, -1]}
		if (endingMatch) {
			text = endingMatch[1]
			const parser = new Parser(endingMatch[2])