RpyPlayerWeb/public_html/js/Parser.js
2015-12-07 17:11:55 +02:00

589 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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.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.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.NUMBER).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 + ".";
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];
};