DPTW = function(input){ const self = this, default_settings = { object_name : "dptw", dptw_autostart : true, settings_files : [ "/json/DPTW.settings.json", "/json/DPTW.settings.secrets.json" ], position : "body", ajax_timeout : 2000, dictionary_file : "/json/DPTW.dictionary.json", class : "dptw" }, sprites = {}, sounds = {}, images = {}; let started = false, game_working = false, game_thread = null, difficulty, character_size, last_word_y, velocity, velocity_increment, current_velocity, text_box, text_cache = "", destruction_animations, destruction_size, destruction_sounds, overwrite_sprites, overwrite_sounds, scream_sounds, ok_sounds, characters_sizer, reduce_life_value, increase_life_value, allow_alter_style, characters_color, allow_reduce_score, character_ok_points, character_wrong_points, word_ok_points, word_wrong_points, multiplier, allow_points_multiplier, zeros_fill_score, allow_pause, game_status = "loading", font_family, main_background, secondary_background, session_time_update, session_last_update = 0, sessions_show_update_ok_message, sessions_show_update_error_message, word_number, word_proportional_timeout, last_word_time, word_enabled_random_timer, allow_ignore_case, multiplier_value; this.sql_session_errors = [ "sql_exception", "session_string_null", "session_empty", "session_bad_format", "session_id_string_empty", "session_id_string_bad", "session_string_incompleted", "session_null", "session_not_id", "session_not_exists", "session_date_out", "session_timeout", null ]; let main_menu = self.main_menu; let kanvas = self.kanvas; let i18n = this.i18n; let errors = this.errors; let dictionary = this.dictionary; let scores = this.scores; const GAME = 2; const MAIN_MENU = 5; const FPS = 4; const WORDS = 0; const SHOTS = 1; const DESTRUCTIONS = 2; const LIFE = 6; const PAUSE = 7; const SCORE = this.SCORE = 8; const construct = () => { self.kanvas = kanvas = new Kanvas({ ...default_settings, ...(typeof input != "object" ? {} : input), autostart : false }); kanvas.extends(self); DPTW.I18N && (i18n = self.i18n = new DPTW.I18N(self, input)); DPTW.Errors && (errors = self.errors = new DPTW.Errors(self, input)); DPTW.Dictionary && (dictionary = self.dictionary = new DPTW.Dictionary(self, input)); DPTW.Scores && (scores = self.scores = new DPTW.Scores(self, input)); self.settings("dptw_autostart") && self.start(); }; this.start = callback => { const end = status => typeof callback == "function" && callback(status); if(started){ end(false); return false; }; started = true; self.load_blocks( self.set_array(self.settings("settings_files")), json => self.settings_add(json, true), () => { difficulty = self.settings("difficulty"); velocity = self.settings("velocity"); velocity_increment = self.settings("velocity_increment"); destruction_size = self.settings("destruction_size"); overwrite_sprites = self.settings(["overwrite_sprites", "overwrite"]); overwrite_sounds = self.settings(["overwrite_sounds", "overwrite"]); characters_sizer = self.settings("characters_sizer"); reduce_life_value = self.settings("reduce_life_value"); increase_life_value = self.settings("increase_life_value"); allow_alter_style = self.settings("allow_alter_style"); characters_color = self.settings("characters_color"); word_proportional_timeout = self.settings("word_proportional_timeout"); word_enabled_random_timer = self.settings("word_enabled_random_timer"); allow_ignore_case = self.settings("allow_ignore_case"); allow_reduce_score = self.settings("allow_reduce_score"); character_ok_points = self.settings("character_ok_points"); word_ok_points = self.settings("word_wrong_points"); character_wrong_points = self.settings("character_wrong_points"); word_wrong_points = self.settings("word_wrong_points"); allow_points_multiplier = self.settings("allow_points_multiplier"); zeros_fill_score = self.settings("zeros_fill_score"); allow_pause = self.settings("allow_pause"); multiplier_value = self.settings("multiplier_value"); font_family = self.settings("font_family"); main_background = self.settings("main_background"); secondary_background = self.settings("secondary_background"); session_time_update = self.settings("session_time_update"); sessions_show_update_ok_message = dptw.settings("sessions_show_update_ok_message"); sessions_show_update_error_message = dptw.settings("sessions_show_update_error_message"); i18n.start(status => { self.load_sprites(self.settings("sprites_files"), true, () => { const destruction_animations_keys = self.settings("destruction_animations"); destruction_animations = []; ( destruction_animations_keys instanceof Array ? destruction_animations_keys : typeof destruction_animations_keys == "string" ? [destruction_animations_keys] : [] ).forEach(key => sprites[key] && destruction_animations.push({ sprite : sprites[key].path, animation : sprites[key].animation, frames_per_second : sprites[key].frames_per_second })); self.load_sounds(self.settings("sounds_files"), true, () => { const sets = { destruction_sounds : destruction_sounds = [], scream_sounds : scream_sounds = [], ok_sounds : ok_sounds = [] }; for(const key in sets){ const set = self.settings(key); (set instanceof Array ? set : typeof set == "string" ? [set] : []).forEach(subkey => sounds[subkey] && sets[key].push({ sound : sounds[subkey].path, fragments : sounds[subkey].fragments, volume : sounds[subkey].volume || 1 })); }; kanvas.start(status => { dictionary.start(status => { scores.start(status => { builder(); game_thread = dptw.threads_add(game_thread_method); end(status); }); }); }); }); }); }); }, 0 ); return true; }; this.load_file = (url, callback) => { let ended = false; const ajax = new XMLHttpRequest(), end = error_message => { if(ended) return; ended = true; callback(ajax.responseText, ajax.status, ajax.readyState, error_message == "OK", error_message); }, date = Date.now(), timeout = self.settings(["ajax_timeout", "timeout"]); ajax.open("get", url, true); ajax.timeout = timeout; ajax.onreadystatechange = () => { if(ended) return; if(ajax.readyState == 4) end((ajax.status >= 200 && ajax.status < 300) || [301, 302, 304].includes(ajax.status) ? "OK" : "HTTP_ERROR"); else if(Date.now() - date > timeout) end("FORCED_TIMEOUT"); }; ajax.send(null); ajax.onerror = () => end("ERROR"); ajax.onabort = () => end("ABORTED"); ajax.ontimeout = () => end("TIMEOUT"); return ajax; }; this.set_array = variable => ( variable instanceof Array ? variable : ["string", "object"].includes(typeof variable) ? [variable] : []) this.load_blocks = (inputs, fragment_callback, callback, i) => { if(i == inputs.length){ typeof callback == "function" && callback(); return; }; const end = () => self.load_blocks(inputs, fragment_callback, callback, i + 1); if(inputs[i]){ if(inputs[i] instanceof Array) self.load_blocks(inputs[i], fragment_callback, end, 0); else if(typeof inputs[i] == "object"){ fragment_callback(inputs[i]); end(); }else if(typeof inputs[i] == "string"){ let json; try{ json = JSON.parse(inputs[i]); }catch(exception){}; if(json) self.load_blocks(self.set_array(json), fragment_callback, end, 0); else self.load_file(inputs[i], response => { try{ json = JSON.parse(response); }catch(exception){}; self.load_blocks(self.set_array(json), fragment_callback, end, 0); }); }else end(); }else end(); }; this.load_sprites = (inputs, overwrite, callback) => this.load_blocks(inputs instanceof Array ? inputs : inputs ? [inputs] : [], json => { typeof overwrite != "boolean" && (overflow = overwrite_sprites); (json.sprites || []).forEach(sprite => { if( sprite && sprite.enabled && sprite.key ){ if(sprite.animation) (overwrite || !sprites[sprite.key]) && (sprites[sprite.key] = sprite); else (overwrite || !images[sprite.key]) && (images[sprite.key] = sprite); }; }); }, callback, 0); this.load_sounds = (inputs, overwrite, callback) => this.load_blocks(inputs instanceof Array ? inputs : inputs ? [inputs] : [], json => { typeof overwrite != "boolean" && (overflow = overwrite_sounds); (json.sounds || []).forEach(sound => ( sound && sound.enabled && sound.key && (overwrite || !sounds[sound.key]) && (sounds[sound.key] = sound) )); }, callback, 0); const create_button = (key, x, y, width, height) => ({ type : "rectangle", x : x, y : y, width : width || 16, height : height || 3, background : "rgba(255, 255, 255, .3)", on_click : "{object_name}.go_to_" + key, childs : [{ type : "text", x : 8, y : .5, align : "center", baseline : "top", text : i18n.get(key), background : "#FFF", size : 2, border_width : .05, border_color : "#000", style : "bold", family : font_family, shadows : [ [0, 0, .125, "#000"], [0, 0, .25, "#000"], [0, 0, .5, "#000"], [0, 0, 1, "#000"] ] }] }); const show_menu = this.show_menu = () => { const half = kanvas.cells / 2, menu = {type : "block", x : -half, y : -half, childs : [ {type : "rectangle", x : 0, y : 0, width : kanvas.cells, height : kanvas.cells, background : "#000", alpha : .7}, {type : "block", x : half - 8, y : half - 9.5, childs : [{ type : "text", x : 8, y : -5, align : "center", baseline : "top", text : i18n.get("test_mode"), background : "#FFF", size : 4, border_width : .05, border_color : "#000", style : "bold", family : font_family, shadows : [ [0, 0, .25, "#000"] ] }]} ]}; game_status = "main_menu"; ["start", "settings", "scores", "manual", "credits"].forEach((key, i) => menu.childs[1].childs.push(create_button(key, 0, (i * 4) + .5))); kanvas.map[MAIN_MENU] = menu; }; const hide_menu = () => kanvas.map[MAIN_MENU] = null; const builder = () => { const half = kanvas.cells / 2, margin = kanvas.cells / 8, padding = half + margin, full_padding = padding * 2, margined = half - margin; game_status = "building"; errors.build_toast(); kanvas.map.push( {type : "rectangle", x : -kanvas.cells_x, y : -kanvas.cells_y, width : kanvas.cells_x * 2, height : kanvas.cells_y * 2, background : "#000", childs : images[secondary_background] ? [ {type : "image", x : 0, y : 0, width : kanvas.cells_x * 2, height : kanvas.cells_y * 2, url : images[secondary_background].path}, {type : "rectangle", x : 0, y : 0, width : kanvas.cells_x, height : kanvas.cells_y, background : "#000", alpha : .85} ] : []}, {type : "cache", name : "main_background_back", x : -padding, y : -padding, width : full_padding, height : full_padding, childs : [ images[main_background] ? {type : "image", x : -half, y : -half, width : kanvas.cells, height : kanvas.cells, url : images[main_background].path} : null, {type : "rectangle", x : -half, y : -half, width : kanvas.cells, height : kanvas.cells, background : "#246", alpha : .5} ]}, null, {type : "cache", name : "main_background_fore", x : -half, y : -half, width : kanvas.cells, height : kanvas.cells, childs : [ {type : "rectangle", x : -half, y : -half, width : kanvas.cells, height : margin, background : [0, -half, 0, -margined, [ [0, "#000"], [1, "rgba(0, 0, 0, 0)"] ]]}, {type : "rectangle", x : -half, y : -half, width : margin, height : kanvas.cells, background : [-half, 0, -margined, 0, [ [0, "#000"], [1, "rgba(0, 0, 0, 0)"] ]]}, {type : "rectangle", x : -half, y : margined, width : kanvas.cells, height : margin, background : [0, half, 0, margined, [ [0, "#000"], [1, "rgba(0, 0, 0, 0)"] ]]}, {type : "rectangle", x : margined, y : -half, width : margin, height : kanvas.cells, background : [half, 0, margined, 0, [ [0, "#000"], [1, "rgba(0, 0, 0, 0)"] ]]}, {type : "rectangle", x : -half, y : -half, width : kanvas.cells, height : kanvas.cells, border_color : "#147", border_width : .4, shadow : [ [0, 0, .5, "#28F"] ]} ]}, {type : "text", x : kanvas.cells_x, y : -kanvas.cells_y, align : "right", baseline : "top", text : "0.00", background : "#FFF"}, null, {type : "rectangle", x : -half + 1, y : -half + 1, width : kanvas.cells - 2, height : 1, alpha : .3, border_color : "#000", border_width : .1, background : "#F00", shadow : [ [0, 0, .1, "#FFF"] ], childs : [ {type : "rectangle", x : .1, y : .1, width : kanvas.cells - 2.2, full : kanvas.cells - 2.2, height : .8, background : "#0F0"} ]}, null, {type : "text", x : kanvas.cells_x, y : kanvas.cells_y, baseline : "bottom", align : "right", text : zeros_fill_score, style : "bold", background : "#FFF", size : 2}, null ); show_menu(); kanvas.on_screen_change.add(() => { with(kanvas.map[0]){ x = -kanvas.cells_x; y = -kanvas.cells_y; }; [kanvas.map[0], kanvas.map[0].childs[0], kanvas.map[0].childs[1]].forEach(level => { if(level){ level.width = kanvas.cells_x * 2; level.height = kanvas.cells_y * 2; }; }); with(kanvas.map[FPS]){ x = kanvas.cells_x - 1; y = -kanvas.cells_y + .3; }; with(kanvas.map[SCORE]){ x = kanvas.cells_x; y = kanvas.cells_y; }; }); kanvas.threads_add(() => { kanvas.map[FPS].text = kanvas.get_real_fps().toFixed(2); }); text_box = kanvas.item_self.appendChild(document.createElement("textarea")); ["up", "down"].forEach(action => text_box.addEventListener("key" + action, check_characters)); kanvas.item_self.querySelector(".kanvas-ui").addEventListener("click", set_focus); }; this.go_to_start = (item, event) => { hide_menu(); start_game(); }; this.go_to_settings = (item, event) => console.log(["settings", item, event]); this.go_to_scores = (item, event) => { kanvas.map[MAIN_MENU] = null; scores.build(); }; this.go_to_manual = (item, event) => window.open("/doc", "_blank"); this.go_to_credits = (item, event) => console.log(["credits", item, event]); const start_game = () => { self.map[GAME] = {type : "block", x : -kanvas.cells / 2, y : -kanvas.cells / 2, childs : [ {type : "block", x : 0, y : 0, childs : []}, {type : "block", x : 0, y : 0, childs : []}, {type : "block", x : 0, y : 0, childs : []} ]}; character_size = kanvas.cells / dictionary.maximum_characters, last_word_y = kanvas.cells; current_velocity = 0 + velocity; text_cache = ""; text_box.value = ""; self.map[LIFE].childs[0].width = self.map[LIFE].childs[0].full; self.map[GAME].childs[WORDS].childs = []; kanvas.map[SCORE].text = zeros_fill_score; multiplier = 1; word_number = 0; last_word_time = 0; game_status = "running"; game_working = true; }; const reduce_life = () => { if((self.map[LIFE].childs[0].width -= reduce_life_value * difficulty) < 0){ self.map[LIFE].childs[0].width = 0; end_game(); }; }; const increase_life = () => ( (self.map[LIFE].childs[0].width += increase_life_value * (1 - difficulty)) > self.map[LIFE].childs[0].full && (self.map[LIFE].childs[0].width = self.map[LIFE].childs[0].full )); const set_score = points => { if(allow_reduce_score || points > 0){ const new_points = Number(kanvas.map[SCORE].text) + points; kanvas.map[SCORE].text = (zeros_fill_score + (new_points < 0 ? 0 : new_points)).slice(-zeros_fill_score.length); }; }; const game_thread_method = () => { const date = Date.now(); if(date - session_last_update > session_time_update){ session_last_update = date; self.load_file("/api/sessions/update", response => { console.log(response); response = JSON.parse(response); session_last_update = Date.now(); errors.validate( response.content.error, [].concat(self.sql_session_errors), {}, sessions_show_update_error_message && "session_update_error", sessions_show_update_ok_message && "session_update_ok" ); }); }; if(!game_working) return; const real_velocity = (current_velocity += (kanvas.delta_time * velocity_increment * difficulty)); if( !word_number || ( word_enabled_random_timer && last_word_y >= character_size && Math.random() * (1 / kanvas.get_real_fps()) < difficulty / 10000 ) || ( word_proportional_timeout && date - last_word_time > word_proportional_timeout * (1 - (real_velocity / kanvas.cells)) ) ){ let i = 0; const l = self.map[GAME].childs[WORDS].childs.length, word = dictionary.get_random_word(); last_word_y = 0; word_number ++; last_word_time = date; for(; i < l; i ++) if(!self.map[GAME].childs[WORDS].childs[i]) break; const x = Math.random() * (dictionary.maximum_characters - word.length + 1) >> 0, characters = [], half = character_size / 2; word.split("").forEach((character, j) => characters.push({ type : "text", x : (character_size * j) + half, y : half, align : "center", baseline : "middle", text : character, size : character_size * (allow_alter_style ? (1 - characters_sizer) + (Math.random() * characters_sizer) : 1), style : allow_alter_style ? Math.random() > .5 ? "bold" : null : "bold", background : characters_color || "rgb(" + [0, 0, 0].map(_ => Math.random() * 256 >> 0).join(",") + ")", shadow : [ [0, 0, .5, "#FFF"], [0, 0, .35, "#FFF"], [0, 0, .2, "#000"] ] })); self.map[GAME].childs[WORDS].childs[i] = {type : "block", x : character_size * x, y : 0, alpha : 0, childs : characters}; }; last_word_y += real_velocity; self.map[GAME].childs[WORDS].childs.forEach((word, j) => { if(word){ if(word.y > kanvas.cells - character_size){ word.childs.forEach(character => { if(!character) return; set_destruction(word.x + character.x, word.y); play_audios_from(scream_sounds);characters_color reduce_life(); set_score(character_wrong_points); }); self.map[GAME].childs[WORDS].childs[j] = null; set_score(word_wrong_points); multiplier = 1; }else{ word.alpha != 1 && (word.alpha += real_velocity / 5) > 1 && (word.alpha = 1); word.y += real_velocity; }; }; }); }; const set_focus = event => text_box.focus(); this.go_to_resume = () => self.end_pause(); this.go_to_left_match = () => { self.end_pause(); end_game(); }; this.start_pause = () => { if(allow_pause && game_status == "running"){ const half = kanvas.cells / 2; game_status = "stopped"; game_working = false; kanvas.map[PAUSE] = {type : "block", x : -half, y : -half, childs : [ {type : "rectangle", x : 0, y : 0, width : kanvas.cells, height : kanvas.cells, background : "#000", alpha : .7}, {type : "text", x : half, y : half - 10, baseline : "middle", align : "center", text : i18n.get("paused"), size : 4, style : "bold", shadow : [ [0, 0, .35, "#BBB"] ]}, create_button("resume", half - 8, half - 3), create_button("left_match", half - 8, half + 1) ]} return true; }; return false; }; this.end_pause = () => { if(game_status == "stopped"){ game_status = "running"; game_working = true; kanvas.map[PAUSE] = null; return true; }; return false; }; const check_characters = event => { if(event.type == "keyup") switch(event.keyCode){ case 27: self.start_pause() || self.end_pause(); default: break; }; if(text_box.value == text_cache) return; const last_changed = text_cache.length == text_box.value.length, characters = text_box.value.substr(text_cache.length - (last_changed ? 1 : 0)).split(""); text_cache = text_box.value; if(game_status != "running") return; characters.forEach(own_character => { allow_ignore_case && (own_character = own_character.toLowerCase()); if(!self.map[GAME].childs[WORDS].childs.some((word, i) => { const ok = word && word.childs.some((character, j) => { const ok = character && (allow_ignore_case ? character.text.toLowerCase() : character.text) == own_character; if(ok){ with(self.map[GAME].childs[WORDS].childs[i].childs[j]) set_destruction( self.map[GAME].childs[WORDS].childs[i].x + x, self.map[GAME].childs[WORDS].childs[i].y + y ); self.map[GAME].childs[WORDS].childs[i].childs[j] = null; set_score(character_ok_points * (allow_points_multiplier ? multiplier : 1)); multiplier += multiplier_value; }; return ok; }); if(ok && self.map[GAME].childs[WORDS].childs[i].childs.every(character => !character)){ self.map[GAME].childs[WORDS].childs[i] = null; play_audios_from(ok_sounds); increase_life(); set_score(word_ok_points); }; return ok; })){ if(!['`', '´', '^',, '¨'].includes(own_character)){ play_audios_from(scream_sounds); reduce_life(); set_score(character_wrong_points); multiplier = 1; }; }; }); }; const play_audios_from_uri = (uri_data, volume, from, to) => { const audio = new Audio(uri_data); audio.load(); audio.currentTime = from; audio.volume = volume; audio.play(); setTimeout(() => audio.pause(), (to ? to - from : audio.duration) * 1000); }; const play_audios_from = set => { const sound = set.length ? set[Math.random() * set.length >> 0] : null, [from, to] = sound.fragments ? sound.fragments[Math.random() * sound.fragments.length >> 0] : [0, 0]; if(!sound) return; if(sound.uri_data){ play_audios_from_uri(sound.uri_data, sound.volume, from, to); return; }else if(sound.loading) return; const ajax = new XMLHttpRequest(); sound.loading = true; ajax.open("get", sound.sound, true); ajax.responseType = "blob"; ajax.onload = () => { if(ajax.readyState == 4){ ajax.loading = false; ajax.status == 200 && play_audios_from_uri(sound.uri_data = URL.createObjectURL(ajax.response), sound.volume, from, to); }; }; ajax.send(null); }; const set_destruction = (x, y) => { const i = Math.random() * destruction_animations.length >> 0, m = self.map[GAME].childs[DESTRUCTIONS].childs.length, half = (character_size / 2) - (destruction_size / 2); let thread, j = 0, k = 0; for(; j < m; j ++) if(!self.map[GAME].childs[DESTRUCTIONS].childs[j]) break; self.map[GAME].childs[DESTRUCTIONS].childs[j] = { type : "image", x : x + half, y : y + half, width : destruction_size, height : destruction_size, cut_x : destruction_animations[i].animation[0][0], cut_y : destruction_animations[i].animation[0][1], cut_width : destruction_animations[i].animation[0][2], cut_height : destruction_animations[i].animation[0][3], url : destruction_animations[i].sprite }; play_audios_from(destruction_sounds); thread = kanvas.threads_add(() => { if(k < destruction_animations[i].animation.length){ const _k = k >> 0; with(self.map[GAME].childs[DESTRUCTIONS].childs[j]){ cut_x = destruction_animations[i].animation[_k][0]; cut_y = destruction_animations[i].animation[_k][1]; cut_width = destruction_animations[i].animation[_k][2]; cut_height = destruction_animations[i].animation[_k][3]; }; k += destruction_animations[i].frames_per_second / kanvas.get_real_fps(); }else{ kanvas.threads_remove(thread); self.map[GAME].childs[DESTRUCTIONS].childs[j] = null; }; }); }; const end_game = () => { game_status = "ended"; game_working = false; scores.build_add(); }; construct(); };