/** * Copyright (C) 2014-2016 Triumph LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ "use strict"; /** * Non-player character add-on. * Provides animated moves for NPC with a specified behavior. * @module npc_ai */ b4w.module["npc_ai"] = function(exports, require) { var m_anim = require("animation"); var m_ctl = require("controls"); var m_quat = require("quat"); var m_scs = require("scenes"); var m_time = require("time"); var m_trans = require("transform"); var m_vec3 = require("vec3"); var m_print = require("print"); var m_util = require("util"); var _ev_tracks = []; var _mat3_tmp = new Float32Array(9); var _vec3_tmp = new Float32Array(3); var _vec3_tmp_2 = new Float32Array(3); var _vec3_tmp_3 = new Float32Array(3); var _vec3_tmp_4 = new Float32Array(3); var _quat4_tmp = new Float32Array(4); var _quat4_tmp_2 = new Float32Array(4); var _quat4_tmp_3 = new Float32Array(4); /** * NPC movement type - walking. * @const {Number} module:npc_ai.NT_WALKING */ var NT_WALKING = exports.NT_WALKING = 10; /** * NPC movement type - flying. * @const {Number} module:npc_ai.NT_FLYING */ var NT_FLYING = exports.NT_FLYING = 20; /** * NPC movement type - swimming. * @const {Number} module:npc_ai.NT_SWIMMING */ var NT_SWIMMING = exports.NT_SWIMMING = 30; var MS_IDLE = 10; var MS_MOVING = 20; var NPC_MAX_ACTIVITY_DISTANCE = 100; /** * Creates a new event track based on a given graph. * @param {Object} graph Animation graph with a number of movement params. * @param {Array} graph.path A list of [x,y,z] points NPC will be moving around. * @param {Number} graph.delay Time delay before each path step. * @param {Object3D} graph.actions Actions for every movement type (move, idle, etc). * @param {Object3D} graph.obj Animated object ID. * @param {Object3D} graph.rig Armature object ID. * @param {Object3D} graph.collider Collider object ID which will be used for collision detection. * @param {Number} graph.speed Movement speed. * @param {Number} graph.rot_speed Rotation speed. * @param {Boolean} graph.random Determines whether the object will perform random moves or not. * @param {Number} graph.type NPC movement type (NT_WALKING, NT_FLYING, etc). * @method module:npc_ai.new_event_track * @cc_externs path delay actions obj collider speed random rig * @cc_externs type rot_speed max_height min_height */ exports.new_event_track = function(graph) { var track = init_event_track(); if (typeof graph.obj != "object") { m_print.error("Can't create event track. Wrong object."); return; } if (typeof graph.rig != "object") { m_print.error("Can't create event track. Wrong rig object"); return; } if (typeof graph.collider != "object") { m_print.error("Can't create event track. Wrong collider object"); return; } track.obj = graph.obj; track.collider = graph.collider; track.rig = graph.rig; m_trans.get_translation(graph.collider, track.base_pos); if (typeof graph.actions == "object") track.actions = graph.actions; if (typeof graph.random == "boolean") track.random = graph.random; if (typeof graph.type == "number") track.type = graph.type; if (typeof graph.speed == "number") track.speed = graph.speed; if (typeof graph.rot_speed == "number") track.rot_speed = graph.rot_speed; if (typeof graph.max_height == "number") track.max_height = graph.max_height; if (typeof graph.min_height == "number") track.min_height = graph.min_height; if (!track.random) { for (var i = 0; i < graph.path.length; i++) { track.start[i] = -1; // starting time for current move track.ended[i] = false; track.fired[i] = false; } track.path = graph.path; track.delay = graph.delay; } // apply all available animations so that they are precached var actions = graph.actions; var cur_anim = m_anim.get_current_anim_name(track.rig) for (var list in actions) { var act_list = actions[list]; for (var j = 0; j < act_list.length; j++) m_anim.apply(track.rig, act_list[j]); } // restore animation if (cur_anim) m_anim.apply(track.rig, cur_anim); _ev_tracks.push(track); } function init_event_track() { return { obj: null, rig: null, collider: null, empty: null, type: NT_WALKING, state: MS_IDLE, actions: {}, base_pos: new Float32Array(3), destination: new Float32Array(3), path: [], start: [], ended: [], fired: [], delay: [], random: true, reached: true, ground_level: 0, vert_correction: 0, vert_cor_water: 0, rotation_mult: 1.0, speed: 1, rot_speed: 0.1, max_height: 0, min_height: 0 } } /** * Calculates destination coordinates and * runs anim_translation function */ function run_track(elapsed, ev_track) { var destination = ev_track.destination; var base_pos = ev_track.base_pos; var current_loc = _vec3_tmp_2; m_trans.get_translation(ev_track.collider, current_loc); var vert_correction = 0; var cur_height = current_loc[2]; destination[2] = cur_height; switch (ev_track.type) { case NT_WALKING: if (ev_track.random && ev_track.reached) { destination[0] = (Math.random()*12 - 6)*ev_track.speed + base_pos[0]; destination[1] = -(Math.random()*12 - 6)*ev_track.speed + base_pos[1]; move_destination_if_too_close(ev_track, destination, current_loc); ev_track.reached = false; } if (ev_track.ground_level) { var vert_correction = ev_track.ground_level - cur_height; var vert_delta = ev_track.speed * elapsed; if (vert_correction > vert_delta) vert_correction = vert_delta; else if (vert_correction < -vert_delta) vert_correction = -vert_delta; } ev_track.vert_correction = vert_correction; break; case NT_FLYING: case NT_SWIMMING: if (ev_track.vert_cor_water) { vert_correction = ev_track.vert_cor_water; } else if (ev_track.vert_correction) { vert_correction = ev_track.vert_correction; if (ev_track.type == NT_SWIMMING) vert_correction *= elapsed; } if (ev_track.random && ev_track.reached) { if (ev_track.type == NT_SWIMMING) { ev_track.speed = Math.random() + 0.1; var magnitude = ev_track.max_depth - ev_track.min_depth; } else var magnitude = ev_track.max_height - ev_track.min_height; var rot_speed = ev_track.rot_speed; destination[0] = Math.random() * 10 - 5 + base_pos[0]; destination[1] = Math.random() * 10 - 5 + base_pos[1]; destination[2] = cur_height - (Math.random() * magnitude - 0.5 * magnitude); ev_track.reached = false; } break; } if (vert_correction) destination[2] = cur_height + vert_correction; anim_translation(elapsed, ev_track); } function move_destination_if_too_close(ev_track, dest, cur_loc) { var cur_rot_q = _quat4_tmp_2; var cur_hor_dir = _vec3_tmp_3; var speed = ev_track.speed; var rot_speed = ev_track.rot_speed; m_trans.get_rotation(ev_track.collider, cur_rot_q); m_vec3.transformQuat(m_util.AXIS_MY, cur_rot_q, cur_hor_dir); cur_hor_dir[2] = 0; m_vec3.normalize(cur_hor_dir, cur_hor_dir); var sin_half_rot_angle = Math.sin(rot_speed/2); var walk_radius = 0.5 * speed / sin_half_rot_angle; var dist = m_vec3.dist(dest, cur_loc); if (dist < 2.0 * walk_radius) m_vec3.scaleAndAdd(dest, cur_hor_dir, (2.0 * walk_radius - dist), dest); } function anim_translation(elapsed, ev_track) { var cur_loc = _vec3_tmp; var cur_dir = _vec3_tmp_2; var new_hor_dir = _vec3_tmp_3; var new_rot_q = _quat4_tmp_3; var rig = ev_track.rig; var collider = ev_track.collider; var dest = ev_track.destination; var speed = ev_track.speed; var actions = ev_track.actions; var cur_anim = m_anim.get_current_anim_name(rig); // skip iteration if idle animation is playing if (m_anim.is_play(rig) && actions.idle && actions.idle.indexOf(cur_anim) != -1) return; m_trans.get_translation(collider, cur_loc); var delta_x = dest[0] - cur_loc[0]; var delta_z = dest[1] - cur_loc[1]; var left_to_pass = Math.sqrt(delta_x * delta_x + delta_z * delta_z); if (left_to_pass > 2.0 * speed * elapsed) { ev_track.state = MS_MOVING; m_util.horizontal_direction(dest, cur_loc, new_hor_dir); dest_anim_correction(ev_track, dest, left_to_pass, new_hor_dir); // rotation hor_rot (ev_track, cur_dir, elapsed, new_rot_q, new_hor_dir); vert_rot(ev_track, cur_dir, elapsed, new_rot_q); m_trans.set_rotation_v(collider, new_rot_q); // translation trans_obj(elapsed, new_rot_q, ev_track, cur_loc, dest); } else if (ev_track.type == NT_WALKING) { if (!actions.move) { ev_track.state = MS_IDLE; ev_track.reached = true; } else if (cur_anim && actions.move.indexOf(cur_anim) != -1 && m_anim.is_play(rig)) ev_track.state = MS_MOVING; else { ev_track.state = MS_IDLE; ev_track.reached = true; } } else if (ev_track.type == NT_SWIMMING || ev_track.type == NT_FLYING) ev_track.reached = true; if (need_proper_animation(ev_track)) apply_animation(ev_track); } function hor_rot(ev_track, cur_dir, elapsed, new_rot_q, new_hor_dir) { var cur_rot_q = _quat4_tmp_2; var cur_hor_dir = _vec3_tmp_4; m_trans.get_rotation(ev_track.collider, cur_rot_q); m_vec3.transformQuat(m_util.AXIS_MY, cur_rot_q, cur_dir); cur_hor_dir[0] = cur_dir[0]; cur_hor_dir[1] = cur_dir[1]; cur_hor_dir[2] = 0; m_vec3.normalize(cur_hor_dir, cur_hor_dir); var vec_dot = m_vec3.dot(cur_hor_dir, new_hor_dir); var angle_to_turn = Math.acos(vec_dot); var angle_ratio = Math.abs(angle_to_turn) / Math.PI; var slerp = elapsed / angle_ratio * ev_track.rot_speed * ev_track.rotation_mult; m_quat.rotationTo(cur_hor_dir, new_hor_dir, new_rot_q); m_quat.rotationTo(m_util.AXIS_MY, cur_hor_dir, cur_rot_q); if (Math.abs(vec_dot) >= 1) { m_quat.copy(cur_rot_q, new_rot_q); return; } m_quat.multiply(new_rot_q, cur_rot_q, new_rot_q); m_quat.slerp(cur_rot_q, new_rot_q, Math.min(slerp, 1), new_rot_q); } function vert_rot(ev_track, cur_dir, elapsed, new_rot_q) { if (ev_track.type == NT_FLYING) return; // synchronization with physics engine elapsed = Math.max(elapsed, 1/60); var new_vert_q = _quat4_tmp; var cur_vert_q = _quat4_tmp_2; var cur_vert_angle = Math.asin(-cur_dir[2]); m_quat.setAxisAngle(m_util.AXIS_X, -cur_vert_angle, cur_vert_q); var delta_hor_dist = ev_track.speed * elapsed; var delta_vert_dist = ev_track.vert_correction; var new_vert_angle = Math.atan(delta_vert_dist / delta_hor_dist); m_quat.setAxisAngle(m_util.AXIS_X, -new_vert_angle, new_vert_q); m_quat.slerp(cur_vert_q, new_vert_q, elapsed, new_vert_q); m_quat.multiply(new_rot_q, new_vert_q, new_rot_q); } function trans_obj(elapsed, new_rot_q, ev_track, cur_loc, dest) { var new_dir = _vec3_tmp_3; var new_loc = _vec3_tmp_4; var def_dir = m_util.AXIS_MY; m_util.quat_to_dir(new_rot_q, def_dir, new_dir); m_vec3.scale(new_dir, ev_track.speed * elapsed, new_loc); m_vec3.add(new_loc, cur_loc, new_loc); if (ev_track.type != NT_WALKING) new_loc[2] = (dest[2] - cur_loc[2]) * elapsed * 0.1 + cur_loc[2]; else new_loc[2] = dest[2]; m_trans.set_translation_v(ev_track.collider, new_loc); } /** * Attaches sensors to characters and runs elapsed_cb every frame. */ exports.enable_animation = function () { if(!_ev_tracks.length) return; create_sensors(); var elapsed_cb = function(obj, id, pulse) { if (pulse == 1) { var elapsed = m_ctl.get_sensor_value(obj, id, 0); for (var i = 0; i < _ev_tracks.length; i++) process_event_track(_ev_tracks[i], elapsed); } } var elapsed_sensor = m_ctl.create_elapsed_sensor(); m_ctl.create_sensor_manifold(_ev_tracks[0].collider, "ELAPSED", m_ctl.CT_CONTINUOUS, [elapsed_sensor], function(s){return s[0]}, elapsed_cb); } /** * Removes all sensors attached to animated objects and stops animation */ exports.disable_animation = function() { if(_ev_tracks.length <= 0) return; for (var i = 0; i < _ev_tracks.length; i++) { var ev = _ev_tracks[i]; if (m_ctl.check_sensor_manifolds(ev.collider)) m_ctl.remove_sensor_manifold(ev.collider); if (m_anim.is_play(ev.rig)) m_anim.stop(ev.rig); } } function elapsed_cb(obj, id, pulse) { if (pulse == 1) { for (var i = 0; i < _ev_tracks.length; i++) { var elapsed = m_ctl.get_sensor_value(obj, id, 0); process_event_track(_ev_tracks[i], elapsed); } } } function process_event_track(ev_track, elapsed) { if (!m_scs.is_visible(ev_track.obj)) return; if (ev_track.random) { run_track(elapsed, ev_track); } else { var focus_time = m_time.get_timeline(); for (var j = 0; j < ev_track.path.length; j++) { if (ev_track.ended[j]) continue; if (!ev_track.fired[j]) { ev_track.fired[j] = true; ev_track.destination[0] = ev_track.path[j][0]; ev_track.destination[1] = ev_track.path[j][1]; ev_track.destination[2] = ev_track.path[j][2]; ev_track.start[j] = focus_time + ev_track.delay[j]; } if (focus_time < ev_track.start[j]) break; run_track(elapsed, ev_track); ev_track.ended[j] = ev_track.reached; break; } if (is_all_ended(ev_track)) unset_all_fired(ev_track); } } function unset_all_fired(track) { for (var i = 0; i < track.path.length; i++) { track.fired[i] = false; track.ended[i] = false; } } function is_all_ended(track) { for (var i = 0; i < track.path.length; i++) if (track.ended[i] == false) return false; return true; } function ground_cb(obj, id, pulse, ev) { if (!ev) return; if (pulse == 1) { switch(ev.type) { case NT_FLYING: var hit_fract = 100 * flying_npc_hit_fract(obj, id); if (hit_fract < ev.min_height) { ev.vert_correction = 10; } else if (hit_fract > ev.max_height) { ev.vert_correction = -10; } else ev.vert_correction = 0; break; case NT_WALKING: var payload = m_ctl.get_sensor_payload(obj, id, 0); ev.ground_level = payload.hit_pos[2]; break; case NT_SWIMMING: var payload = m_ctl.get_sensor_payload(obj, id, 0); if (id == "CLOSE_GROUND") { ev.vert_correction = payload.hit_fract * 100 - 1; if (ev.vert_correction < 0.1) ev.vert_correction = 0.05; else ev.vert_correction = 0; } else if (id == "CLOSE_WATER") { ev.vert_cor_water = hit_fract * 100; if (ev.vert_cor_water < ev.min_depth) ev.vert_cor_water = -0.02; else if (ev.vert_cor_water > ev.max_depth) ev.vert_cor_water = 0.02; else ev.vert_cor_water = 0; } break; } } else ev.vert_correction = 0; } function flying_npc_hit_fract(obj, id) { for (var i = 0; i < 3; i++) { var hit_pos = m_ctl.get_sensor_payload(obj, id, i).hit_fract; if (hit_pos) return hit_pos; } } function create_sensors() { for (var i = 0; i < _ev_tracks.length; i++) { var ev_track = _ev_tracks[i]; create_track_ray_sensors(ev_track); create_track_collision_sensors(ev_track); if (m_anim.get_current_anim_name(ev_track.rig)) m_anim.play(ev_track.rig); } } function create_track_ray_sensors(ev_track) { var ZERO_POINT = m_vec3.create(); var collider = ev_track.collider; switch (ev_track.type) { case NT_FLYING: var near_ground_sens = m_ctl.create_ray_sensor(collider, ZERO_POINT, [0, 0, -100], "TERRAIN", true, true); var near_stone_sens = m_ctl.create_ray_sensor(collider, ZERO_POINT, [0, 0, -100], "STONE", true, true); var near_water_sens = m_ctl.create_ray_sensor(collider, ZERO_POINT, [0, 0, -100], "WATER", true, true); var ground_sens_arr = [near_ground_sens, near_stone_sens, near_water_sens]; m_ctl.create_sensor_manifold(collider, "CLOSE_GROUND", m_ctl.CT_CONTINUOUS, ground_sens_arr, function(s){return s[0] || s[1] || s[2]}, ground_cb, ev_track); break; case NT_WALKING: var near_ground_sens = m_ctl.create_ray_sensor(collider, [0, 0, 1], [0, 0, -99], "TERRAIN", true, true); var ground_sens_arr = [near_ground_sens]; m_ctl.create_sensor_manifold(collider, "CLOSE_GROUND", m_ctl.CT_CONTINUOUS, ground_sens_arr, null, ground_cb, ev_track); break; case NT_SWIMMING: var near_ground_sens = m_ctl.create_ray_sensor(collider, [0, 0, 1], [0, 0, -99], "TERRAIN", true, true); var near_water_sens = m_ctl.create_ray_sensor(collider, ZERO_POINT, [0, 0, 100], "WATER", true, true); var ground_sens_arr = [near_ground_sens]; var water_sens_arr = [near_water_sens]; m_ctl.create_sensor_manifold(collider, "CLOSE_WATER", m_ctl.CT_CONTINUOUS, water_sens_arr, null, ground_cb, ev_track); m_ctl.create_sensor_manifold(collider, "CLOSE_GROUND", m_ctl.CT_CONTINUOUS, ground_sens_arr, null, ground_cb, ev_track); break; } } function create_track_collision_sensors(ev_track) { var collider = ev_track.collider; if (ev_track.type != NT_WALKING) return; var need_payload = false; var collision_sensor = m_ctl.create_collision_sensor(collider, "CONSTRUCTION", need_payload); function collision_cb(obj, id, pulse) { if (pulse == 1) { m_vec3.copy(ev_track.base_pos, ev_track.destination); ev_track.rotation_mult = 4.0; } else { ev_track.rotation_mult = 1.0; } } m_ctl.create_sensor_manifold(collider, "CONSTRUCTION_COLL", m_ctl.CT_CONTINUOUS, [collision_sensor], null, collision_cb); } function need_proper_animation(ev_track) { var obj = ev_track.rig; if (!m_anim.is_play(obj)) { return true; } var cur_anim = m_anim.get_current_anim_name(obj); if (!cur_anim) return true; var actions = ev_track.actions; switch (ev_track.state) { case MS_IDLE: if (!actions.idle) return false; if (actions.idle.indexOf(cur_anim) == -1) return true; break; case MS_MOVING: if (!actions.move) return false; if (actions.move.indexOf(cur_anim) != -1) return false; if (actions.move_start && actions.move_start.indexOf(cur_anim) != -1) return false; if (actions.move_blends && actions.move_blends.indexOf(cur_anim) != -1) return false; break; default: return false; } return true; } function apply_animation(ev_track) { var obj = ev_track.rig; if (m_anim.is_play(obj)) return; var anim_to_play = null; switch (ev_track.state) { case MS_IDLE: anim_to_play = get_idle_animation(ev_track); break; case MS_MOVING: anim_to_play = get_move_animation(ev_track); break; } if (anim_to_play) { m_anim.apply(obj, anim_to_play); m_anim.set_behavior(obj, m_anim.AB_FINISH_RESET); m_anim.set_frame(obj, 0); m_anim.play(obj); } } function get_idle_animation(ev_track) { var actions = ev_track.actions; if (!actions.idle) return null; return m_util.random_from_array(actions.idle); } function get_move_animation(ev_track) { var cur_anim = m_anim.get_current_anim_name(ev_track.rig); var actions = ev_track.actions; if (need_move_blend_animation(ev_track, cur_anim)) { return get_proper_move_blend_animation(ev_track, cur_anim); } if (need_move_animation(ev_track, cur_anim)) { return get_proper_move_animation(ev_track, cur_anim); } if (need_move_start_animation(ev_track, cur_anim)) { return m_util.random_from_array(actions.move_start); } return null; } function need_move_animation(ev_track, cur_anim) { var actions = ev_track.actions; if (!actions.move) return false; if (actions.move.indexOf(cur_anim) != -1) return true; if (!(actions.move_blends || actions.move_start)) return true; if (!actions.move_start && !cur_anim) return true; if (actions.move_blends && actions.move_blends.indexOf(cur_anim) != -1) return true; if (actions.move_start && actions.move_start.indexOf(cur_anim) != -1) return true; return false; } function need_move_blend_animation(ev_track, cur_anim) { var actions = ev_track.actions; if (!actions.move_blends) return false; if (actions.move_blends.indexOf(cur_anim) != -1) return false; if (actions.move && actions.move.indexOf(cur_anim) != -1) { var identifier = Math.random(); return identifier > 0.33; } return false; } function need_move_start_animation(ev_track, cur_anim) { var actions = ev_track.actions; if (!actions.move_start) return false; if (actions.move_start.indexOf(cur_anim) != -1) return false; if (actions.move && actions.move.indexOf(cur_anim) == -1) return true; return false; } function get_proper_move_blend_animation(ev_track, cur_anim) { var actions = ev_track.actions; var ind = actions.move.indexOf(cur_anim); return actions.move_blends[ind]; } function get_proper_move_animation(ev_track, cur_anim) { var actions = ev_track.actions; if (!actions.move_blends) return m_util.random_from_array(actions.move); var move_blend_anim_ind = actions.move_blends.indexOf(cur_anim); if (move_blend_anim_ind != -1) { var ind = move_blend_anim_ind + 1; ind = ind < actions.move.length? ind: 0; return actions.move[ind]; } if (actions.move.indexOf(cur_anim) != -1) return cur_anim; else return m_util.random_from_array(actions.move); } function dest_anim_correction(ev_track, dest, l_to_p, new_dir) { var obj = ev_track.rig; if (!m_anim.is_animated(obj)) return var speed = ev_track.speed; var left_to_pass = l_to_p; var cur_frame = m_anim.get_frame(obj); var anim_length = m_anim.get_anim_length(obj); var path_per_cycle = anim_length / 24 * speed; var left_to_pass_anim = (1 - cur_frame / anim_length) * path_per_cycle; var correlation = left_to_pass / left_to_pass_anim; if (correlation < 0.9) { var scale = left_to_pass_anim - left_to_pass; m_vec3.scaleAndAdd(dest, new_dir, scale, dest); } } }