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.
 
 
 

830 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.map = map;
  229. this.titleScreen = false;
  230. physics.init(collisionMap);
  231. let w = this.w = map.length;
  232. let h = this.h = map[0].length;
  233. for (let i = 0; i < w; i++) {
  234. let row = map[i];
  235. for (let j = 0; j < h; j++) {
  236. if (!row[j].split)
  237. row[j] += '';
  238. row[j] = row[j].split(',');
  239. }
  240. }
  241. this.clean();
  242. spritePool.clean();
  243. this.stage.filters = [new PIXI.filters.AlphaFilter()];
  244. this.stage.filterArea = new PIXI.Rectangle(0, 0, Math.max(w * scale, this.width), Math.max(h * scale, this.height));
  245. this.hiddenRooms = hiddenRooms;
  246. this.sprites = _.get2dArray(w, h, 'array');
  247. this.stage.children.sort((a, b) => {
  248. if (a.layer === 'tiles')
  249. return -1;
  250. else if (b.layer === 'tiles')
  251. return 1;
  252. return 0;
  253. });
  254. if (this.zoneId !== null) {
  255. events.emit('onRezone', {
  256. oldZoneId: this.zoneId,
  257. newZoneId: zoneId
  258. });
  259. }
  260. this.zoneId = zoneId;
  261. clientObjects.forEach(c => {
  262. c.zoneId = this.zoneId;
  263. events.emit('onGetObject', c);
  264. });
  265. if (staticCamera) {
  266. this.staticCamera = true;
  267. this.setPosition({
  268. pos: {
  269. x: cameraPosition.x,
  270. y: cameraPosition.y
  271. },
  272. instant: true,
  273. staticPosition: true
  274. });
  275. } else
  276. this.staticCamera = false;
  277. //Normally, the mounts mod queues this event when unmounting.
  278. // If we rezone, our effects are destroyed, so the event is queued,
  279. // but flushForTarget clears the event right after and the event is never received.
  280. // We emit it again here to make sure the speed is reset after entering the new zone.
  281. events.emit('onMoveSpeedChange', 0);
  282. },
  283. /*
  284. pos: { x, y }
  285. The x and y positions the camera should be centered on (not yet multiplied by scale)
  286. instant: boolean
  287. should the camera pan to the location or not
  288. */
  289. setPosition: function ({ pos = { x: 0, y: 0 }, instant, staticPosition }) {
  290. let { x, y } = pos;
  291. x = (x - (this.width / (scale * 2))) * scale;
  292. y = (y - (this.height / (scale * 2))) * scale;
  293. let player = window.player;
  294. if (player) {
  295. let px = player.x;
  296. let py = player.y;
  297. let hiddenRooms = this.hiddenRooms || [];
  298. let hLen = hiddenRooms.length;
  299. for (let i = 0; i < hLen; i++) {
  300. let h = hiddenRooms[i];
  301. if (!h.discoverable)
  302. continue;
  303. if (
  304. px < h.x ||
  305. px >= h.x + h.width ||
  306. py < h.y ||
  307. py >= h.y + h.height ||
  308. !physics.isInPolygon(px, py, h.area)
  309. )
  310. continue;
  311. h.discovered = true;
  312. }
  313. }
  314. const staticCamera = window.staticCamera ?? this.staticCamera;
  315. if (staticCamera && staticPosition === undefined) {
  316. this.updateSprites();
  317. return;
  318. }
  319. if (instant) {
  320. this.moveTo = null;
  321. this.pos = {
  322. x,
  323. y
  324. };
  325. this.stage.x = -~~x;
  326. this.stage.y = -~~y;
  327. } else {
  328. this.moveTo = {
  329. x,
  330. y
  331. };
  332. }
  333. this.updateSprites();
  334. },
  335. isVisible: function (x, y) {
  336. let stage = this.stage;
  337. let sx = -stage.x;
  338. let sy = -stage.y;
  339. let sw = this.width;
  340. let sh = this.height;
  341. return (!(x < sx || y < sy || x >= sx + sw || y >= sy + sh));
  342. },
  343. isHidden: function (x, y) {
  344. let hiddenRooms = this.hiddenRooms;
  345. let hLen = hiddenRooms.length;
  346. if (!hLen)
  347. return false;
  348. const { player: { x: px, y: py } } = window;
  349. let foundVisibleLayer = null;
  350. let foundHiddenLayer = null;
  351. const fnTileInArea = physics.isInArea.bind(physics, x, y);
  352. const fnPlayerInArea = physics.isInArea.bind(physics, px, py);
  353. hiddenRooms.forEach(h => {
  354. const { discovered, layer, interior } = h;
  355. const playerInHider = fnPlayerInArea(h);
  356. const tileInHider = fnTileInArea(h);
  357. if (playerInHider) {
  358. if (interior && !tileInHider) {
  359. foundHiddenLayer = layer;
  360. return;
  361. }
  362. } else if (tileInHider && !discovered) {
  363. foundHiddenLayer = layer;
  364. return;
  365. } else if (tileInHider && discovered) {
  366. foundVisibleLayer = layer;
  367. return;
  368. }
  369. if (!tileInHider)
  370. return;
  371. foundVisibleLayer = layer;
  372. });
  373. //We compare hider layers to cater for hiders inside hiders
  374. return (
  375. foundHiddenLayer > foundVisibleLayer ||
  376. (
  377. foundHiddenLayer === 0 &&
  378. foundVisibleLayer === null
  379. )
  380. );
  381. },
  382. updateSprites: function () {
  383. updateSprites(this);
  384. },
  385. update: function () {
  386. let time = +new Date();
  387. if (this.moveTo) {
  388. let deltaX = this.moveTo.x - this.pos.x;
  389. let deltaY = this.moveTo.y - this.pos.y;
  390. if (deltaX !== 0 || deltaY !== 0) {
  391. let distance = Math.max(Math.abs(deltaX), Math.abs(deltaY));
  392. let moveSpeedMax = this.moveSpeedMax;
  393. if (this.moveSpeed < moveSpeedMax)
  394. this.moveSpeed += this.moveSpeedInc;
  395. let moveSpeed = this.moveSpeed;
  396. if (moveSpeedMax < 1.6)
  397. moveSpeed *= 1 + (distance / 200);
  398. let elapsed = time - this.lastTick;
  399. moveSpeed *= (elapsed / 15);
  400. if (moveSpeed > distance)
  401. moveSpeed = distance;
  402. deltaX = (deltaX / distance) * moveSpeed;
  403. deltaY = (deltaY / distance) * moveSpeed;
  404. this.pos.x = this.pos.x + deltaX;
  405. this.pos.y = this.pos.y + deltaY;
  406. } else {
  407. this.moveSpeed = 0;
  408. this.moveTo = null;
  409. }
  410. const staticCamera = this.staticCamera || window.staticCamera;
  411. let stage = this.stage;
  412. if (staticCamera !== true) {
  413. stage.x = -~~this.pos.x;
  414. stage.y = -~~this.pos.y;
  415. }
  416. let halfScale = scale / 2;
  417. if (Math.abs(stage.x - this.lastUpdatePos.x) > halfScale || Math.abs(stage.y - this.lastUpdatePos.y) > halfScale)
  418. this.updateSprites();
  419. events.emit('onSceneMove');
  420. }
  421. this.lastTick = time;
  422. },
  423. buildContainer: function (obj) {
  424. let container = new PIXI.Container();
  425. this.layers[obj.layerName || obj.sheetName].addChild(container);
  426. return container;
  427. },
  428. buildRectangle: function (obj) {
  429. let graphics = new PIXI.Graphics();
  430. let alpha = obj.alpha;
  431. if (obj.has('alpha'))
  432. graphics.alpha = alpha;
  433. let fillAlpha = obj.fillAlpha;
  434. if (obj.has('fillAlpha'))
  435. fillAlpha = 1;
  436. graphics.beginFill(obj.color || '0x48edff', fillAlpha);
  437. if (obj.strokeColor)
  438. graphics.lineStyle(scaleMult, obj.strokeColor);
  439. graphics.drawRect(0, 0, obj.w, obj.h);
  440. graphics.endFill();
  441. (obj.parent || this.layers[obj.layerName || obj.sheetName]).addChild(graphics);
  442. graphics.position.x = obj.x;
  443. graphics.position.y = obj.y;
  444. return graphics;
  445. },
  446. moveRectangle: function (obj) {
  447. obj.sprite.position.x = obj.x;
  448. obj.sprite.position.y = obj.y;
  449. obj.sprite.width = obj.w;
  450. obj.sprite.height = obj.h;
  451. },
  452. buildObject: function (obj) {
  453. const { sheetName, parent: container, layerName, visible = true } = obj;
  454. const sprite = new PIXI.Sprite();
  455. obj.sprite = sprite;
  456. this.setSprite(obj);
  457. sprite.visible = visible;
  458. const spriteContainer = container || this.layers[layerName || sheetName] || this.layers.objects;
  459. spriteContainer.addChild(sprite);
  460. obj.w = sprite.width;
  461. obj.h = sprite.height;
  462. return sprite;
  463. },
  464. addFilter: function (sprite, config) {
  465. const filter = new shaderOutline(config);
  466. if (!sprite.filters)
  467. sprite.filters = [filter];
  468. else
  469. sprite.filters.push(filter);
  470. return filter;
  471. },
  472. removeFilter: function (sprite) {
  473. if (sprite.filters)
  474. sprite.filters = null;
  475. },
  476. buildText: function (obj) {
  477. const { text, visible, x, y, parent: spriteParent, layerName } = obj;
  478. const { fontSize = 14, color = 0xF2F5F5 } = obj;
  479. const textSprite = new PIXI.Text(text, {
  480. fontFamily: 'bitty',
  481. fontSize: fontSize,
  482. fill: color,
  483. stroke: 0x2d2136,
  484. strokeThickness: 4
  485. });
  486. if (visible === false)
  487. textSprite.visible = false;
  488. textSprite.x = x - (textSprite.width / 2);
  489. textSprite.y = y;
  490. const parentSprite = spriteParent ?? this.layers[layerName];
  491. parentSprite.addChild(textSprite);
  492. return textSprite;
  493. },
  494. buildEmitter: function (config) {
  495. const { layerName = 'particles' } = config;
  496. const particleEngine = particleEngines[layerName];
  497. return particleEngine.buildEmitter(config);
  498. },
  499. destroyEmitter: function (emitter) {
  500. const particleEngine = emitter.particleEngine;
  501. particleEngine.destroyEmitter(emitter);
  502. },
  503. setSprite: function (obj) {
  504. const { sprite, sheetName, cell } = obj;
  505. const bigSheets = globals.clientConfig.bigTextures;
  506. const isBigSheet = bigSheets.includes(sheetName);
  507. const newSize = isBigSheet ? 24 : 8;
  508. obj.w = newSize * scaleMult;
  509. obj.h = obj.w;
  510. sprite.width = obj.w;
  511. sprite.height = obj.h;
  512. sprite.texture = this.getTexture(sheetName, cell, newSize);
  513. if (newSize !== sprite.size) {
  514. sprite.size = newSize;
  515. this.setSpritePosition(obj);
  516. }
  517. },
  518. setSpritePosition: function (obj) {
  519. const { sprite, x, y, flipX, offsetX = 0, offsetY = 0 } = obj;
  520. sprite.x = (x * scale) + (flipX ? scale : 0) + offsetX;
  521. const oldY = sprite.y;
  522. sprite.y = (y * scale) + offsetY;
  523. if (sprite.width > scale) {
  524. if (flipX)
  525. sprite.x += scale;
  526. else
  527. sprite.x -= scale;
  528. sprite.y -= (scale * 2);
  529. }
  530. if (oldY !== sprite.y)
  531. this.reorder();
  532. sprite.scale.x = flipX ? -scaleMult : scaleMult;
  533. },
  534. reorder: function () {
  535. this.layers.mobs.children.sort((a, b) => b.y - a.y);
  536. },
  537. destroyObject: function (obj) {
  538. if (obj.sprite.parent)
  539. obj.sprite.parent.removeChild(obj.sprite);
  540. },
  541. //Changes the moveSpeedMax and moveSpeedInc variables
  542. // moveSpeed changes when mounting and unmounting
  543. // moveSpeed: 0 | moveSpeedMax: 1.5 | moveSpeedInc: 0.5
  544. // moveSpeed: 200 | moveSpeedMax: 5.5 | moveSpeedInc: 0.2
  545. // Between these values we should follow an exponential curve for moveSpeedInc since
  546. // a higher chance will proc more often, meaning the buildup in distance becomes greater
  547. adaptCameraMoveSpeed: function (moveSpeed) {
  548. const factor = Math.sqrt(moveSpeed);
  549. const maxValue = Math.sqrt(200);
  550. this.moveSpeedMax = 1.5 + ((moveSpeed / 200) * 3.5);
  551. this.moveSpeedInc = 0.2 + (((maxValue - factor) / maxValue) * 0.3);
  552. },
  553. updateMapAtPosition: function (x, y, mapCellString) {
  554. const { map, sprites, layers: { tileSprites: container } } = this;
  555. const row = sprites[x];
  556. if (!row)
  557. return;
  558. const cell = row[y];
  559. if (!cell)
  560. return;
  561. cell.forEach(c => {
  562. c.visible = false;
  563. spritePool.store(c);
  564. });
  565. cell.length = 0;
  566. map[x][y] = mapCellString.split(',');
  567. map[x][y].forEach(m => {
  568. m--;
  569. let tile = spritePool.getSprite(m);
  570. if (!tile) {
  571. tile = this.buildTile(m, x, y);
  572. container.addChild(tile);
  573. tile.type = m;
  574. tile.sheetNum = tileOpacity.getSheetNum(m);
  575. } else {
  576. tile.position.x = x * scale;
  577. tile.position.y = y * scale;
  578. tile.visible = true;
  579. }
  580. cell.push(tile);
  581. cell.visible = true;
  582. });
  583. },
  584. updateMapRows: function (rows) {
  585. const { map, sprites, layers: { tileSprites: container } } = this;
  586. rows.forEach(({ rowNumber: x, cols }) => {
  587. const row = sprites[x];
  588. cols.forEach(({ colNumber: y, cells }) => {
  589. const cellSprites = row[y];
  590. cellSprites.forEach(c => {
  591. c.visible = false;
  592. spritePool.store(c);
  593. });
  594. cellSprites.length = 0;
  595. map[x][y] = cells;
  596. cells.forEach((m, k) => {
  597. m--;
  598. let flipped = '';
  599. if (tileOpacity.canFlip(m)) {
  600. if (mRandom() < 0.5)
  601. flipped = 'flip';
  602. }
  603. let tile = spritePool.getSprite(flipped + m);
  604. if (!tile) {
  605. tile = this.buildTile(m, x, y);
  606. container.addChild(tile);
  607. tile.type = m;
  608. tile.sheetNum = tileOpacity.getSheetNum(m);
  609. } else {
  610. tile.position.x = x * scale;
  611. tile.position.y = y * scale;
  612. if (flipped !== '')
  613. tile.position.x += scale;
  614. tile.visible = true;
  615. }
  616. tile.z = k;
  617. cellSprites.push(tile);
  618. cellSprites.visible = true;
  619. });
  620. });
  621. });
  622. container.children.sort((a, b) => a.z - b.z);
  623. },
  624. render: function () {
  625. if (!this.stage)
  626. return;
  627. effects.render();
  628. particleLayers.forEach(p => particleEngines[p].update());
  629. this.renderer.render(this.stage);
  630. }
  631. };
  632. });