You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

824 lines
18 KiB

  1. define([
  2. 'js/resources',
  3. 'js/system/events',
  4. 'js/misc/physics',
  5. 'js/rendering/effects',
  6. 'js/rendering/tileOpacity',
  7. 'js/rendering/particles',
  8. 'js/rendering/shaders/outline',
  9. 'js/rendering/spritePool',
  10. 'js/rendering/helpers/updateSprites',
  11. 'js/system/globals',
  12. 'js/rendering/helpers/renderLoginBackground',
  13. 'js/rendering/helpers/resetRenderer'
  14. ], function (
  15. resources,
  16. events,
  17. physics,
  18. effects,
  19. tileOpacity,
  20. particles,
  21. shaderOutline,
  22. spritePool,
  23. updateSprites,
  24. globals,
  25. renderLoginBackground,
  26. resetRenderer
  27. ) {
  28. const mRandom = Math.random.bind(Math);
  29. const particleLayers = ['particlesUnder', 'particles'];
  30. const particleEngines = {};
  31. return {
  32. stage: null,
  33. layers: {
  34. particlesUnder: null,
  35. objects: null,
  36. mobs: null,
  37. characters: null,
  38. attacks: null,
  39. effects: null,
  40. particles: null,
  41. lightPatches: null,
  42. lightBeams: null,
  43. tileSprites: null,
  44. hiders: null
  45. },
  46. titleScreen: false,
  47. width: 0,
  48. height: 0,
  49. showTilesW: 0,
  50. showTilesH: 0,
  51. pos: {
  52. x: 0,
  53. y: 0
  54. },
  55. moveTo: null,
  56. moveSpeed: 0,
  57. moveSpeedMax: 1.50,
  58. moveSpeedInc: 0.5,
  59. lastUpdatePos: {
  60. x: 0,
  61. y: 0
  62. },
  63. zoneId: null,
  64. textures: {},
  65. textureCache: {},
  66. sprites: [],
  67. lastTick: null,
  68. hiddenRooms: null,
  69. staticCamera: false,
  70. init: function () {
  71. PIXI.settings.GC_MODE = PIXI.GC_MODES.AUTO;
  72. PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
  73. PIXI.settings.SPRITE_MAX_TEXTURES = Math.min(PIXI.settings.SPRITE_MAX_TEXTURES, 16);
  74. PIXI.settings.RESOLUTION = 1;
  75. events.on('onGetMap', this.onGetMap.bind(this));
  76. events.on('onToggleFullscreen', this.toggleScreen.bind(this));
  77. events.on('onMoveSpeedChange', this.adaptCameraMoveSpeed.bind(this));
  78. events.on('resetRenderer', resetRenderer.bind(this));
  79. this.width = $('body').width();
  80. this.height = $('body').height();
  81. this.showTilesW = Math.ceil((this.width / scale) / 2) + 3;
  82. this.showTilesH = Math.ceil((this.height / scale) / 2) + 3;
  83. this.renderer = new PIXI.Renderer({
  84. width: this.width,
  85. height: this.height,
  86. backgroundColor: '0x2d2136'
  87. });
  88. window.addEventListener('resize', this.onResize.bind(this));
  89. $(this.renderer.view).appendTo('.canvas-container');
  90. this.stage = new PIXI.Container();
  91. let layers = this.layers;
  92. Object.keys(layers).forEach(l => {
  93. layers[l] = new PIXI.Container();
  94. layers[l].layer = (l === 'tileSprites') ? 'tiles' : l;
  95. this.stage.addChild(layers[l]);
  96. });
  97. const textureList = globals.clientConfig.textureList;
  98. const sprites = resources.sprites;
  99. textureList.forEach(t => {
  100. this.textures[t] = new PIXI.BaseTexture(sprites[t]);
  101. this.textures[t].scaleMode = PIXI.SCALE_MODES.NEAREST;
  102. });
  103. particleLayers.forEach(p => {
  104. const engine = $.extend({}, particles);
  105. engine.init({
  106. r: this,
  107. renderer: this.renderer,
  108. stage: this.layers[p]
  109. });
  110. particleEngines[p] = engine;
  111. });
  112. this.buildSpritesTexture();
  113. },
  114. buildSpritesTexture: function () {
  115. const { clientConfig: { atlasTextureDimensions, atlasTextures } } = globals;
  116. let container = new PIXI.Container();
  117. let totalHeight = 0;
  118. atlasTextures.forEach(t => {
  119. let texture = this.textures[t];
  120. let tile = new PIXI.Sprite(new PIXI.Texture(texture));
  121. tile.width = texture.width;
  122. tile.height = texture.height;
  123. tile.x = 0;
  124. tile.y = totalHeight;
  125. atlasTextureDimensions[t] = {
  126. w: texture.width / 8,
  127. h: texture.height / 8
  128. };
  129. container.addChild(tile);
  130. totalHeight += tile.height;
  131. });
  132. let renderTexture = PIXI.RenderTexture.create(this.textures.tiles.width, totalHeight);
  133. this.renderer.render(container, renderTexture);
  134. this.textures.sprites = renderTexture;
  135. this.textures.scaleMult = PIXI.SCALE_MODES.NEAREST;
  136. },
  137. toggleScreen: function () {
  138. let isFullscreen = (window.innerHeight === screen.height);
  139. if (isFullscreen) {
  140. let doc = document;
  141. (doc.cancelFullscreen || doc.msCancelFullscreen || doc.mozCancelFullscreen || doc.webkitCancelFullScreen).call(doc);
  142. return 'Windowed';
  143. }
  144. let el = $('body')[0];
  145. (el.requestFullscreen || el.msRequestFullscreen || el.mozRequestFullscreen || el.webkitRequestFullscreen).call(el);
  146. return 'Fullscreen';
  147. },
  148. buildTitleScreen: function () {
  149. this.titleScreen = true;
  150. renderLoginBackground(this);
  151. },
  152. onResize: function () {
  153. if (isMobile)
  154. return;
  155. this.width = $('body').width();
  156. this.height = $('body').height();
  157. this.showTilesW = Math.ceil((this.width / scale) / 2) + 3;
  158. this.showTilesH = Math.ceil((this.height / scale) / 2) + 3;
  159. this.renderer.resize(this.width, this.height);
  160. if (window.player) {
  161. this.setPosition({
  162. pos: {
  163. x: window.player.x,
  164. y: window.player.y
  165. },
  166. instant: true
  167. });
  168. }
  169. if (this.titleScreen) {
  170. this.clean();
  171. this.buildTitleScreen();
  172. }
  173. events.emit('onResize');
  174. },
  175. getTexture: function (baseTex, cell, size) {
  176. size = size || 8;
  177. let textureName = baseTex + '_' + cell;
  178. let textureCache = this.textureCache;
  179. let cached = textureCache[textureName];
  180. if (!cached) {
  181. let y = ~~(cell / 8);
  182. let x = cell - (y * 8);
  183. cached = new PIXI.Texture(this.textures[baseTex], new PIXI.Rectangle(x * size, y * size, size, size));
  184. textureCache[textureName] = cached;
  185. }
  186. return cached;
  187. },
  188. clean: function () {
  189. this.stage.removeChild(this.layers.hiders);
  190. this.layers.hiders = new PIXI.Container();
  191. this.layers.hiders.layer = 'hiders';
  192. this.stage.addChild(this.layers.hiders);
  193. let container = this.layers.tileSprites;
  194. this.stage.removeChild(container);
  195. this.layers.tileSprites = container = new PIXI.Container();
  196. container.layer = 'tiles';
  197. this.stage.addChild(container);
  198. this.stage.children.sort((a, b) => {
  199. if (a.layer === 'hiders')
  200. return 1;
  201. else if (b.layer === 'hiders')
  202. return -1;
  203. else if (a.layer === 'tiles')
  204. return -1;
  205. else if (b.layer === 'tiles')
  206. return 1;
  207. return 0;
  208. });
  209. },
  210. buildTile: function (c, i, j) {
  211. let alpha = tileOpacity.map(c);
  212. let canFlip = tileOpacity.canFlip(c);
  213. let tile = new PIXI.Sprite(this.getTexture('sprites', c));
  214. tile.alpha = alpha;
  215. tile.position.x = i * scale;
  216. tile.position.y = j * scale;
  217. tile.width = scale;
  218. tile.height = scale;
  219. if (canFlip && mRandom() < 0.5) {
  220. tile.position.x += scale;
  221. tile.scale.x = -scaleMult;
  222. }
  223. return tile;
  224. },
  225. onGetMap: function (msg) {
  226. const { zoneId, collisionMap, map, hiddenRooms, clientObjects, rendererConfig } = msg;
  227. const { staticCamera = false, cameraPosition } = rendererConfig;
  228. this.staticCamera = staticCamera;
  229. this.map = map;
  230. this.titleScreen = false;
  231. physics.init(collisionMap);
  232. let w = this.w = map.length;
  233. let h = this.h = map[0].length;
  234. for (let i = 0; i < w; i++) {
  235. let row = map[i];
  236. for (let j = 0; j < h; j++) {
  237. if (!row[j].split)
  238. row[j] += '';
  239. row[j] = row[j].split(',');
  240. }
  241. }
  242. this.clean();
  243. spritePool.clean();
  244. this.stage.filters = [new PIXI.filters.AlphaFilter()];
  245. this.stage.filterArea = new PIXI.Rectangle(0, 0, Math.max(w * scale, this.width), Math.max(h * scale, this.height));
  246. this.hiddenRooms = hiddenRooms;
  247. this.sprites = _.get2dArray(w, h, 'array');
  248. this.stage.children.sort((a, b) => {
  249. if (a.layer === 'tiles')
  250. return -1;
  251. else if (b.layer === 'tiles')
  252. return 1;
  253. return 0;
  254. });
  255. if (this.zoneId !== null) {
  256. events.emit('onRezone', {
  257. oldZoneId: this.zoneId,
  258. newZoneId: zoneId
  259. });
  260. }
  261. this.zoneId = zoneId;
  262. clientObjects.forEach(c => {
  263. c.zoneId = this.zoneId;
  264. events.emit('onGetObject', c);
  265. });
  266. if (cameraPosition) {
  267. this.setPosition({
  268. x: (cameraPosition.x - (this.width / (scale * 2))) * scale,
  269. y: (cameraPosition.y - (this.height / (scale * 2))) * scale
  270. }, true, true);
  271. }
  272. //Normally, the mounts mod queues this event when unmounting.
  273. // If we rezone, our effects are destroyed, so the event is queued,
  274. // but flushForTarget clears the event right after and the event is never received.
  275. // We emit it again here to make sure the speed is reset after entering the new zone.
  276. events.emit('onMoveSpeedChange', 0);
  277. },
  278. /*
  279. pos: { x, y }
  280. The x and y positions the camera should be centered on (not yet multiplied by scale)
  281. instant: boolean
  282. should the camera pan to the location or not
  283. */
  284. setPosition: function ({ pos = { x: 0, y: 0 }, instant }) {
  285. let { x, y } = pos;
  286. x = (x - (this.width / (scale * 2))) * scale;
  287. y = (y - (this.height / (scale * 2))) * scale;
  288. let player = window.player;
  289. if (player) {
  290. let px = player.x;
  291. let py = player.y;
  292. let hiddenRooms = this.hiddenRooms || [];
  293. let hLen = hiddenRooms.length;
  294. for (let i = 0; i < hLen; i++) {
  295. let h = hiddenRooms[i];
  296. if (!h.discoverable)
  297. continue;
  298. if (
  299. px < h.x ||
  300. px >= h.x + h.width ||
  301. py < h.y ||
  302. py >= h.y + h.height ||
  303. !physics.isInPolygon(px, py, h.area)
  304. )
  305. continue;
  306. h.discovered = true;
  307. }
  308. }
  309. const staticCamera = window.staticCamera || this.staticCamera;
  310. if (staticCamera && !instant) {
  311. this.updateSprites();
  312. return;
  313. }
  314. if (instant) {
  315. this.moveTo = null;
  316. this.pos = {
  317. x,
  318. y
  319. };
  320. this.stage.x = -~~x;
  321. this.stage.y = -~~y;
  322. } else {
  323. this.moveTo = {
  324. x,
  325. y
  326. };
  327. }
  328. this.updateSprites();
  329. },
  330. isVisible: function (x, y) {
  331. let stage = this.stage;
  332. let sx = -stage.x;
  333. let sy = -stage.y;
  334. let sw = this.width;
  335. let sh = this.height;
  336. return (!(x < sx || y < sy || x >= sx + sw || y >= sy + sh));
  337. },
  338. isHidden: function (x, y) {
  339. let hiddenRooms = this.hiddenRooms;
  340. let hLen = hiddenRooms.length;
  341. if (!hLen)
  342. return false;
  343. const { player: { x: px, y: py } } = window;
  344. let foundVisibleLayer = null;
  345. let foundHiddenLayer = null;
  346. const fnTileInArea = physics.isInArea.bind(physics, x, y);
  347. const fnPlayerInArea = physics.isInArea.bind(physics, px, py);
  348. hiddenRooms.forEach(h => {
  349. const { discovered, layer, interior } = h;
  350. const playerInHider = fnPlayerInArea(h);
  351. const tileInHider = fnTileInArea(h);
  352. if (playerInHider) {
  353. if (interior && !tileInHider) {
  354. foundHiddenLayer = layer;
  355. return;
  356. }
  357. } else if (tileInHider && !discovered) {
  358. foundHiddenLayer = layer;
  359. return;
  360. } else if (tileInHider && discovered) {
  361. foundVisibleLayer = layer;
  362. return;
  363. }
  364. if (!tileInHider)
  365. return;
  366. foundVisibleLayer = layer;
  367. });
  368. //We compare hider layers to cater for hiders inside hiders
  369. return (
  370. foundHiddenLayer > foundVisibleLayer ||
  371. (
  372. foundHiddenLayer === 0 &&
  373. foundVisibleLayer === null
  374. )
  375. );
  376. },
  377. updateSprites: function () {
  378. updateSprites(this);
  379. },
  380. update: function () {
  381. let time = +new Date();
  382. if (this.moveTo) {
  383. let deltaX = this.moveTo.x - this.pos.x;
  384. let deltaY = this.moveTo.y - this.pos.y;
  385. if (deltaX !== 0 || deltaY !== 0) {
  386. let distance = Math.max(Math.abs(deltaX), Math.abs(deltaY));
  387. let moveSpeedMax = this.moveSpeedMax;
  388. if (this.moveSpeed < moveSpeedMax)
  389. this.moveSpeed += this.moveSpeedInc;
  390. let moveSpeed = this.moveSpeed;
  391. if (moveSpeedMax < 1.6)
  392. moveSpeed *= 1 + (distance / 200);
  393. let elapsed = time - this.lastTick;
  394. moveSpeed *= (elapsed / 15);
  395. if (moveSpeed > distance)
  396. moveSpeed = distance;
  397. deltaX = (deltaX / distance) * moveSpeed;
  398. deltaY = (deltaY / distance) * moveSpeed;
  399. this.pos.x = this.pos.x + deltaX;
  400. this.pos.y = this.pos.y + deltaY;
  401. } else {
  402. this.moveSpeed = 0;
  403. this.moveTo = null;
  404. }
  405. const staticCamera = this.staticCamera || window.staticCamera;
  406. let stage = this.stage;
  407. if (staticCamera !== true) {
  408. stage.x = -~~this.pos.x;
  409. stage.y = -~~this.pos.y;
  410. }
  411. let halfScale = scale / 2;
  412. if (Math.abs(stage.x - this.lastUpdatePos.x) > halfScale || Math.abs(stage.y - this.lastUpdatePos.y) > halfScale)
  413. this.updateSprites();
  414. events.emit('onSceneMove');
  415. }
  416. this.lastTick = time;
  417. },
  418. buildContainer: function (obj) {
  419. let container = new PIXI.Container();
  420. this.layers[obj.layerName || obj.sheetName].addChild(container);
  421. return container;
  422. },
  423. buildRectangle: function (obj) {
  424. let graphics = new PIXI.Graphics();
  425. let alpha = obj.alpha;
  426. if (obj.has('alpha'))
  427. graphics.alpha = alpha;
  428. let fillAlpha = obj.fillAlpha;
  429. if (obj.has('fillAlpha'))
  430. fillAlpha = 1;
  431. graphics.beginFill(obj.color || '0x48edff', fillAlpha);
  432. if (obj.strokeColor)
  433. graphics.lineStyle(scaleMult, obj.strokeColor);
  434. graphics.drawRect(0, 0, obj.w, obj.h);
  435. graphics.endFill();
  436. (obj.parent || this.layers[obj.layerName || obj.sheetName]).addChild(graphics);
  437. graphics.position.x = obj.x;
  438. graphics.position.y = obj.y;
  439. return graphics;
  440. },
  441. moveRectangle: function (obj) {
  442. obj.sprite.position.x = obj.x;
  443. obj.sprite.position.y = obj.y;
  444. obj.sprite.width = obj.w;
  445. obj.sprite.height = obj.h;
  446. },
  447. buildObject: function (obj) {
  448. const { sheetName, parent: container, layerName, visible = true } = obj;
  449. const sprite = new PIXI.Sprite();
  450. obj.sprite = sprite;
  451. this.setSprite(obj);
  452. sprite.visible = visible;
  453. const spriteContainer = container || this.layers[layerName || sheetName] || this.layers.objects;
  454. spriteContainer.addChild(sprite);
  455. obj.w = sprite.width;
  456. obj.h = sprite.height;
  457. return sprite;
  458. },
  459. addFilter: function (sprite, config) {
  460. const filter = new shaderOutline(config);
  461. if (!sprite.filters)
  462. sprite.filters = [filter];
  463. else
  464. sprite.filters.push(filter);
  465. return filter;
  466. },
  467. removeFilter: function (sprite, filter) {
  468. if (sprite.filters)
  469. sprite.filters = null;
  470. },
  471. buildText: function (obj) {
  472. const { text, visible, x, y, parent: spriteParent, layerName } = obj;
  473. const { fontSize = 14, color = 0xF2F5F5 } = obj;
  474. const textSprite = new PIXI.Text(text, {
  475. fontFamily: 'bitty',
  476. fontSize: fontSize,
  477. fill: color,
  478. stroke: 0x2d2136,
  479. strokeThickness: 4
  480. });
  481. if (visible === false)
  482. textSprite.visible = false;
  483. textSprite.x = x - (textSprite.width / 2);
  484. textSprite.y = y;
  485. const parentSprite = spriteParent ?? this.layers[layerName];
  486. parentSprite.addChild(textSprite);
  487. return textSprite;
  488. },
  489. buildEmitter: function (config) {
  490. const { layerName = 'particles' } = config;
  491. const particleEngine = particleEngines[layerName];
  492. return particleEngine.buildEmitter(config);
  493. },
  494. destroyEmitter: function (emitter) {
  495. const particleEngine = emitter.particleEngine;
  496. particleEngine.destroyEmitter(emitter);
  497. },
  498. setSprite: function (obj) {
  499. const { sprite, sheetName, cell } = obj;
  500. const bigSheets = globals.clientConfig.bigTextures;
  501. const isBigSheet = bigSheets.includes(sheetName);
  502. const newSize = isBigSheet ? 24 : 8;
  503. obj.w = newSize * scaleMult;
  504. obj.h = obj.w;
  505. sprite.width = obj.w;
  506. sprite.height = obj.h;
  507. sprite.texture = this.getTexture(sheetName, cell, newSize);
  508. if (newSize !== sprite.size) {
  509. sprite.size = newSize;
  510. this.setSpritePosition(obj);
  511. }
  512. },
  513. setSpritePosition: function (obj) {
  514. const { sprite, x, y, flipX, offsetX = 0, offsetY = 0 } = obj;
  515. sprite.x = (x * scale) + (flipX ? scale : 0) + offsetX;
  516. const oldY = sprite.y;
  517. sprite.y = (y * scale) + offsetY;
  518. if (sprite.width > scale) {
  519. if (flipX)
  520. sprite.x += scale;
  521. else
  522. sprite.x -= scale;
  523. sprite.y -= (scale * 2);
  524. }
  525. if (oldY !== sprite.y)
  526. this.reorder();
  527. sprite.scale.x = flipX ? -scaleMult : scaleMult;
  528. },
  529. reorder: function () {
  530. this.layers.mobs.children.sort((a, b) => b.y - a.y);
  531. },
  532. destroyObject: function (obj) {
  533. if (obj.sprite.parent)
  534. obj.sprite.parent.removeChild(obj.sprite);
  535. },
  536. //Changes the moveSpeedMax and moveSpeedInc variables
  537. // moveSpeed changes when mounting and unmounting
  538. // moveSpeed: 0 | moveSpeedMax: 1.5 | moveSpeedInc: 0.5
  539. // moveSpeed: 200 | moveSpeedMax: 5.5 | moveSpeedInc: 0.2
  540. // Between these values we should follow an exponential curve for moveSpeedInc since
  541. // a higher chance will proc more often, meaning the buildup in distance becomes greater
  542. adaptCameraMoveSpeed: function (moveSpeed) {
  543. const factor = Math.sqrt(moveSpeed);
  544. const maxValue = Math.sqrt(200);
  545. this.moveSpeedMax = 1.5 + ((moveSpeed / 200) * 3.5);
  546. this.moveSpeedInc = 0.2 + (((maxValue - factor) / maxValue) * 0.3);
  547. },
  548. updateMapAtPosition: function (x, y, mapCellString) {
  549. const { map, sprites, layers: { tileSprites: container } } = this;
  550. const row = sprites[x];
  551. if (!row)
  552. return;
  553. const cell = row[y];
  554. if (!cell)
  555. return;
  556. cell.forEach(c => {
  557. c.visible = false;
  558. spritePool.store(c);
  559. });
  560. cell.length = 0;
  561. map[x][y] = mapCellString.split(',');
  562. map[x][y].forEach(m => {
  563. m--;
  564. let tile = spritePool.getSprite(m);
  565. if (!tile) {
  566. tile = this.buildTile(m, x, y);
  567. container.addChild(tile);
  568. tile.type = m;
  569. tile.sheetNum = tileOpacity.getSheetNum(m);
  570. } else {
  571. tile.position.x = x * scale;
  572. tile.position.y = y * scale;
  573. tile.visible = true;
  574. }
  575. cell.push(tile);
  576. cell.visible = true;
  577. });
  578. },
  579. updateMapRows: function (rows) {
  580. const { map, sprites, layers: { tileSprites: container } } = this;
  581. rows.forEach(({ rowNumber: x, cols }) => {
  582. const row = sprites[x];
  583. cols.forEach(({ colNumber: y, cells }) => {
  584. const cellSprites = row[y];
  585. cellSprites.forEach(c => {
  586. c.visible = false;
  587. spritePool.store(c);
  588. });
  589. cellSprites.length = 0;
  590. map[x][y] = cells;
  591. cells.forEach((m, k) => {
  592. m--;
  593. let flipped = '';
  594. if (tileOpacity.canFlip(m)) {
  595. if (mRandom() < 0.5)
  596. flipped = 'flip';
  597. }
  598. let tile = spritePool.getSprite(flipped + m);
  599. if (!tile) {
  600. tile = this.buildTile(m, x, y);
  601. container.addChild(tile);
  602. tile.type = m;
  603. tile.sheetNum = tileOpacity.getSheetNum(m);
  604. } else {
  605. tile.position.x = x * scale;
  606. tile.position.y = y * scale;
  607. if (flipped !== '')
  608. tile.position.x += scale;
  609. tile.visible = true;
  610. }
  611. tile.z = k;
  612. cellSprites.push(tile);
  613. cellSprites.visible = true;
  614. });
  615. });
  616. });
  617. container.children.sort((a, b) => a.z - b.z);
  618. },
  619. render: function () {
  620. if (!this.stage)
  621. return;
  622. effects.render();
  623. particleLayers.forEach(p => particleEngines[p].update());
  624. this.renderer.render(this.stage);
  625. }
  626. };
  627. });