-
-
Save goto-bus-stop/cb59e5f0ae03ec12c3800f120c16eef3 to your computer and use it in GitHub Desktop.
reasonably accurate JS implementation of the AoE RMS parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Token { | |
constructor (id, name, type, value, argTypes) { | |
this.id = id | |
this.name = name | |
this.type = type | |
this.value = value | |
this.argTypes = argTypes | |
} | |
} | |
/** | |
* Microsoft C Runtime rand() implementation | |
*/ | |
class CRandom { | |
constructor (seed) { | |
this.seed = seed | 0 // make it an int32 | |
} | |
next () { | |
this.seed = (((this.seed * 214013) | 0) + 2531011) | 0 | |
return (this.seed >> 16) & 0x7fff | |
} | |
nextRange (max) { | |
return Math.floor(this.next() * max / 0x7fff) | |
} | |
} | |
const TOKEN_TYPE_SYNTAX = 0 | |
const TOKEN_TYPE_DEFINE = 1 | |
const TOKEN_TYPE_CONST = 2 | |
const TOK_DEFINE = 0 | |
const TOK_UNDEFINE = 1 // eslint-disable-line no-unused-vars | |
const TOK_CONST = 2 | |
const TOK_IF = 3 | |
const TOK_ELIF = 4 | |
const TOK_ELSE = 5 | |
const TOK_ENDIF = 6 | |
const TOK_START_RANDOM = 7 | |
const TOK_PERCENT_CHANCE = 8 | |
const TOK_END_RANDOM = 9 | |
const TOK_INCLUDE = 10 | |
const TOK_PLAYER_SETUP = 11 | |
const TOK_LAND_GENERATION = 17 | |
const TOK_CLIFF_GENERATION = 34 | |
const TOK_TERRAIN_GENERATION = 42 | |
const TOK_OBJECTS_GENERATION = 47 | |
const TOK_CONNECTION_GENERATION = 62 | |
const TOK_BLOCK_OPEN = 67 | |
const TOK_BLOCK_CLOSE = 68 | |
const TOK_COMMENT_OPEN = 69 | |
const TOK_COMMENT_CLOSE = 70 | |
const TOK_ELEVATION_GENERATION = 79 | |
const TOK_INCLUDE_DRS = 92 | |
const ARGTYPE_NONE = 0 | |
const ARGTYPE_STRING = 1 | |
const ARGTYPE_INT = 2 | |
const ARGTYPE_TOKEN = 3 | |
const ARGTYPE_TOKEN2 = 4 | |
const ARGTYPE_FILE = 5 | |
const RANDOM_STATE_DEAD = -1 | |
const RANDOM_STATE_PRE = 1 | |
const RANDOM_STATE_MATCH = 2 | |
const RANDOM_STATE_POST = 3 | |
const IF_STATE_DEAD = -1 | |
const IF_STATE_FAIL = 1 | |
const IF_STATE_MATCH = 2 | |
const IF_STATE_DONE = 3 | |
// Generated from the list of tokens in age2_x1.exe | |
// [name, id, type, argTypes] | |
const defaultTokens = [ | |
['#define', 0, 0, [1, 0, 0, 0]], | |
['#undefine', 1, 0, [1, 0, 0, 0]], | |
['#const', 2, 0, [1, 2, 0, 0]], | |
['if', 3, 0, [4, 0, 0, 0]], | |
['elseif', 4, 0, [4, 0, 0, 0]], | |
['else', 5, 0, [0, 0, 0, 0]], | |
['endif', 6, 0, [0, 0, 0, 0]], | |
['start_random', 7, 0, [0, 0, 0, 0]], | |
['percent_chance', 8, 0, [2, 0, 0, 0]], | |
['end_random', 9, 0, [0, 0, 0, 0]], | |
['#include', 10, 0, [5, 0, 0, 0]], | |
['<PLAYER_SETUP>', 11, 0, [0, 0, 0, 0]], | |
['random_placement', 12, 0, [0, 0, 0, 0]], | |
['grouped_by_team', 13, 0, [0, 0, 0, 0]], | |
['min_distance', 14, 0, [0, 0, 0, 0]], | |
['max_distance', 15, 0, [0, 0, 0, 0]], | |
['set_position', 16, 0, [0, 0, 0, 0]], | |
['<LAND_GENERATION>', 17, 0, [0, 0, 0, 0]], | |
['land_percent', 18, 0, [2, 0, 0, 0]], | |
['land_position', 71, 0, [2, 2, 0, 0]], | |
['land_id', 72, 0, [2, 0, 0, 0]], | |
['base_terrain', 19, 0, [3, 0, 0, 0]], | |
['create_player_lands', 20, 0, [0, 0, 0, 0]], | |
['terrain_type', 21, 0, [3, 0, 0, 0]], | |
['base_size', 22, 0, [2, 0, 0, 0]], | |
['left_border', 23, 0, [2, 0, 0, 0]], | |
['right_border', 24, 0, [2, 0, 0, 0]], | |
['top_border', 25, 0, [2, 0, 0, 0]], | |
['bottom_border', 26, 0, [2, 0, 0, 0]], | |
['border_fuzziness', 27, 0, [2, 0, 0, 0]], | |
['zone', 28, 0, [2, 0, 0, 0]], | |
['set_zone_by_team', 29, 0, [0, 0, 0, 0]], | |
['set_zone_randomly', 30, 0, [0, 0, 0, 0]], | |
['other_zone_avoidance_distance', 31, 0, [2, 0, 0, 0]], | |
['create_land', 32, 0, [0, 0, 0, 0]], | |
['assign_to_player', 33, 0, [2, 0, 0, 0]], | |
['<CLIFF_GENERATION>', 34, 0, [0, 0, 0, 0]], | |
['min_number_of_cliffs', 35, 0, [2, 0, 0, 0]], | |
['max_number_of_cliffs', 36, 0, [2, 0, 0, 0]], | |
['min_length_of_cliff', 37, 0, [2, 0, 0, 0]], | |
['max_length_of_cliff', 38, 0, [2, 0, 0, 0]], | |
['cliff_curliness', 39, 0, [2, 0, 0, 0]], | |
['min_distance_cliffs', 40, 0, [2, 0, 0, 0]], | |
['min_terrain_distance', 41, 0, [2, 0, 0, 0]], | |
['<TERRAIN_GENERATION>', 42, 0, [0, 0, 0, 0]], | |
['create_terrain', 43, 0, [3, 0, 0, 0]], | |
['percent_of_land', 44, 0, [2, 0, 0, 0]], | |
['number_of_clumps', 45, 0, [2, 0, 0, 0]], | |
['spacing_to_other_terrain_types', 46, 0, [2, 0, 0, 0]], | |
['<OBJECTS_GENERATION>', 47, 0, [0, 0, 0, 0]], | |
['create_object', 48, 0, [3, 0, 0, 0]], | |
['set_scaling_to_map_size', 49, 0, [0, 0, 0, 0]], | |
['number_of_groups', 50, 0, [2, 0, 0, 0]], | |
['number_of_objects', 51, 0, [2, 0, 0, 0]], | |
['group_variance', 52, 0, [2, 0, 0, 0]], | |
['group_placement_radius', 53, 0, [2, 0, 0, 0]], | |
['set_loose_grouping', 54, 0, [0, 0, 0, 0]], | |
['set_tight_grouping', 55, 0, [0, 0, 0, 0]], | |
['terrain_to_place_on', 56, 0, [3, 0, 0, 0]], | |
['set_gaia_object_only', 57, 0, [0, 0, 0, 0]], | |
['set_place_for_every_player', 58, 0, [0, 0, 0, 0]], | |
['place_on_specific_land_id', 59, 0, [2, 0, 0, 0]], | |
['min_distance_to_players', 60, 0, [2, 0, 0, 0]], | |
['max_distance_to_players', 61, 0, [2, 0, 0, 0]], | |
['<CONNECTION_GENERATION>', 62, 0, [0, 0, 0, 0]], | |
['create_connect_all_players_land', 63, 0, [0, 0, 0, 0]], | |
['create_connect_teams_land', 64, 0, [0, 0, 0, 0]], | |
['create_connect_same_land_zones', 65, 0, [0, 0, 0, 0]], | |
['create_connect_all_lands', 66, 0, [0, 0, 0, 0]], | |
['{', 67, 0, [0, 0, 0, 0]], | |
['}', 68, 0, [0, 0, 0, 0]], | |
['/*', 69, 0, [0, 0, 0, 0]], | |
['*/', 70, 0, [0, 0, 0, 0]], | |
['clumping_factor', 73, 0, [2, 0, 0, 0]], | |
['number_of_tiles', 74, 0, [2, 0, 0, 0]], | |
['set_scale_by_groups', 75, 0, [0, 0, 0, 0]], | |
['set_scale_by_size', 76, 0, [0, 0, 0, 0]], | |
['set_avoid_player_start_areas', 77, 0, [0, 0, 0, 0]], | |
['min_distance_group_placement', 78, 0, [2, 0, 0, 0]], | |
['<ELEVATION_GENERATION>', 79, 0, [0, 0, 0, 0]], | |
['create_elevation', 80, 0, [2, 0, 0, 0]], | |
['spacing', 81, 0, [2, 0, 0, 0]], | |
['default_terrain_placement', 82, 0, [3, 0, 0, 0]], | |
['replace_terrain', 83, 0, [3, 3, 0, 0]], | |
['terrain_cost', 84, 0, [3, 2, 0, 0]], | |
['terrain_size', 85, 0, [3, 2, 2, 0]], | |
['min_placement_distance', 86, 0, [2, 0, 0, 0]], | |
['set_scaling_to_player_number', 87, 0, [0, 0, 0, 0]], | |
['height_limits', 88, 0, [2, 2, 0, 0]], | |
['set_flat_terrain_only', 89, 0, [0, 0, 0, 0]], | |
['max_distance_to_other_zones', 91, 0, [2, 0, 0, 0]], | |
['#include_drs', 92, 0, [5, 2, 0, 0]], | |
['temp_min_distance_group_placement', 93, 0, [2, 0, 0, 0]] | |
] | |
const { floor, abs } = Math | |
/** | |
* An implementation of the Age of Empires 2 Random Map Script parser, | |
* staying true to the original. | |
*/ | |
class Parser { | |
constructor (options = {}) { | |
this.options = Object.assign({ | |
numPlayers: 2, | |
size: 120, | |
random: null, | |
onWarn: (warning) => {} | |
}, options) | |
this.random = this.options.random || new CRandom(Date.now()) | |
this.tokenTypes = [] | |
for (const token of defaultTokens) { | |
this.defineToken(...token) | |
} | |
this.commentDepth = 0 | |
this.randomStack = [] | |
this.ifStack = [] | |
this.parseState = [] | |
this.stages = [] | |
this.terrains = [] | |
this.lands = [] | |
this.landMeta = {} | |
this.activeLands = [] | |
this.objects = [] | |
this.connections = [] | |
this.elevations = [] | |
this.cliffs = {} | |
this.terrainHotspots = [] | |
this.objectHotspots = [] | |
this.elevationHotspots = [] | |
this.cliffHotspots = [] | |
} | |
log (...args) { | |
console.log( | |
`(${this.line}:${this.column})`, | |
...args | |
) | |
} | |
warn (str) { | |
const warning = new Error(str) | |
warning.index = this.index | |
warning.line = this.line | |
warning.column = this.column | |
this.options.onWarn(warning) | |
} | |
error (str) { | |
const err = new Error(`(${this.line}:${this.column}) ${str}`) | |
err.index = this.index | |
err.line = this.line | |
err.column = this.column | |
return err | |
} | |
/** | |
* Define a builtin token, both constants and control flow (if, #include). | |
* @param {string} name The name of the token. | |
* @param {number} id The builtin token ID. Used by the parser to determine behaviour. | |
* @param {number} type The token type (constant, control flow, command, etc) | |
* @param {Array.<number>} argTypes The argument types for this token. | |
*/ | |
defineToken (name, id, type, argTypes) { | |
this.tokenTypes.push(new Token(id, name, type, null, argTypes)) | |
} | |
/** | |
* Define a user token, ie. a constant. | |
* @param {string} name The name of the constant. | |
* @param {number} type The constant type (existence or number). | |
* @param {number} value The integer value of the constant. | |
* @param {Array.<number>} argTypes The argument types for this token (none). | |
*/ | |
defineUserToken (name, type, value, argTypes) { | |
this.tokenTypes.push(new Token(null, name, type, value, argTypes)) | |
} | |
/** | |
* Parse some code. | |
* @param {string|Buffer} code | |
*/ | |
write (code) { | |
this.code = code.toString() | |
this.index = 0 | |
this.line = 0 | |
this.column = 0 | |
this._runParseLoop() | |
} | |
/** | |
* Finish parsing, return the result. | |
*/ | |
end () { | |
return { | |
sections: this.stages, | |
terrains: this.terrains, | |
lands: this.lands, | |
activeLands: this.activeLands, | |
objects: this.objects, | |
connections: this.connections, | |
elevations: this.elevations, | |
cliffs: this.cliffs, | |
landMeta: this.landMeta, | |
terrainHotspots: this.terrainHotspots, | |
objectHotspots: this.objectHotspots, | |
elevationHotspots: this.elevationHotspots, | |
cliffHotspots: this.cliffHotspots | |
} | |
} | |
/** | |
* Parse all available tokens. | |
*/ | |
_runParseLoop () { | |
while (this.readNextToken()) { | |
if (this.currentToken) this.parseToken() | |
} | |
} | |
_pushState () { | |
this.parseState.push({ | |
code: this.code, | |
index: this.index, | |
line: this.line, | |
column: this.column | |
}) | |
} | |
_popState () { | |
Object.assign(this, this.parseState.pop()) | |
} | |
includeCode (code) { | |
this._pushState() | |
this.write(code) | |
this._popState() | |
this._runParseLoop() | |
} | |
include (file) { | |
this._pushState() | |
if (!this.options.include) { | |
throw this.error('#include is not supported') | |
} | |
this.options.include(file, (err, code) => { | |
if (err) throw err | |
this.write(code) | |
this._popState() | |
this._runParseLoop() | |
}) | |
} | |
readNextToken () { | |
const word = this.readNextWord() | |
if (!word) return false | |
const token = this.getTokenType(word) | |
this.currentToken = null | |
if (token) { | |
this.currentToken = token | |
return this.currentToken | |
} else if (this.commentDepth === 0) { | |
this.warn(`${word}, unrecognized command ignored.`) | |
} | |
return true | |
} | |
getTokenType (word) { | |
return this.tokenTypes.find((token) => { | |
return token.name === word | |
}) | |
} | |
/** | |
* Get the current ifState, from the top of the stack. | |
* | |
* If we're not inside an `if` statement act like we MATCHed one. | |
*/ | |
get ifState () { | |
return this.ifStack[this.ifStack.length - 1] || IF_STATE_MATCH | |
} | |
/** | |
* Set the current ifState, at the top of the stack. | |
*/ | |
set ifState (value) { | |
this.ifStack[this.ifStack.length - 1] = value | |
} | |
/** | |
* Get the current randomState, from the top of the stack. | |
*/ | |
get randomState () { | |
const last = this.randomStack[this.randomStack.length - 1] | |
if (last) return last.state | |
throw this.error('Attempted to get `randomState`, but stack is empty') | |
} | |
/** | |
* Set the current randomState, at the top of the stack. | |
*/ | |
set randomState (value) { | |
const last = this.randomStack[this.randomStack.length - 1] | |
if (!last) throw this.error('Attempted to set `randomState`, but stack is empty') | |
last.state = value | |
} | |
/** | |
* Get the current randomValue, from the top of the stack. | |
*/ | |
get randomValue () { | |
const last = this.randomStack[this.randomStack.length - 1] | |
if (last) return last.value | |
throw this.error('Attempted to get `randomValue`, but stack is empty') | |
} | |
/** | |
* Set the current randomValue, at the top of the stack. | |
*/ | |
set randomValue (value) { | |
const last = this.randomStack[this.randomStack.length - 1] | |
if (!last) throw this.error('Attempted to set `randomValue`, but stack is empty') | |
last.value = value | |
} | |
parseToken () { | |
const token = this.currentToken | |
const { | |
id, | |
argTypes | |
} = token | |
if (id === TOK_COMMENT_OPEN) { // /* | |
this.commentDepth += 1 | |
return | |
} else if (id === TOK_COMMENT_CLOSE) { // */ | |
this.commentDepth -= 1 | |
return | |
} | |
if (this.commentDepth > 0) { | |
return | |
} | |
const args = [] | |
for (const argType of argTypes) { | |
if (argType === ARGTYPE_NONE) break // Stop parsing arguments. | |
if (argType === ARGTYPE_STRING) args.push(this.readString()) | |
if (argType === ARGTYPE_INT) args.push(this.readInt()) | |
if (argType === ARGTYPE_TOKEN) { | |
const value = this.readToken() | |
// This one is only added to the arguments list when it exists in src. | |
// Not sure why this is different from ARGTYE_TOKEN2, because it seems like | |
// src attempts to use it anyway during the generation phase. | |
if (typeof value === 'object') args.push(value) | |
} | |
if (argType === ARGTYPE_TOKEN2) args.push(this.readToken()) | |
if (argType === ARGTYPE_FILE) args.push(this.readString()) | |
} | |
this.currentArgs = args | |
if (this.ifState === IF_STATE_MATCH) { | |
switch (id) { | |
case TOK_START_RANDOM: // start_random | |
if (this.randomStack.length > 0 && this.randomState !== RANDOM_STATE_MATCH) { | |
this.randomStack.push({ | |
value: 0, | |
state: RANDOM_STATE_DEAD | |
}) | |
break | |
} | |
this.randomStack.push({ | |
value: this.random.nextRange(100), | |
state: RANDOM_STATE_PRE | |
}) | |
break | |
case TOK_PERCENT_CHANCE: // percent_chance | |
if (this.randomState === RANDOM_STATE_PRE) { | |
const [ percent ] = args | |
if (this.randomValue > percent) { | |
this.randomValue -= percent | |
} else { | |
this.log(`Entering percent_chance: ${percent}%`) | |
// Take this branch! | |
this.randomState = RANDOM_STATE_MATCH | |
} | |
} else if (this.randomState !== RANDOM_STATE_DEAD) { | |
this.randomState = RANDOM_STATE_POST | |
} | |
break | |
case TOK_END_RANDOM: // end_random | |
if (this.randomState === RANDOM_STATE_PRE) { | |
this.warn('Non-exhaustive random branch') | |
} | |
this.randomStack.pop() | |
break | |
} | |
} | |
if (this.randomStack.length === 0 || this.randomState === RANDOM_STATE_MATCH) { | |
switch (id) { | |
case TOK_IF: { // if | |
// We can only take this branch if we are already matching, | |
// but need to keep track of if block depth in order to | |
// balance nested ifs correctly. | |
if (this.ifState !== IF_STATE_MATCH) { | |
this.ifStack.push(IF_STATE_DEAD) | |
break | |
} | |
const [ condition ] = args | |
this.ifStack.push(condition ? IF_STATE_MATCH : IF_STATE_FAIL) | |
if (condition) this.log('Entering if condition', this.ifState) | |
else this.log('Skipping if condition', this.ifState) | |
break | |
} | |
case TOK_ELIF: // elseif | |
// FIXME this might be wrong if this `if/elseif/endif` sequence is nested | |
// within an `if` statement with a failing condition, since it would be checking | |
// the outer if? | |
// Need to check src | |
if (this.ifStack.length > 0) { | |
const [ condition ] = args | |
if (this.ifState === IF_STATE_FAIL) { | |
this.ifState = condition ? IF_STATE_MATCH : IF_STATE_FAIL | |
} else if (this.ifState === IF_STATE_MATCH) { | |
this.ifState = IF_STATE_DONE | |
} | |
} | |
break | |
case TOK_ELSE: // else | |
if (this.ifStack.length > 0) { | |
if (this.ifState === IF_STATE_FAIL) { | |
this.ifState = IF_STATE_MATCH | |
} else { | |
this.ifState = IF_STATE_DONE | |
} | |
} | |
break | |
case TOK_ENDIF: // endif | |
if (this.ifStack.length > 0) { | |
this.ifStack.pop() | |
} | |
break | |
} | |
} | |
if (this.ifState === IF_STATE_MATCH && (this.randomStack.length === 0 || this.randomState === RANDOM_STATE_MATCH)) { | |
switch (id) { | |
case TOK_DEFINE: { // #define | |
const [ name ] = args | |
this.log('Defining constant', name) | |
this.defineUserToken(name, TOKEN_TYPE_DEFINE, 0, | |
[ARGTYPE_NONE, ARGTYPE_NONE, ARGTYPE_NONE, ARGTYPE_NONE]) | |
break | |
} | |
case TOK_CONST: { // #const | |
const [ name, value ] = args | |
this.log('Defining constant', name, value) | |
this.defineUserToken(name, TOKEN_TYPE_CONST, value, | |
[ARGTYPE_NONE, ARGTYPE_NONE, ARGTYPE_NONE, ARGTYPE_NONE]) | |
break | |
} | |
case TOK_INCLUDE: // #include | |
throw this.error('#include is not supported') | |
case TOK_INCLUDE_DRS: { // #include_drs | |
throw this.error('#include_drs is not supported') | |
} | |
case TOK_PLAYER_SETUP: // <PLAYER_SETUP> | |
case TOK_LAND_GENERATION: // <LAND_GENERATION> | |
case TOK_CLIFF_GENERATION: // <CLIFF_GENERATION> | |
case TOK_TERRAIN_GENERATION: // <TERRAIN_GENERATION> | |
case TOK_OBJECTS_GENERATION: // <OBJECTS_GENERATION> | |
case TOK_CONNECTION_GENERATION: // <CONNECTION_GENERATION> | |
case TOK_ELEVATION_GENERATION: // <ELEVATION_GENERATION> | |
this.parseSectionHeader(id) | |
break | |
case TOK_BLOCK_OPEN: // { | |
this.insideBlock = true | |
break | |
case TOK_BLOCK_CLOSE: // } | |
this.insideBlock = false | |
break | |
default: | |
if (id <= 9) break | |
switch (this.stage) { | |
case TOK_PLAYER_SETUP: this.parsePlayerSetup(token, args); break | |
case TOK_LAND_GENERATION: this.parseLandGeneration(token, args); break | |
case TOK_CLIFF_GENERATION: this.parseCliffGeneration(token, args); break | |
case TOK_TERRAIN_GENERATION: this.parseTerrainGeneration(token, args); break | |
case TOK_OBJECTS_GENERATION: this.parseObjectsGeneration(token, args); break | |
case TOK_CONNECTION_GENERATION: this.parseConnectionGeneration(token, args); break | |
case TOK_ELEVATION_GENERATION: this.parseElevationGeneration(token, args); break | |
} | |
} | |
} | |
} | |
parsePlayerSetup (token) { | |
const { id, name } = token | |
if (id < 12 || id > 16) { | |
throw this.error(`Unknown token in <PLAYER_SETUP> section: ${name}`) | |
} | |
} | |
parseLandGeneration (token, args) { | |
const { id } = token | |
if (!this.insideBlock) { | |
if (id === 19) { // base_terrain | |
this.landMeta.baseTerrain = args[0].value | |
return | |
} | |
if (id === 20) { // create_player_lands | |
this.activeLands = [] | |
for (let i = 1; i <= this.options.numPlayers; i += 1) { | |
const landId = this.createLand(i) | |
this.activeLands.push(landId) | |
} | |
this.isPlayerLand = true | |
return | |
} | |
if (id === 32) { // create_land | |
const landId = this.createLand(0) | |
this.activeLands.push(landId) | |
this.isPlayerLand = false | |
return | |
} | |
} | |
for (const landId of this.activeLands) { | |
const land = this.lands[landId] | |
switch (id) { | |
case 71: // land_position | |
if (this.isPlayerLand) break | |
land.position = { | |
x: args[0] / 100 * this.options.size, | |
y: args[1] / 100 * this.options.size | |
} | |
break | |
case 18: // land_percent | |
land.tiles = args[0] / 100 * this.options.size * this.options.size // TODO multiply * that weird thing in the src | |
break | |
case 74: // number_of_tiles | |
land.tiles = args[0] | |
break | |
case 21: // terrain_type | |
land.terrain = args[0].type | |
break | |
case 23: // left_border | |
land.leftBorder = args[0] * this.options.size / 100 | |
break | |
case 24: // right_border | |
land.rightBorder = this.options.size - args[0] * this.options.size / 100 | |
break | |
case 25: // top_border | |
land.topBorder = args[0] * this.options.size / 100 | |
break | |
case 26: // bottom_border | |
land.bottomBorder = this.options.size - args[0] * this.options.size / 100 | |
break | |
case 27: // border_fuzziness | |
land.borderFuzziness = args[0] | |
break | |
case 28: // zone | |
land.zone = args[0] + 10 // what's this for? | |
break | |
case 29: // set_zone_by_team | |
// TODO implement this correctly | |
land.zone = landId - this.activeLands[0] + 1 | |
break | |
case 30: // set_zone_randomly | |
land.zone = this.random.nextRange(this.options.numPlayers - 1) + 2 | |
break | |
case 31: // other_zone_avoidance_distance | |
land.spacing = args[0] | |
break | |
case 72: // land_id | |
land.id = args[0] + 10 | |
break | |
case 22: // base_size | |
land.baseSize = args[0] | |
break | |
case 73: // clumping_factor | |
land.clumpiness = args[0] | |
break | |
case 86: // min_placement_distance | |
land.minPlacementDistance = args[0] | |
break | |
case 33: // assign_to_player | |
if (!this.isPlayerLand) { | |
if (args[0] < 0 || args[0] > 9) break | |
if (args[0] >= this.options.numPlayers) { | |
console.log(`Player ${args[0]} does not exist, removing land`) | |
this.lands.pop() | |
this.objectHotspots.pop() | |
} | |
const objectHs = this.objectHotspots[landId] | |
objectHs.playerId = args[0] | |
if (!objectHs.id) objectHs.id = 1 | |
} | |
break | |
} | |
} | |
} | |
createLand (zone) { | |
this.lands.push({ | |
tiles: this.options.size ** 2, | |
position: { x: -1, y: -1 }, | |
terrain: 0, | |
baseSize: 3, | |
spacing: 0, | |
zone: zone, | |
clumpiness: 8, | |
leftBorder: 0, | |
topBorder: 0, | |
rightBorder: this.options.size, | |
bottomBorder: this.options.size, | |
borderFuzziness: 20, | |
minPlacementDistance: -1 | |
}) | |
this.objectHotspots.push({ | |
x: -1, | |
y: -1, | |
// hotspot data | |
id: zone > 0 ? 1 : 0, | |
playerId: zone | |
}) | |
return this.lands.length - 1 | |
} | |
parseCliffGeneration (token, args) { | |
const { id } = token | |
const cliffs = this.cliffs | |
switch (id) { | |
case 35: // min_number_of_cliffs | |
cliffs.minNumber = args[0] | |
break | |
case 36: // max_number_of_cliffs | |
cliffs.maxNumber = args[0] | |
break | |
case 37: // min_length_of_cliff | |
cliffs.minLength = args[0] | |
break | |
case 38: // max_length_of_cliff | |
cliffs.maxLength = args[0] | |
break | |
case 39: // cliff_curliness | |
cliffs.curliness = args[0] | |
break | |
case 40: // min_distance_cliffs | |
cliffs.minDistanceBetweenCliffs = args[0] | |
break | |
case 41: // min_terrain_distance | |
cliffs.minDistanceToTerrain = args[0] | |
break | |
default: | |
this.warn('Command is not valid in this frame of reference.') | |
} | |
} | |
parseTerrainGeneration (token, args) { | |
const { id } = token | |
if (!this.insideBlock) { | |
if (id === 43) { // create_terrain | |
this.terrains.push({ | |
tiles: this.options.size, // NOT squared, just the horizontal amount of tiles | |
type: args[0].value, | |
numberOfClumps: 1, | |
spacingToOtherTerrainTypes: 0, | |
baseTerrain: null, | |
clumpiness: 20, | |
avoidPlayerStartAreas: false, | |
minHeight: 0, | |
maxHeight: 0, | |
flatOnly: false, | |
scalingType: 0 | |
}) | |
return | |
} | |
} | |
if (!this.terrains.length) { | |
this.warn('Must use a create command before braces.') | |
return | |
} | |
const terrain = this.terrains[this.terrains.length - 1] | |
switch (id) { | |
case 19: // base_terrain | |
terrain.baseTerrain = args[0].value | |
break | |
case 18: // land_percent | |
terrain.tiles = -args[0] | |
break | |
case 0x4A: // number_of_tiles | |
terrain.tiles = args[0] | |
break | |
case 0x2D: // number_of_clumps | |
terrain.numberOfClumps = args[0] | |
break | |
case 0x2E: // spacing_to_other_terrain_types | |
terrain.spacingToOtherTerrainTypes = args[0] | |
break | |
case 0x49: // clumping_factor | |
terrain.clumpiness = args[0] | |
break | |
case 0x4D: | |
terrain.avoidPlayerStartAreas = true | |
break | |
case 0x4B: // set_scale_by_groups | |
terrain.scalingType = 1 | |
break | |
case 0x4C: // set_scale_by_size | |
terrain.scalingType = 2 | |
break | |
case 0x58: // height_limits | |
terrain.minHeight = args[0] | |
terrain.maxHeight = args[1] | |
break | |
case 0x59: // set_flat_terrain_only | |
terrain.flatOnly = true | |
break | |
default: | |
this.warn('Command is not valid in this frame of reference.') | |
} | |
} | |
parseObjectsGeneration (token, args) { | |
const { id } = token | |
if (!this.insideBlock) { | |
if (id === 48 /* create_object */) { | |
this.objects.push({ | |
type: args[0] ? args[0].value : 0, | |
baseTerrain: -1, | |
groupingType: 0, | |
scalingType: 0, | |
amount: 1, | |
groupVariance: 0, | |
numberOfGroups: 1, | |
groupPlacementRadius: 3, | |
playerId: -1, | |
landId: -1, | |
minDistanceToPlayers: -1, | |
maxDistanceToPlayers: -1, | |
minDistanceGroupPlacement: 0, | |
maxDistanceToOtherZones: 0 | |
}) | |
} | |
return | |
} | |
if (this.objects.length === 0) { | |
this.warn('Must use a create command before braces.') | |
return | |
} | |
const object = this.objects[this.objects.length - 1] | |
switch (id) { | |
case 0x31: // set_scaling_to_map_size | |
object.scalingType = 1 | |
break | |
case 0x57: // set_scaling_to_player_number | |
object.scalingType = 2 | |
break | |
case 0x32: // number_of_groups | |
if (object.groupingType === 0) { | |
object.amount = object.numberOfGroups | |
object.groupingType = 1 | |
} | |
object.numberOfGroups = args[0] | |
break | |
case 0x33: // number_of_objects | |
if (object.groupingType !== 0) { | |
object.amount = args[0] | |
} else { | |
object.numberOfGroups = args[0] | |
} | |
break | |
case 0x34: // group_variance | |
object.groupVariance = args[0] | |
break | |
case 0x35: // group_placement_radius | |
object.groupPlacementRadius = args[0] | |
break | |
case 0x36: // set_loose_grouping | |
if (object.groupingType === 0) { | |
object.amount = object.numberOfGroups | |
object.numberOfGroups = 1 | |
} | |
object.groupingType = 1 | |
break | |
case 0x37: // set_tight_grouping | |
if (object.groupingType === 0) { | |
object.amount = object.numberOfGroups | |
object.numberOfGroups = 1 | |
} | |
object.groupingType = 2 | |
break | |
case 0x38: // terrain_to_place_on | |
object.baseTerrain = args[0].value | |
break | |
case 0x39: // set_gaia_object_only | |
object.playerId = 0 | |
break | |
case 0x3A: // set_place_for_every_player | |
object.landId = 1 | |
break | |
case 0x3B: // place_on_specific_land_id | |
object.landId = args[0] + 10 | |
break | |
case 0x4E: // min_distance_group_placement | |
object.minDistanceGroupPlacement = args[0] | |
break | |
case 0x5D: // temp_min_distance_group_placement | |
break | |
case 0x3C: // min_distance_to_players | |
if (object.landId === -1) object.landId = -2 | |
object.minDistanceToPlayers = args[0] | |
break | |
case 0x3D: // max_distance_to_players | |
if (object.landId === -1) object.landId = -2 | |
object.maxDistanceToPlayers = args[0] | |
break | |
case 0x5B: // max_distance_to_other_zones | |
object.maxDistanceToOtherZones = args[0] | |
break | |
default: | |
this.warn('Command is not valid in this frame of reference.') | |
} | |
} | |
parseConnectionGeneration (token, args) { | |
const { id } = token | |
if (!this.insideBlock) { | |
if (id < 63 || id > 66) { | |
this.warn('Command is not valid in this frame of reference.') | |
return | |
} | |
const terrains = [] | |
for (let i = 0; i < 99; i += 1) { | |
terrains.push({ | |
terrainCost: 1, | |
terrainSize: 1, | |
terrainVariation: 0, | |
replaceTerrain: -1 | |
}) | |
} | |
this.connections.push({ | |
terrains, | |
type: id | |
}) | |
return | |
} | |
if (this.connections.length === 0) { | |
this.warn('Must use a create command before braces.') | |
return | |
} | |
const connection = this.connections[this.connections.length - 1] | |
switch (id) { | |
case 0x52: // default_terrain_placement | |
for (const terrain of connection.terrains) { | |
terrain.replaceTerrain = args[0].value | |
} | |
break | |
case 0x53: // replace_terrain | |
connection.terrains[args[0].value].replaceTerrain = args[1] | |
break | |
case 0x54: // terrain_cost | |
connection.terrains[args[0].value].terrainCost = args[1] | |
break | |
case 0x55: // terrain_size | |
connection.terrains[args[0].value].terrainSize = args[1] | |
connection.terrains[args[0].value].terrainVariation = args[2] | |
break | |
} | |
} | |
parseElevationGeneration (token, args) { | |
const { id } = token | |
if (!this.insideBlock) { | |
if (id === 80) { // create_elevation | |
const [ height ] = args | |
this.activeElevations = [] | |
for (let h = 0; h < height; h += 1) { | |
const elevation = { | |
numberOfTiles: 0, | |
height: Math.min(h, 7), | |
numberOfClumps: 1, | |
// Base level has spacing:2 by default. | |
spacing: h === 0 ? 2 : 1, | |
baseElevation: h | |
} | |
this.elevations.push(elevation) | |
this.activeElevations.push(elevation) | |
} | |
} | |
return | |
} | |
if (this.elevations.length === 0) { | |
this.warn('Must use a create command before braces.') | |
return | |
} | |
for (const elevation of this.activeElevations) { | |
switch (id) { | |
case 0x53: // spacing | |
elevation.spacing = args[0] | |
break | |
case 0x13: // base_terrain | |
elevation.baseTerrain = args[0].value | |
break | |
case 0x2D: // number_of_clumps | |
elevation.numberOfClumps = args[0] | |
break | |
case 0x4A: // number_of_tiles | |
elevation.numberOfTiles = args[0] | |
break | |
case 0x4B: // set_scale_by_groups | |
elevation.scalingType = 1 | |
break | |
case 0x4C: // set_scale_by_size | |
elevation.scalingType = 2 | |
break | |
case 0x4D: // set_avoid_player_start_areas | |
// This is the default behaviour. | |
break | |
} | |
} | |
} | |
readNextWord () { | |
const match = this.code.slice(this.index).match(/^(\S+)(\s+)/) | |
if (!match) return null | |
const word = match[1] | |
this.startIndex = this.index | |
this.endIndex = this.startIndex + word.length | |
this.index += match[0].length | |
const lines = countLines(match[0]) | |
if (lines > 0) { | |
this.line += lines | |
this.column = match[0].split(/\n/g)[lines].length | |
} else { | |
this.column += match[0].length | |
} | |
return word | |
} | |
parseSectionHeader (type) { | |
this.stage = type | |
this.stages.push({ | |
[TOK_PLAYER_SETUP]: 'players', | |
[TOK_LAND_GENERATION]: 'land', | |
[TOK_CLIFF_GENERATION]: 'cliffs', | |
[TOK_TERRAIN_GENERATION]: 'terrain', | |
[TOK_OBJECTS_GENERATION]: 'objects', | |
[TOK_CONNECTION_GENERATION]: 'connections', | |
[TOK_ELEVATION_GENERATION]: 'elevation' | |
}[type]) | |
} | |
readString () { | |
const word = this.readNextWord() | |
if (word) { | |
const token = this.getTokenType(word) | |
if (!token || token.value) { | |
return word | |
} | |
} | |
return null | |
} | |
readInt () { | |
const word = this.readNextWord() | |
if (word) { | |
const token = this.getTokenType(word) | |
if (!token || token.type !== TOKEN_TYPE_SYNTAX) { | |
return parseInt(word, 10) | |
} | |
} | |
return null | |
} | |
readToken () { | |
const word = this.readNextWord() | |
if (word) { | |
const token = this.getTokenType(word) | |
return token | |
} | |
return null | |
} | |
} | |
function countLines (str) { | |
return (str.match(/\n/g) || []).length | |
} | |
module.exports = Parser |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage: