RpyPlayerWeb/public_html/js/Parser.js

724 lines
23 KiB
JavaScript
Raw Normal View History

2015-12-07 17:11:55 +02:00
/* global TokenType, ViewActivity, TextUtils, Variables, Operator */
2015-12-06 13:49:35 +02:00
function Parser(tokens) {
this.tokens = tokens;
this.tokensCount = tokens.length;
this.position = 0;
this.lastPosition = 0;
this.labels = {};
2015-12-07 17:11:55 +02:00
/** Оптимизация, чтобы каждый раз не искать endmenu/endif,
* если их попросту нет в необработанном сценарии */
this.hasEndMenu = false;
this.hasEndIf = false;
Variables.init();
2015-12-06 13:49:35 +02:00
}
Parser.prototype.EOF = new Token("", TokenType.EOF);
Parser.prototype.getInstance = function () {
return this;
};
2015-12-07 19:35:47 +02:00
Parser.prototype.setPosition = function (position) {
this.position = position;
this.next();
};
2015-12-07 17:16:40 +02:00
Parser.prototype.jumpLabel = function (label) {
if (label in this.labels) {
this.position = this.labels[label];
}
};
2015-12-06 13:49:35 +02:00
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();
2015-12-08 12:35:14 +02:00
if (this.match(token, TokenType.QUEUE)) return this.queue();
if (this.match(token, TokenType.STOP)) return this.stop();
2015-12-06 13:49:35 +02:00
if (this.match(token, TokenType.SHOW)) return this.show();
if (this.match(token, TokenType.HIDE)) return this.hide();
2015-12-07 17:16:40 +02:00
if (this.match(token, TokenType.JUMP)) return this.jump();
2015-12-07 17:22:11 +02:00
if (this.match(token, TokenType.IF)) return this.ifStatement();
2015-12-06 13:49:35 +02:00
2015-12-07 19:35:47 +02:00
if (this.lookMatch(1, TokenType.COLON)) {
2015-12-06 13:49:35 +02:00
// 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;
}
2015-12-07 19:35:47 +02:00
}
2015-12-06 13:49:35 +02:00
// Текст с именем автора реплики.
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)) {
2015-12-07 14:38:58 +02:00
ViewActivity.getInstance().finish();
2015-12-06 13:49:35 +02:00
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;
}
2015-12-07 17:11:55 +02:00
if (this.match(token, TokenType.WORD)) {
2015-12-06 13:49:35 +02:00
if (this.match(TokenType.EQ)) {
// variable = expression
2015-12-07 17:11:55 +02:00
Variables.setVariable(token.getText(), this.expression().eval());
2015-12-06 13:49:35 +02:00
return false;
}
if (this.lookMatch(1, TokenType.EQ) && this.match(TokenType.PLUS)) {
// variable += expression
this.consume(TokenType.EQ);
var varValue = Variables.getVariable(token.getText());
2015-12-07 17:11:55 +02:00
Variables.setVariable(token.getText(), varValue + this.expression().eval());
2015-12-06 13:49:35 +02:00
return false;
}
if (this.lookMatch(1, TokenType.EQ) && this.match(TokenType.MINUS)) {
// variable -= expression
this.consume(TokenType.EQ);
var varValue = Variables.getVariable(token.getText());
2015-12-07 17:11:55 +02:00
Variables.setVariable(token.getText(), varValue - this.expression().eval());
2015-12-06 13:49:35 +02:00
return false;
}
2015-12-07 17:11:55 +02:00
}
2015-12-06 13:49:35 +02:00
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();
2015-12-09 11:16:17 +02:00
if (this.match(TokenType.SOUND)) return this.playSound();
if (this.match(TokenType.SOUNDLOOP)) return this.playSoundLoop();
2015-12-06 13:49:35 +02:00
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 name = this.consumeMusicName();
var fade = this.matchFade();
2015-12-09 11:16:17 +02:00
ViewActivity.getInstance().sound(name, fade);
return false;
};
Parser.prototype.playSoundLoop = function() {
var name = this.consumeMusicName();
var fade = this.matchFade();
ViewActivity.getInstance().soundLoop(name, fade);
2015-12-06 13:49:35 +02:00
return false;
};
Parser.prototype.playAmbience = function() {
var name = this.consumeMusicName();
var fade = this.matchFade();
2015-12-09 10:55:10 +02:00
ViewActivity.getInstance().ambience(name, fade);
2015-12-06 13:49:35 +02:00
return false;
};
2015-12-08 12:35:14 +02:00
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);
}
2015-12-09 11:16:17 +02:00
if (this.match(TokenType.SOUND)) {
var fade = this.matchFade();
2015-12-08 12:30:28 +02:00
ViewActivity.getInstance().stopSound(fade);
}
2015-12-09 11:16:17 +02:00
if (this.match(TokenType.SOUNDLOOP)) {
var fade = this.matchFade();
ViewActivity.getInstance().stopSoundLoop(fade);
}
if (this.match(TokenType.AMBIENCE)) {
var fade = this.matchFade();
2015-12-09 10:55:10 +02:00
ViewActivity.getInstance().stopAmbience(fade);
}
return false;
};
2015-12-07 19:35:47 +02:00
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;
};
2015-12-07 17:16:40 +02:00
Parser.prototype.jump = function() {
var labelName = this.consume(TokenType.WORD).getText();
this.jumpLabel(labelName);
return false;
};
2015-12-07 17:22:11 +02:00
Parser.prototype.ifStatement = function() {
2015-12-07 17:50:16 +02:00
var condition = this.expression();
2015-12-07 17:22:11 +02:00
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;
};
2015-12-07 17:11:55 +02:00
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;
};
2015-12-06 13:49:35 +02:00
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() {
2015-12-07 21:28:44 +02:00
return "true" === (this.consume(TokenType.WORD).getText().toLowerCase());
2015-12-06 13:49:35 +02:00
};
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) {
2015-12-07 21:28:44 +02:00
if (this.get(0).getType() !== type) throw "Ожидался токен " + type + ", но получен " + this.get(0);
2015-12-06 13:49:35 +02:00
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];
};