UNPKG

creatures

Version:

Library to interface with the Creatures 2 game

718 lines (605 loc) 13.5 kB
var Extractor = require('binary-extractor'), Blast = __Protoblast, Nr = Blast.Bound.Number, Fn = Blast.Collection.Function; // Get the Creatures namespace var Creatures = Fn.getNamespace('Develry.Creatures'); /** * The Genome class * * @author Jelle De Loecker <jelle@develry.be> * @since 0.1.0 * @version 0.2.0 * * @param {CreaturesApplication} app * @param {String} path */ var Genome = Fn.inherits('Develry.Creatures.Base', function Genome(app, path) { // The parent creatures app this.app = app; // The path to the file this.path = path; // The genes this.genes = []; }); /** * Possible genetypes * * @author Jelle De Loecker <jelle@develry.be> * @since 0.1.0 * @version 0.1.0 */ Genome.setProperty('types', { '00' : { name : 'Brain lobe', data : [ // General 'x', 'y', 'width', 'height', 'perception', // Cell body 'threshold', 'leakage', 'rest_state', 'input_gain', {name: 'state', length: 8}, 'winner_takes_all', '__skip__' //{name: 'dendrite_0', length: 47}, //{name: 'dendrite_1', length: 47} ] }, '01' : { name : 'Brain organ', data : [ 'clock_rate', 'life_force_repair_rate', 'life_force_start', 'biotick_start', 'atp_damage_coefficient' ] }, '10' : { name : 'Receptor', data : [ 'organ', 'tissue', 'locus', 'chemical', 'threshold', 'nominal', 'gain', 'flags' ] }, '11' : { name : 'Emitter', data : [ 'organ', 'tissue', 'locus', 'chemical', 'threshold', 'sample_rate', 'gain', 'flags' ] }, '12' : { name : 'Chemical reaction', data : [ 'l1a', 'l1c', 'l2a', 'l2c', 'r1a', 'r1c', 'r2a', 'r2c', 'reaction_rate' ] }, '13' : { name : 'Half lives', data : [ {name: 'values', length: 256} ] }, '14' : { name : 'Initial concentration', data : [ 'chemical', 'amount' ] }, '20' : { name : 'Stimulus', data : [ 'type', 'significance', 'sensory_neuron', 'intensity', {name: 'flags', values: ['modulate', 'neuron_offset', 'sensed_when_asleep']}, 'chemical_1', 'chemical_1_amount', 'chemical_2', 'chemical_2_amount', 'chemical_3', 'chemical_3_amount', 'chemical_4', 'chemical_4_amount' ] }, '21' : { name : 'Genus', data : [ {name: 'species', values: ['norn', 'grendel', 'ettin', 'shee']}, {name: 'mother_moniker', length: 4}, {name: 'father_moniker', length: 4} ] }, '22' : { name : 'Appearance', data : [ {name: 'body_part', values: ['head', 'body', 'leg', 'arm', 'tail']}, 'breed', 'species' ] }, '23' : { name : 'Pose', data : [ 'pose_number', {name: 'pose_string', length: 15} ] }, '24' : { name : 'Gait', data : [ 'gait_number', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8' ] }, '25' : { name : 'Instinct', data : [ 'lobe_nr_1', 'cell_nr_1', 'lobe_nr_2', 'cell_nr_2', 'lobe_nr_3', 'cell_nr_3', 'creature_action', 'reward_or_punish', 'amount' ] }, '26' : { name : 'Pigment', data : [ {name: 'color', values: ['red', 'green', 'blue']}, 'intensity' ] }, '27' : { name : 'Pigment bleed', data : [ 'rotation', 'swap' ] }, '30' : { name : 'Organ', data : [ 'clock_rate', 'life_force_repair_rate', 'life_force_start', 'biotick_start', 'atp_damage_coefficient' ] } }); /** * Read the file and process it * * @author Jelle De Loecker <jelle@develry.be> * @since 0.1.0 * @version 0.2.4 * * @param {Boolean} refresh * @param {Function} callback */ Genome.setMethod(function load(refresh, callback) { var that = this; if (typeof refresh == 'function') { callback = refresh; refresh = false; } if (!callback) { callback = Fn.Thrower; } // Callback if this has already been loaded and a refresh is not needed if (this.loaded && !refresh) { return Blast.setImmediate(function immediate() { callback(null); }); } this.readFile(this.path, function gotFileBuffer(err, buffer) { if (err) { return callback(err); } // Process the buffer that.processBuffer(buffer); that.loaded = true; return callback(null); }); }); /** * Process the buffer * * @author Jelle De Loecker <jelle@develry.be> * @since 0.1.0 * @version 0.2.6 * * @param {Buffer|Extractor} buffer * @param {Boolean} from_export Is this buffer from an export file? * * @return {Number} The next index in the buffer that isn't a gene */ Genome.setMethod(function processBuffer(buffer, from_export) { var gene, temp, gen; if (Buffer.isBuffer(buffer)) { gen = new Extractor(buffer) } else { gen = buffer; buffer = gen.buffer; } if (!from_export) { // Get the header first temp = gen.readBytes(3).toString(); if (temp != 'dna') { throw new Error('Can\'t read file: not a genome'); } // Get the version of the genome file this.version = Number(gen.readBytes(1).toString()); } else { // Jump to the first gene part after the CGenome string gen.index = buffer.indexOf('gene', buffer.indexOf('CGenome')); } // Reset the genes array this.genes.length = 0; // Consume the buffer while ((temp = gen.readBytes(4).toString()) == 'gene') { // Get the gene header gene = { // 00, 01, 02 or 03 type : gen.readByte(), subtype : gen.readByte(), // Specific gene within a type/subtype category sequence : gen.readByte(), // Appears to indicate a duplicate gene duplicate : gen.readByte(), // The "switch-on" time for the gene stage : gen.readByte(), // Contains sex dependence, mutability & dormancy flags : gen.readByte(), mutation_chance : gen.readByte() }; // Extract the flags gene.mutable = Nr.bitAt(gene.flags, 0); gene.duplicable = Nr.bitAt(gene.flags, 1); gene.deletable = Nr.bitAt(gene.flags, 2); gene.male_only = Nr.bitAt(gene.flags, 3); gene.female_only = Nr.bitAt(gene.flags, 4); gene.dormant = Nr.bitAt(gene.flags, 5); gene.male = gene.male_only || gene.female_only == 0; gene.female = gene.female_only || gene.male_only == 0; // Now get the data itself gene.data = this.extractGeneData(gene, gen); this.genes.push(gene); } if (this.genes.length < 50) { console.warn('Too few genes found'); } return gen.index - 4; }); /** * Extract gene data * * @author Jelle De Loecker <jelle@develry.be> * @since 0.1.0 * @version 0.2.4 * * @param {Object} header * @param {BinaryExtractor} gen */ Genome.setMethod(function extractGeneData(header, gen) { var typestr = String(header.type) + String(header.subtype), result = {}, piece, info, temp, i; // Get the name of the type of this gene info = this.types[typestr]; if (!info) { this.log('error', 'Unable to find info on gene type', typestr); console.info('Header of unknown gene:', header); return; } // Get the name header.name = info.name; for (i = 0; i < info.data.length; i++) { piece = info.data[i]; // If it's just a string, only 1 byte is needed if (typeof piece == 'string') { if (piece == '__skip__') { temp = gen.after('gene'); if (temp != -1) { gen.index -= 4; } } else { result[piece] = gen.readByte(); } } else { if (piece.length) { result[piece.name] = gen.readBytes(piece.length); } else { temp = gen.readByte(); result[piece.name] = temp; // Add the value name if (piece.values) { result[piece.name + '_name'] = piece.values[temp]; } } } } return result; }); /** * Get all genes of a specific type * * @author Jelle De Loecker <jelle@develry.be> * @since 0.2.0 * @version 0.2.0 * * @param {String} name * * @return {Array} */ Genome.setMethod(function getGenesOfName(name) { var result = [], gene, i; for (i = 0; i < this.genes.length; i++) { gene = this.genes[i]; if (gene.name == name) { result.push(gene); } } return result; }); /** * Get pigment info * * @author Jelle De Loecker <jelle@develry.be> * @since 0.2.4 * @version 0.2.4 * * @return {Object} */ Genome.setMethod(function getPigmentInfo(creature) { var intensity, result = {}, genes, gene, r, g, b, i; // Get the pigment genes genes = this.getGenesOfName('Pigment'); // Prepare the rgb arrays r = []; g = []; b = []; for (i = 0; i < genes.length; i++) { gene = genes[i]; if (creature) { // Skip genes that are not for this gender if ((creature.male && !gene.male) || (creature.female && !gene.female)) { continue; } } // Skip dormant genes if (gene.dormant) { continue; } if (gene.data && gene.data.color_name) { intensity = 1 + (0.125 * ((gene.data.intensity - 128) / 128)); switch (gene.data.color_name) { case 'red': r.push(intensity); break; case 'green': g.push(intensity); break; case 'blue': b.push(intensity); break; } } } if (r.length) { r = Creatures.S16.meanReduce(r); } else { r = 1; } if (g.length) { g = Creatures.S16.meanReduce(g); } else { g = 1; } if (b.length) { b = Creatures.S16.meanReduce(b); } else { b = 1; } result = { r : r, g : g, b : b }; return result; }); /** * Get pigment bleed info * * @author Jelle De Loecker <jelle@develry.be> * @since 0.2.4 * @version 0.2.4 * * @return {Object} */ Genome.setMethod(function getPigmentBleedInfo(creature) { var result = [], rotations = [], swaps = [], genes, gene, i; // Get the pigment genes genes = this.getGenesOfName('Pigment bleed'); for (i = 0; i < genes.length; i++) { gene = genes[i]; if (creature) { // Skip genes that are not for this gender if ((creature.male && !gene.male) || (creature.female && !gene.female)) { continue; } } // Skip dormant genes if (gene.dormant) { continue; } if (gene.data) { result.push(gene.data); // Calculate the rotation coefficient gene.data.rotation_coef = (0.33 * ((gene.data.rotation - 128) / 128)); if (gene.data.rotation >= 128) { gene.data.abs_rot = gene.data.rotation - 128; } else { gene.data.abs_rot = 128 - gene.data.rotation; } gene.data.inv_rot = 127 - gene.data.abs_rot; // Calculate the swap coefficient gene.data.swap_coef = Math.abs(1 * ((gene.data.swap - 128) / 128)); if (gene.data.swap >= 128) { gene.data.abs_swap = gene.data.swap - 128; } else { gene.data.abs_swap = 128 - gene.data.swap; } gene.data.inv_swap = 127 - gene.data.abs_swap; } } return result; }); /** * Get a specific body image of this creature * * @author Jelle De Loecker <jelle@develry.be> * @since 0.2.0 * @version 0.2.4 * * @param {Export|Creature} creature * @param {String} part_name The body part type * @param {Function} callback * * @return {Creatures.S16} */ Genome.setMethod(function getBodyPartImage(creature, part_name, callback) { var that = this, breed_char, filename, try_age, prefix, parts, genes, gene, s16, i; parts = { head : 'A', body : 'B', left_thigh : 'C', left_shin : 'D', left_foot : 'E', right_thigh : 'F', right_shin : 'G', right_foot : 'H', left_humerus : 'I', left_upper_arm : 'I', left_radius : 'J', left_forearm : 'J', right_radius : 'K', right_forearm : 'K', // Tails are not in C1 tail_root : 'M', tail_tip : 'N', // Ears & hair are present in CV // but supported in DS left_ear : 'O', right_ear : 'P', hair : 'Q' }; // The first char of the filename is the body part prefix = parts[part_name]; // Get the appearance genes genes = that.getGenesOfName('Appearance'); for (i = 0; i < genes.length; i++) { gene = genes[i]; if (gene.data.body_part_name == part_name) { break; } else { gene = null } } if (!gene) { console.warn('Could not find', part_name, 'gene in', genes, this); return callback(null); } // Add the species number if (creature.female) { prefix += (gene.data.species + 4); } else { prefix += gene.data.species; } // Not all lifestages are available, // try with the current one first try_age = creature.agen || 0; // The last part of the filename is the breed character breed_char = String.fromCharCode(65 + gene.data.breed); // The filename we want filename = prefix + try_age + breed_char + '.s16'; // Create the s16 instance s16 = new Creatures.S16(that.app, filename); // Set the pigment info s16.setPigment(this.getPigmentInfo(creature)); s16.setPigmentBleed(this.getPigmentBleedInfo(creature)); s16.load(function loaded(err) { if (err) { if (err.code == 'ENOENT' && try_age > 0) { try_age--; // Construct the new filename of a lower lifestage // (Ettins for example only have sprites for lifestage 0) filename = prefix + try_age + breed_char + '.s16'; s16 = new Creatures.S16(that.app, filename); return s16.load(loaded); } return callback(err); } callback(null, s16); }); return s16; }); module.exports = Genome;