/* global TokenType, ViewActivity, TextUtils, Variables, Operator */ function Parser(tokens) { this.tokens = tokens; this.tokensCount = tokens.length; this.position = 0; this.lastPosition = 0; this.labels = {}; /** Оптимизация, чтобы каждый раз не искать endmenu/endif, * если их попросту нет в необработанном сценарии */ this.hasEndMenu = false; this.hasEndIf = false; Variables.init(); } Parser.prototype.EOF = new Token("", TokenType.EOF); Parser.prototype.getInstance = function () { return this; }; Parser.prototype.setPosition = function (position) { this.position = position; this.next(); }; Parser.prototype.jumpLabel = function (label) { if (label in this.labels) { this.position = this.labels[label]; } }; Parser.prototype.next = function () { this.lastPosition = this.position; var terminal = false; var counter = 0; do { try { terminal = this.statement(); } catch (re) { console.log("Parser.next() " + re); if (this.tokens.length === 0) return; } // антизацикливание counter++; if (counter >= 1000) { this.position++; counter = 0; } } while (!terminal); }; Parser.prototype.findFrom = function(from, which, step) { var pos = from; while (true) { pos += step; if (pos < 0 || pos >= this.tokensCount) break; var token = this.tokens[pos]; if (which === token.getType()) return pos; } // Возвращаем текущее значение во избежание лишних проверок. return from; }; Parser.prototype.statement = function() { var token = this.get(0); if (this.match(token, TokenType.COMMAND)) return this.command(); if (this.match(token, TokenType.SCENE)) return this.scene(); if (this.match(token, TokenType.PLAY)) return this.play(); if (this.match(token, TokenType.QUEUE)) return this.queue(); if (this.match(token, TokenType.STOP)) return this.stop(); if (this.match(token, TokenType.SHOW)) return this.show(); if (this.match(token, TokenType.HIDE)) return this.hide(); if (this.match(token, TokenType.JUMP)) return this.jump(); if (this.match(token, TokenType.IF)) return this.ifStatement(); if (this.lookMatch(1, TokenType.COLON)) { // menu: if (this.match(token, TokenType.MENU)) return this.menu(); // Остаток от меню выбора. Пропускаем до появления ENDMENU. if (this.match(token, TokenType.TEXT)) { if (this.hasEndMenu) this.position += this.skipMenu(); return false; } // Остаток от условного выражения. Пропускаем до появления ENDIF. if (this.match(token, TokenType.ELSE)) { if (this.hasEndIf) this.position += this.skipIf(); return false; } } // Текст с именем автора реплики. if (this.lookMatch(1, TokenType.TEXT) && this.match(token, TokenType.WORD)) { var whoid = token.getText(); ViewActivity.getInstance().text(whoid, this.consume(TokenType.TEXT).getText()); return true; } // Обычный текст. if (this.match(token, TokenType.TEXT)) { ViewActivity.getInstance().text(token.getText()); return true; } if (this.match(token, TokenType.EOF)) { ViewActivity.getInstance().finish(); return true; } if (this.match(token, TokenType.WINDOW)) { if (this.match(TokenType.SHOW)) return ViewActivity.getInstance().windowShow(this.matchWithEffect()); else if (this.match(TokenType.HIDE)) return ViewActivity.getInstance().windowHide(this.matchWithEffect()); return false; } if (!TextUtils.isEmpty(this.matchWithEffect())) return false; this.position++; return false; }; Parser.prototype.command = function() { var token = this.get(0); if (this.match(token, TokenType.RENPY_PAUSE)) { this.consume(TokenType.LPAREN); var pause = this.consumeDouble(); var hard = false; if (!this.lookMatch(0, TokenType.RPAREN)) { this.consume(TokenType.WORD); // hard this.consume(TokenType.EQ); hard = this.consumeBoolean(); } this.consume(TokenType.RPAREN); ViewActivity.getInstance().pause(Math.floor(1000 * pause), hard); return true; } if (this.match(token, TokenType.RENPY_SAY)) { this.consume(TokenType.LPAREN); var whoid = this.consume(TokenType.WORD).getText(); // TODO: this.consume(TokenType.COMMA) var text = this.consume(TokenType.TEXT).getText(); // TODO: this.consume(TokenType.COMMA) this.consume(TokenType.WORD); // interact this.consume(TokenType.EQ); var interact = this.consumeBoolean(); this.consume(TokenType.RPAREN); ViewActivity.getInstance().text(whoid, text); return interact; } if (this.match(token, TokenType.PERSISTENT_SPRITE_TIME)) { this.consume(TokenType.EQ); this.consume(TokenType.TEXT); return false; } if (this.match(token, TokenType.PROLOG_TIME) || this.match(token, TokenType.DAY_TIME) || this.match(token, TokenType.SUNSET_TIME) || this.match(token, TokenType.NIGHT_TIME)) { this.consume(TokenType.LPAREN); this.consume(TokenType.RPAREN); return false; } if (this.match(token, TokenType.MAKE_NAMES_KNOWN) || this.match(token, TokenType.MAKE_NAMES_UNKNOWN)) { this.consume(TokenType.LPAREN); this.consume(TokenType.RPAREN); if (token.getType() === TokenType.MAKE_NAMES_KNOWN) { ViewActivity.getInstance().makeNamesKnown(); } else ViewActivity.getInstance().makeNamesUnknown(); return false; } if (this.match(token, TokenType.SET_NAME)) { this.consume(TokenType.LPAREN); var whoid = this.consume(TokenType.TEXT).getText(); // TODO: this.consume(TokenType.COMMA) var name = this.consume(TokenType.TEXT).getText(); this.consume(TokenType.RPAREN); ViewActivity.getInstance().meet(whoid, name); return false; } // Карта if (this.match(token, TokenType.DISABLE_ALL_ZONES)) { this.consume(TokenType.LPAREN); this.consume(TokenType.RPAREN); ViewActivity.getInstance().disableAllZones(); return false; } if (this.match(token, TokenType.DISABLE_CURRENT_ZONE)) { this.consume(TokenType.LPAREN); this.consume(TokenType.RPAREN); ViewActivity.getInstance().disableCurrentZone(); return false; } if (this.match(token, TokenType.RESET_ZONE)) { this.consume(TokenType.LPAREN); var zone = this.consume(TokenType.TEXT).getText(); this.consume(TokenType.RPAREN); ViewActivity.getInstance().resetZone(zone); return false; } if (this.match(token, TokenType.SET_ZONE)) { this.consume(TokenType.LPAREN); var zone = this.consume(TokenType.TEXT).getText(); // TODO: this.consume(TokenType.COMMA) var label = this.consume(TokenType.TEXT).getText(); this.consume(TokenType.RPAREN); ViewActivity.getInstance().setZone(zone, label); return false; } if (this.match(token, TokenType.SHOW_MAP)) { this.consume(TokenType.LPAREN); this.consume(TokenType.RPAREN); ViewActivity.getInstance().showMap(); return true; } if (this.match(token, TokenType.WORD)) { if (this.match(TokenType.EQ)) { // variable = expression Variables.setVariable(token.getText(), this.expression().eval()); return false; } if (this.lookMatch(1, TokenType.EQ) && this.match(TokenType.PLUS)) { // variable += expression this.consume(TokenType.EQ); var varValue = Variables.getVariable(token.getText()); Variables.setVariable(token.getText(), varValue + this.expression().eval()); return false; } if (this.lookMatch(1, TokenType.EQ) && this.match(TokenType.MINUS)) { // variable -= expression this.consume(TokenType.EQ); var varValue = Variables.getVariable(token.getText()); Variables.setVariable(token.getText(), varValue - this.expression().eval()); return false; } } return false; }; Parser.prototype.scene = function() { var type = ""; if (this.match(TokenType.BG)) type = "bg"; else if (this.match(TokenType.CG)) type = "cg"; else if (this.match(TokenType.ANIM)) type = "anim"; var name = this.consume(TokenType.WORD).getText(); var effect = this.matchWithEffect(); ViewActivity.getInstance().background(type, name, effect); return true; }; Parser.prototype.show = function() { var whoid = this.consume(TokenType.WORD).getText(); var params = ""; // emotion? cloth? distance? while (this.lookMatch(0, TokenType.WORD)) { var text = this.consume(TokenType.WORD).getText(); params += text; if (text.equals("close") || text.equals("far")) break; } // Положение (left, cleft, ...) var position = ""; if (this.match(TokenType.AT)) { position = this.consume(TokenType.WORD).getText(); } // Псевдоним (для показа одно и того же спрайта в разных местах) var alias = ""; if (this.match(TokenType.AS)) { alias = this.consume(TokenType.WORD).getText(); } var effect = this.matchWithEffect(); ViewActivity.getInstance().sprite(whoid, params, position, alias, effect); return false; }; Parser.prototype.hide = function() { var whoid = this.consume(TokenType.WORD).getText(); var effect = this.matchWithEffect(); ViewActivity.getInstance().hideSprite(whoid, effect); return false; }; Parser.prototype.play = function() { if (this.match(TokenType.MUSIC)) return this.playMusic(); if (this.match(TokenType.AMBIENCE)) return this.playAmbience(); if (this.lookMatch(0, TokenType.SOUND) || this.lookMatch(0, TokenType.SOUNDLOOP)) { return this.playSound(); } return false; }; Parser.prototype.playMusic = function() { var name = this.consumeMusicName(); var fade = this.matchFade(); ViewActivity.getInstance().music(name, fade); return false; }; Parser.prototype.playSound = function() { var loop = false; if (this.match(TokenType.SOUND)) loop = false; else if (this.match(TokenType.SOUNDLOOP)) loop = true; var name = this.consumeMusicName(); var fade = this.matchFade(); ViewActivity.getInstance().sound(name, loop, fade); return false; }; Parser.prototype.playAmbience = function() { var name = this.consumeMusicName(); var fade = this.matchFade(); return false; }; Parser.prototype.queue = function() { if (this.match(TokenType.MUSIC)) return this.queueMusic(); if (this.match(TokenType.SOUND)) return this.queueSound(); return false; }; Parser.prototype.queueMusic = function() { var name = this.consumeMusicName(); ViewActivity.getInstance().addMusicToQueue(name); return false; }; Parser.prototype.queueSound = function() { var name = this.consumeMusicName(); ViewActivity.getInstance().addSoundToQueue(name); return false; }; Parser.prototype.stop = function() { if (this.match(TokenType.MUSIC)) { var fade = this.matchFade(); ViewActivity.getInstance().stopMusic(fade); } if (this.match(TokenType.SOUND) || this.match(TokenType.SOUNDLOOP)) { var fade = this.matchFade(); ViewActivity.getInstance().stopSound(fade); } if (this.match(TokenType.AMBIENCE)) { var fade = this.matchFade(); // ViewActivity.getInstance().stopAmbience(fade); } return false; }; Parser.prototype.menu = function() { // menu: title? this.consume(TokenType.COLON); var title = ""; if (this.lookMatch(0, TokenType.TEXT) && !this.lookMatch(1, TokenType.COLON)) { title = this.consume(TokenType.TEXT).getText(); } if (!this.hasEndMenu) return false; // Ищем элементы выбора var menu = new Menu(title); var pos = 0; var level = 1; // уровень вложенности меню while (true) { // Расчёт уровня меню. if (this.lookMatch(pos, TokenType.MENU) && this.lookMatch(pos + 1, TokenType.COLON)) { level++; pos++; } if (this.lookMatch(pos, TokenType.ENDMENU)) { level--; // Завершаем работу по достижению ENDMENU первого уровня. if (level <= 0) break; } if (level == 1) { // Добавляем только пункты из меню первого уровня. if (this.lookMatch(pos, TokenType.TEXT) && this.lookMatch(pos + 1, TokenType.COLON)) { menu.addItem(this.get(pos).getText(), this.position + pos + 2); pos++; } } if (this.lookMatch(pos, TokenType.EOF)) return false; pos++; } ViewActivity.getInstance().menu(menu); return true; }; Parser.prototype.jump = function() { var labelName = this.consume(TokenType.WORD).getText(); this.jumpLabel(labelName); return false; }; Parser.prototype.ifStatement = function() { var condition = this.expression(); this.consume(TokenType.COLON); if (!this.hasEndIf) return false; if (condition.eval() == 0) { // Если условие не верно, пропускаем блок до следующего ENDIF var pos = 0; var level = 1; // уровень вложенности блока while (true) { // Расчёт уровня блока. if (this.lookMatch(pos, TokenType.IF)) { level++; pos++; } if (this.lookMatch(pos, TokenType.ELSE)) { level--; pos += 2; // пропускаем ELSE и двоеточие // Завершаем работу по достижению ELSE первого уровня. if (level <= 0) break; } if (this.lookMatch(pos, TokenType.ENDIF)) { level--; // Завершаем работу по достижению ENDIF первого уровня. if (level <= 0) break; } if (this.lookMatch(pos, TokenType.EOF)) return false; pos++; } this.position += pos; } return false; }; Parser.prototype.expression = function() { return this.orTest(); }; Parser.prototype.orTest = function() { var expression = this.andTest(); while (true) { if (this.match(TokenType.OR)) { expression = new BinaryExpression(Operator.BOOLEAN_OR, expression, this.andTest()); continue; } break; } return expression; }; Parser.prototype.andTest = function() { var expression = this.notTest(); while (true) { if (this.match(TokenType.AND)) { expression = new BinaryExpression(Operator.BOOLEAN_AND, expression, this.notTest()); continue; } break; } return expression; }; Parser.prototype.notTest = function() { if (this.match(TokenType.NOT)) { return new ValueExpression( this.notTest().eval() != 0 ? 0 : 1 ); } return this.comparison(); }; Parser.prototype.comparison = function() { var expression = this.additive(); while (true) { if (this.lookMatch(1, TokenType.EQ)) { if (this.match(TokenType.EQ)) { // == this.consume(TokenType.EQ); expression = new BinaryExpression(Operator.EQUALS, expression, this.additive()); continue; } if (this.match(TokenType.GT)) { // >= this.consume(TokenType.EQ); expression = new BinaryExpression(Operator.GTEQ, expression, this.additive()); continue; } if (this.match(TokenType.LT)) { // <= this.consume(TokenType.EQ); expression = new BinaryExpression(Operator.LTEQ, expression, this.additive()); continue; } if (this.match(TokenType.EXCL)) { // != this.consume(TokenType.EQ); expression = new BinaryExpression(Operator.NOTEQUALS, expression, this.additive()); continue; } } if (this.match(TokenType.LT)) { expression = new BinaryExpression(Operator.LT, expression, this.additive()); continue; } if (this.match(TokenType.GT)) { expression = new BinaryExpression(Operator.GT, expression, this.additive()); continue; } break; } return expression; }; Parser.prototype.additive = function() { var expression = this.unary(); while (true) { if (this.match(TokenType.PLUS)) { expression = new BinaryExpression(Operator.ADD, expression, this.unary()); continue; } if (this.match(TokenType.MINUS)) { expression = new BinaryExpression(Operator.SUBTRACT, expression, this.unary()); continue; } break; } return expression; }; Parser.prototype.unary = function() { if (this.match(TokenType.MINUS)) { return new ValueExpression( -this.primary().eval() ); } if (this.match(TokenType.PLUS)) { return this.primary(); } return this.primary(); }; Parser.prototype.primary = function() { var current = this.get(0); if (this.match(current, TokenType.NUMBER)) { return new ValueExpression( parseFloat(current.getText()) ); } if (this.match(current, TokenType.WORD)) { return new VariableExpression(current.getText()); } if (this.match(current, TokenType.LPAREN)) { var expr = this.expression(); this.match(TokenType.RPAREN); return expr; } throw "Неизвестное выражение"; }; Parser.prototype.preScan = function() { // Сканируем все метки, для быстрого перехода командой jump. // А также определяем параметры для оптимизации. for (var i = 0; i < this.tokensCount - 2; i++) { var current = this.tokens[i].getType(); if (current === TokenType.ENDMENU) { this.hasEndMenu = true; } else if (current === TokenType.ENDIF) { this.hasEndIf = true; } else if ( (current === TokenType.LABEL) && (this.tokens[i + 2].getType() === TokenType.COLON) ) { // label word : var token = this.tokens[i + 1]; if (token.getType() === TokenType.WORD) { // Добавляем позицию команды, следующей после метки. this.labels[token.getText()] = i + 3; } i += 2; } } }; Parser.prototype.skipMenu = function() { var pos = 0; var level = 1; // уровень вложенности меню while (true) { // Расчёт уровня меню. if (this.lookMatch(pos, TokenType.MENU) && this.lookMatch(pos + 1, TokenType.COLON)) { level++; pos += 2; } if (this.lookMatch(pos, TokenType.ENDMENU)) { pos++; level--; // Завершаем работу по достижению ENDMENU первого уровня. if (level <= 0) break; } if (this.lookMatch(pos, TokenType.EOF)) return 0; pos++; } return pos; }; Parser.prototype.skipIf = function() { var pos = 0; var level = 1; while (true) { if (this.lookMatch(pos, TokenType.IF)) level++; else if (this.lookMatch(pos, TokenType.ENDIF)) { pos++; level--; if (level <= 0) break; } else if (this.lookMatch(pos, TokenType.EOF)) return 0; pos++; } return pos; }; Parser.prototype.consumeMusicName = function() { var name = ""; if (this.lookMatch(1, TokenType.LBRACKET)) { // music_list["music"] this.consume(TokenType.WORD); this.consume(TokenType.LBRACKET); name = this.consume(TokenType.TEXT).getText(); this.consume(TokenType.RBRACKET); } else if (this.lookMatch(0, TokenType.TEXT)) { name = this.consume(TokenType.TEXT).getText(); } else { name = this.consume(TokenType.WORD).getText(); } return name; }; Parser.prototype.matchFade = function() { var result = new FadeInfo(); if (this.match(TokenType.FADEIN)) { result.fadeIn = true; result.duration = this.consumeDouble(); } else if (this.match(TokenType.FADEOUT)) { result.fadeOut = true; result.duration = this.consumeDouble(); } return result; }; Parser.prototype.matchWithEffect = function() { if (this.match(TokenType.WITH)) { return this.consume(TokenType.WORD).getText(); } return ""; }; Parser.prototype.consumeDouble = function() { return parseFloat(this.consume(TokenType.NUMBER).getText()); }; Parser.prototype.consumeBoolean = function() { return "true" === (this.consume(TokenType.WORD).getText().toLowerCase()); }; Parser.prototype.matchWithEffect = function() { if (this.match(TokenType.WITH)) { return this.consume(TokenType.WORD).getText(); } return ""; }; Parser.prototype.match_1 = function(type) { if (this.get(0).getType() !== type) return false; this.position++; return true; }; Parser.prototype.match_2 = function(token, type) { if (type !== token.getType()) return false; this.position++; return true; }; Parser.prototype.match = function(arg1, arg2) { if (arguments.length === 1) return this.match_1(arg1); else return this.match_2(arg1, arg2); }; Parser.prototype.consume = function(type) { if (this.get(0).getType() !== type) throw "Ожидался токен " + type + ", но получен " + this.get(0); return this.tokens[this.position++]; }; Parser.prototype.lookMatch = function(pos, type) { return (type === this.get(pos).getType()); }; Parser.prototype.get = function(offset) { if (this.position + offset >= this.tokensCount) return this.EOF; return this.tokens[this.position + offset]; };