├── .gitignore ├── images ├── play.png ├── pause.png ├── crosshair.png ├── settings.png └── fullscreen.png ├── config.template.sh ├── tsconfig.json ├── styles ├── mapviewer.css └── replayviewer.css ├── LICENSE ├── src ├── Utils.ts ├── Event.ts ├── RouteLine.ts ├── OptionsMenu.ts ├── BinaryReader.ts ├── ReplayFile.ts ├── KeyDisplay.ts ├── ReplayControls.ts └── ReplayViewer.ts ├── README.md ├── index.template.html └── js ├── replayviewer.d.ts ├── sourceutils.d.ts └── replayviewer.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.lnk 2 | config.sh 3 | -------------------------------------------------------------------------------- /images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metapyziks/GOKZReplayViewer/HEAD/images/play.png -------------------------------------------------------------------------------- /images/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metapyziks/GOKZReplayViewer/HEAD/images/pause.png -------------------------------------------------------------------------------- /images/crosshair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metapyziks/GOKZReplayViewer/HEAD/images/crosshair.png -------------------------------------------------------------------------------- /images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metapyziks/GOKZReplayViewer/HEAD/images/settings.png -------------------------------------------------------------------------------- /images/fullscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Metapyziks/GOKZReplayViewer/HEAD/images/fullscreen.png -------------------------------------------------------------------------------- /config.template.sh: -------------------------------------------------------------------------------- 1 | TARGETDIR= 2 | RESOURCEDIR=resources 3 | BASEURL= 4 | MAPSURL= 5 | INDEXTEMPLATE=index.template.html 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "noEmitOnError": true, 5 | "removeComments": false, 6 | "sourceMap": false, 7 | "declaration": true, 8 | "outFile": "js/replayviewer.js", 9 | "target": "es5" 10 | }, 11 | "compileOnSave": true, 12 | "include": ["src/**/*.ts"], 13 | "exclude": [ 14 | "node_modules", 15 | "wwwroot" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /styles/mapviewer.css: -------------------------------------------------------------------------------- 1 | .map-viewer canvas { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | .map-viewer:-webkit-full-screen { 8 | position: absolute !important; 9 | top: 0 !important; 10 | left: 0 !important; 11 | bottom: 0 !important; 12 | right: 0 !important; 13 | } 14 | 15 | .map-viewer .side-panel { 16 | position: relative; 17 | float: right; 18 | clear: right; 19 | z-index: 32; 20 | width: 256px; 21 | background-color: rgba(0, 0, 0, 0.25); 22 | color: white; 23 | font-family: sans-serif; 24 | padding: 16px; 25 | margin: 8px; 26 | user-select: none; 27 | } 28 | 29 | .map-viewer .side-panel .slider { 30 | width: 240px; 31 | margin: 8px; 32 | } 33 | 34 | .map-viewer .side-panel .label { 35 | display: inline-block; 36 | font-size: 10pt; 37 | padding-right: 8px; 38 | color: #cccccc; 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 James King 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | namespace Gokz { 2 | export class Utils { 3 | static deltaAngle(a: number, b: number): number { 4 | return (b - a) - Math.floor((b - a + 180) / 360) * 360; 5 | } 6 | 7 | static hermiteValue(p0: number, p1: number, p2: number, p3: number, t: number): number { 8 | const m0 = (p2 - p0) * 0.5; 9 | const m1 = (p3 - p1) * 0.5; 10 | 11 | const t2 = t * t; 12 | const t3 = t * t * t; 13 | 14 | return (2 * t3 - 3 * t2 + 1) * p1 + (t3 - 2 * t2 + t) * m0 15 | + (-2 * t3 + 3 * t2) * p2 + (t3 - t2) * m1; 16 | } 17 | 18 | static hermitePosition(p0: Facepunch.Vector3, p1: Facepunch.Vector3, 19 | p2: Facepunch.Vector3, p3: Facepunch.Vector3, t: number, out: Facepunch.Vector3) { 20 | out.x = Utils.hermiteValue(p0.x, p1.x, p2.x, p3.x, t); 21 | out.y = Utils.hermiteValue(p0.y, p1.y, p2.y, p3.y, t); 22 | out.z = Utils.hermiteValue(p0.z, p1.z, p2.z, p3.z, t); 23 | } 24 | 25 | static hermiteAngles(a0: Facepunch.Vector2, a1: Facepunch.Vector2, 26 | a2: Facepunch.Vector2, a3: Facepunch.Vector2, t: number, out: Facepunch.Vector2) { 27 | out.x = Utils.hermiteValue( 28 | a1.x + Utils.deltaAngle(a1.x, a0.x), 29 | a1.x, 30 | a1.x + Utils.deltaAngle(a1.x, a2.x), 31 | a1.x + Utils.deltaAngle(a1.x, a3.x), t); 32 | out.y = Utils.hermiteValue( 33 | a1.y + Utils.deltaAngle(a1.y, a0.y), 34 | a1.y, 35 | a1.y + Utils.deltaAngle(a1.y, a2.y), 36 | a1.y + Utils.deltaAngle(a1.y, a3.y), t); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Event.ts: -------------------------------------------------------------------------------- 1 | namespace Gokz { 2 | export type Handler = (args: TEventArgs, sender: TSender) => void; 3 | export class Event { 4 | private readonly sender: TSender; 5 | private handlers: Handler[] = []; 6 | 7 | constructor(sender: TSender) { 8 | this.sender = sender; 9 | } 10 | 11 | addListener(handler: Handler): void { 12 | this.handlers.push(handler); 13 | } 14 | 15 | removeListener(handler: Handler): boolean { 16 | const index = this.handlers.indexOf(handler); 17 | if (index === -1) return false; 18 | 19 | this.handlers.splice(index, 1); 20 | return true; 21 | } 22 | 23 | clearListeners(): void { 24 | this.handlers = []; 25 | } 26 | 27 | dispatch(args: TEventArgs): void { 28 | const count = this.handlers.length; 29 | for (let i = 0; i < count; ++i) { 30 | this.handlers[i](args, this.sender); 31 | } 32 | } 33 | } 34 | 35 | export class ChangedEvent extends Event { 36 | private prevValue: TValue; 37 | private equalityComparison: (a: TValue, b: TValue) => boolean; 38 | 39 | constructor(sender: TSender, equalityComparison?: (a: TValue, b: TValue) => boolean) { 40 | super(sender); 41 | 42 | if (equalityComparison != null) { 43 | this.equalityComparison = equalityComparison; 44 | } else { 45 | this.equalityComparison = (a, b) => a === b; 46 | } 47 | } 48 | 49 | reset(): void { 50 | this.prevValue = undefined; 51 | } 52 | 53 | update(value: TValue, args?: TEventArgs): void { 54 | if (this.equalityComparison(this.prevValue, value)) return; 55 | this.prevValue = value; 56 | this.dispatch(args === undefined ? value as any : args); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GOKZ Replay Viewer 2 | Prototype of a WebGL GOKZ recording player. 3 | 4 | ![](https://files.facepunch.com/ziks/2017/October/29/chrome_2017-10-29_22-27-47.png) 5 | 6 | ## Try it out! 7 | https://metapyziks.github.io/GOKZReplayViewer/ 8 | 9 | ## Usage 10 | ### Export Maps 11 | First you'll need to use [SourceUtils](https://github.com/Metapyziks/SourceUtils) to export a bunch of maps, and 12 | host them on a web server. 13 | 14 | ### Copy Resources 15 | Your web page will need the `images`, `js` and `styles` folders from this repo. 16 | 17 | ### Setup Web Page 18 | 19 | Make sure you include these references in your web page: 20 | 21 | ```html 22 | Needed for decompressing map content 23 | 24 | 25 | 26 | Map / replay viewer scripts 27 | 28 | 29 | 30 | 31 | Map / replay viewer styles 32 | 33 | 34 | ``` 35 | 36 | Then in the body of your page add a div that will host the canvas: 37 | 38 | ```html 39 |
40 | ``` 41 | 42 | Finally, use this JavaScript to create the viewer when the page loads: 43 | 44 | ```javascript 45 | var viewer; 46 | window.onload = function() { 47 | // Create a replay viewer canvas inside the #example-viewer div 48 | viewer = new Gokz.ReplayViewer(document.getElementById("example-viewer")); 49 | 50 | // Show FPS and frame time 51 | viewer.showDebugPanel = true; 52 | 53 | // Start playing immediately when the replay is loaded 54 | viewer.isPlaying = true; 55 | 56 | // Set the URL to look for maps exported using https://github.com/Metapyziks/SourceUtils 57 | // The example below will make the app look for de_dust2 at http://www.example.com/maps/de_dust2/index.json 58 | viewer.mapBaseUrl = "http://www.example.com/maps"; 59 | 60 | // Start downloading a replay 61 | viewer.loadReplay("http://www.example.com/replays/test-replay.replay"); 62 | 63 | // Attach an event handler to when a replay file is loaded 64 | viewer.replayLoaded.addListener(function(replay) { 65 | console.log("Replay is on map: " + replay.mapName + ", ran by: " + replay.playerName); 66 | }); 67 | 68 | // Attach an event handler to when the current tick changes 69 | viewer.tickChanged.addListener(function(tickData) { 70 | if ((tickData.buttons & Gokz.Button.Jump) !== 0) { 71 | console.log("Jump is pressed on tick: " + tickData.tick); 72 | } 73 | }); 74 | 75 | // Start the main loop 76 | viewer.animate(); 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /src/RouteLine.ts: -------------------------------------------------------------------------------- 1 | namespace Gokz { 2 | interface IRouteLineSegment { 3 | debugLine: WebGame.DebugLine; 4 | clusters: {[index: number]: boolean}; 5 | } 6 | 7 | export class RouteLine extends SourceUtils.Entities.PvsEntity { 8 | private static readonly segmentTicks = 60 * 128; 9 | 10 | private readonly segments: IRouteLineSegment[]; 11 | 12 | private isVisible = false; 13 | 14 | get visible(): boolean { 15 | return this.isVisible; 16 | } 17 | 18 | set visible(value: boolean) { 19 | if (this.isVisible === value) return; 20 | 21 | this.isVisible = value; 22 | if (value) { 23 | this.map.addPvsEntity(this); 24 | } else { 25 | this.map.removePvsEntity(this); 26 | } 27 | 28 | this.map.viewer.forceDrawListInvalidation(true); 29 | } 30 | 31 | constructor(map: SourceUtils.Map, replay: ReplayFile) { 32 | super(map, { classname: "route_line", clusters: null }); 33 | 34 | this.segments = new Array(Math.ceil(replay.tickCount / RouteLine.segmentTicks)); 35 | 36 | const tickData = new TickData(); 37 | const progressScale = 16 / replay.tickRate; 38 | const lastPos = new Facepunch.Vector3(); 39 | const currPos = new Facepunch.Vector3(); 40 | 41 | for (let i = 0; i < this.segments.length; ++i) { 42 | const firstTick = i * RouteLine.segmentTicks; 43 | const lastTick = Math.min((i + 1) * RouteLine.segmentTicks, replay.tickCount - 1); 44 | 45 | const segment = this.segments[i] = { 46 | debugLine: new WebGame.DebugLine(map.viewer), 47 | clusters: {} 48 | }; 49 | 50 | const debugLine = segment.debugLine; 51 | const clusters = segment.clusters; 52 | 53 | debugLine.setColor({x: 0.125, y: 0.75, z: 0.125}, {x: 0.0, y: 0.25, z: 0.0}); 54 | debugLine.frequency = 4.0; 55 | 56 | let lineStartTick = firstTick; 57 | 58 | for (let t = firstTick; t <= lastTick; ++t) { 59 | replay.getTickData(t, tickData); 60 | 61 | currPos.copy(tickData.position); 62 | currPos.z += 16; 63 | 64 | const leaf = map.getLeafAt(currPos); 65 | if (leaf != null && leaf.cluster !== -1) { 66 | clusters[leaf.cluster] = true; 67 | } 68 | 69 | // Start new line if first in segment or player teleported 70 | if (t === firstTick || lastPos.sub(currPos).lengthSq() > 1024.0) { 71 | debugLine.moveTo(currPos); 72 | lineStartTick = t; 73 | } else { 74 | debugLine.lineTo(currPos, (t - lineStartTick) * progressScale); 75 | } 76 | 77 | lastPos.copy(currPos); 78 | } 79 | 80 | debugLine.update(); 81 | } 82 | } 83 | 84 | protected onPopulateDrawList(drawList: WebGame.DrawList, clusters: number[]): void { 85 | for (let segment of this.segments) { 86 | if (clusters == null) { 87 | drawList.addItem(segment.debugLine); 88 | continue; 89 | } 90 | 91 | const segmentClusters = segment.clusters; 92 | 93 | for (let cluster of clusters) { 94 | if (segmentClusters[cluster]) { 95 | drawList.addItem(segment.debugLine); 96 | break; 97 | } 98 | } 99 | } 100 | } 101 | 102 | dispose(): void { 103 | this.visible = false; 104 | 105 | for (let segment of this.segments) { 106 | segment.debugLine.dispose(); 107 | } 108 | 109 | this.segments.splice(0, this.segments.length); 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /src/OptionsMenu.ts: -------------------------------------------------------------------------------- 1 | namespace Gokz { 2 | export class OptionsMenu { 3 | private readonly viewer: ReplayViewer; 4 | 5 | readonly element: HTMLElement; 6 | readonly titleElem: HTMLSpanElement; 7 | readonly optionContainer: HTMLElement; 8 | 9 | constructor(viewer: ReplayViewer, container?: HTMLElement) { 10 | this.viewer = viewer; 11 | 12 | if (container === undefined) { 13 | container = this.viewer.container; 14 | } 15 | 16 | const element = this.element = document.createElement("div"); 17 | element.classList.add("options-menu"); 18 | element.innerHTML = `
`; 19 | 20 | container.appendChild(element); 21 | 22 | this.titleElem = element.getElementsByClassName("options-title")[0] as HTMLSpanElement; 23 | this.optionContainer = element.getElementsByClassName("options-list")[0] as HTMLElement; 24 | 25 | viewer.showOptionsChanged.addListener(showOptions => { 26 | if (showOptions) this.show(); 27 | else this.hide(); 28 | }); 29 | } 30 | 31 | show(): void { 32 | this.element.style.display = "block"; 33 | this.showMainPage(); 34 | 35 | if (this.viewer.controls != null) { 36 | this.viewer.controls.hideSpeedControl(); 37 | } 38 | } 39 | 40 | hide(): void { 41 | this.element.style.display = "none"; 42 | this.clear(); 43 | } 44 | 45 | private clear(): void { 46 | this.optionContainer.innerHTML = ""; 47 | } 48 | 49 | private showMainPage(): void { 50 | const viewer = this.viewer; 51 | 52 | this.clear(); 53 | this.setTitle("Options"); 54 | this.addToggleOption("Show Crosshair", 55 | () => viewer.showCrosshair, 56 | value => viewer.showCrosshair = value, 57 | viewer.showCrosshairChanged); 58 | this.addToggleOption("Show Framerate", 59 | () => viewer.showDebugPanel, 60 | value => viewer.showDebugPanel = value); 61 | this.addToggleOption("Show Key Presses", 62 | () => viewer.showKeyDisplay, 63 | value => viewer.showKeyDisplay = value, 64 | viewer.showKeyDisplayChanged); 65 | this.addToggleOption("Free Camera", 66 | () => viewer.cameraMode === SourceUtils.CameraMode.FreeCam, 67 | value => viewer.cameraMode = value 68 | ? SourceUtils.CameraMode.FreeCam 69 | : SourceUtils.CameraMode.Fixed, 70 | viewer.cameraModeChanged); 71 | } 72 | 73 | private setTitle(title: string): void { 74 | this.titleElem.innerText = title; 75 | } 76 | 77 | private addToggleOption(label: string, getter: () => boolean, setter: (value: boolean) => void, changed?: Event): void { 78 | const option = document.createElement("div"); 79 | option.classList.add("option"); 80 | option.innerHTML = `${label}
`; 81 | 82 | this.optionContainer.appendChild(option); 83 | 84 | const toggle = option.getElementsByClassName("toggle")[0] as HTMLElement; 85 | 86 | const updateOption = () => { 87 | if (getter()) { 88 | toggle.classList.add("on"); 89 | } else { 90 | toggle.classList.remove("on"); 91 | } 92 | }; 93 | 94 | option.addEventListener("click", ev => { 95 | setter(!getter()); 96 | 97 | if (changed == null){ 98 | updateOption(); 99 | } 100 | }); 101 | 102 | if (changed != null) { 103 | changed.addListener(() => updateOption()); 104 | } 105 | 106 | updateOption(); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /src/BinaryReader.ts: -------------------------------------------------------------------------------- 1 | namespace Gokz { 2 | export enum SeekOrigin { 3 | Begin, 4 | Current, 5 | End 6 | } 7 | 8 | export class BinaryReader { 9 | private readonly buffer: ArrayBuffer; 10 | private readonly view: DataView; 11 | private offset: number; 12 | 13 | constructor(buffer: ArrayBuffer) { 14 | this.buffer = buffer; 15 | this.view = new DataView(buffer); 16 | this.offset = 0; 17 | } 18 | 19 | seek(offset: number, origin: SeekOrigin): number { 20 | switch (origin) { 21 | case SeekOrigin.Begin: 22 | return this.offset = offset; 23 | case SeekOrigin.End: 24 | return this.offset = this.buffer.byteLength - offset; 25 | default: 26 | return this.offset = this.offset + offset; 27 | } 28 | } 29 | 30 | getOffset(): number { 31 | return this.offset; 32 | } 33 | 34 | readUint8(): number { 35 | const value = this.view.getUint8(this.offset); 36 | this.offset += 1; 37 | return value; 38 | } 39 | 40 | readInt32(): number { 41 | const value = this.view.getInt32(this.offset, true); 42 | this.offset += 4; 43 | return value; 44 | } 45 | 46 | readUint32(): number { 47 | const value = this.view.getUint32(this.offset, true); 48 | this.offset += 4; 49 | return value; 50 | } 51 | 52 | readFloat32(): number { 53 | const value = this.view.getFloat32(this.offset, true); 54 | this.offset += 4; 55 | return value; 56 | } 57 | 58 | // http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt 59 | 60 | /* utf.js - UTF-8 <=> UTF-16 convertion 61 | * 62 | * Copyright (C) 1999 Masanao Izumo 63 | * Version: 1.0 64 | * LastModified: Dec 25 1999 65 | * This library is free. You can redistribute it and/or modify it. 66 | */ 67 | 68 | static utf8ArrayToStr(array: number[]): string { 69 | var out, i, len, c; 70 | var char2, char3; 71 | 72 | out = ""; 73 | len = array.length; 74 | i = 0; 75 | while(i < len) { 76 | c = array[i++]; 77 | switch(c >> 4) { 78 | case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: 79 | // 0xxxxxxx 80 | out += String.fromCharCode(c); 81 | break; 82 | case 12: case 13: 83 | // 110x xxxx 10xx xxxx 84 | char2 = array[i++]; 85 | out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); 86 | break; 87 | case 14: 88 | // 1110 xxxx 10xx xxxx 10xx xxxx 89 | char2 = array[i++]; 90 | char3 = array[i++]; 91 | out += String.fromCharCode(((c & 0x0F) << 12) | 92 | ((char2 & 0x3F) << 6) | 93 | ((char3 & 0x3F) << 0)); 94 | break; 95 | } 96 | } 97 | 98 | return out; 99 | } 100 | 101 | readString(length?: number): string { 102 | if (length === undefined) { 103 | length = this.readUint8(); 104 | } 105 | 106 | let chars = new Array(length); 107 | for (let i = 0; i < length; ++i) { 108 | chars[i] = this.readUint8(); 109 | } 110 | 111 | return BinaryReader.utf8ArrayToStr(chars); 112 | } 113 | 114 | readVector2(vec?: Facepunch.Vector2): Facepunch.Vector2 { 115 | if (vec === undefined) vec = new Facepunch.Vector2(); 116 | vec.set(this.readFloat32(), this.readFloat32()); 117 | return vec; 118 | } 119 | 120 | readVector3(vec?: Facepunch.Vector3): Facepunch.Vector3 { 121 | if (vec === undefined) vec = new Facepunch.Vector3(); 122 | vec.set(this.readFloat32(), this.readFloat32(), this.readFloat32()); 123 | return vec; 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | GOKZ Replay Viewer 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 49 | 50 | 51 |

GOKZ Replay Viewer

52 |
53 | 87 |
88 |
89 |

Example Replays

90 |

kz_exps_cursedjourney

91 | 95 |

kz_colors_v2

96 | 99 |

kz_reach_v2

100 | 105 |
106 | 107 | 108 | -------------------------------------------------------------------------------- /src/ReplayFile.ts: -------------------------------------------------------------------------------- 1 | namespace Gokz { 2 | export enum GlobalMode { 3 | Vanilla = 0, 4 | KzSimple = 1, 5 | KzTimer = 2 6 | } 7 | 8 | export enum GlobalStyle { 9 | Normal = 0 10 | } 11 | 12 | export enum Button { 13 | Attack = 1 << 0, 14 | Jump = 1 << 1, 15 | Duck = 1 << 2, 16 | Forward = 1 << 3, 17 | Back = 1 << 4, 18 | Use = 1 << 5, 19 | Cancel = 1 << 6, 20 | Left = 1 << 7, 21 | Right = 1 << 8, 22 | MoveLeft = 1 << 9, 23 | MoveRight = 1 << 10, 24 | Attack2 = 1 << 11, 25 | Run = 1 << 12, 26 | Reload = 1 << 13, 27 | Alt1 = 1 << 14, 28 | Alt2 = 1 << 15, 29 | Score = 1 << 16, 30 | Speed = 1 << 17, 31 | Walk = 1 << 18, 32 | Zoom = 1 << 19, 33 | Weapon1 = 1 << 20, 34 | Weapon2 = 1 << 21, 35 | BullRush = 1 << 22, // ...what? 36 | Grenade1 = 1 << 23, 37 | Grenade2 = 1 << 24 38 | } 39 | 40 | export enum EntityFlag { 41 | OnGround = 1 << 0, 42 | Ducking = 1 << 1, 43 | WaterJump = 1 << 2, 44 | OnTrain = 1 << 3, 45 | InRain = 1 << 4, 46 | Frozen = 1 << 5, 47 | AtControls = 1 << 6, 48 | Client = 1 << 7, 49 | FakeClient = 1 << 8, 50 | InWater = 1 << 9, 51 | Fly = 1 << 10, 52 | Swim = 1 << 11, 53 | Conveyor = 1 << 12, 54 | Npc = 1 << 13, 55 | GodMode = 1 << 14, 56 | NoTarget = 1 << 15, 57 | AimTarget = 1 << 16, 58 | PartialGround = 1 << 17, 59 | StaticProp = 1 << 18, 60 | Graphed = 1 << 19, 61 | Grenade = 1 << 20, 62 | StepMovement = 1 << 21, 63 | DontTouch = 1 << 22, 64 | BaseVelocity = 1 << 23, 65 | WorldBrush = 1 << 24, 66 | Object = 1 << 25, 67 | KillMe = 1 << 26, 68 | OnFire = 1 << 27, 69 | Dissolving = 1 << 28, 70 | TransRagdoll = 1 << 29, 71 | UnblockableByPlayer = 1 << 30, 72 | Freezing = 1 << 31 73 | } 74 | 75 | export class TickData { 76 | readonly position = new Facepunch.Vector3(); 77 | readonly angles = new Facepunch.Vector2(); 78 | tick = -1; 79 | buttons: Button = 0; 80 | flags: EntityFlag = 0; 81 | 82 | getEyeHeight(): number { 83 | return (this.flags & EntityFlag.Ducking) != 0 ? 46 : 64; 84 | } 85 | } 86 | 87 | export class ReplayFile { 88 | static readonly MAGIC = 0x676F6B7A; 89 | 90 | private readonly reader: BinaryReader; 91 | private readonly firstTickOffset: number; 92 | private readonly tickSize: number; 93 | 94 | readonly formatVersion: number; 95 | readonly pluginVersion: string; 96 | 97 | readonly mapName: string; 98 | readonly course: number; 99 | readonly mode: GlobalMode; 100 | readonly style: GlobalStyle; 101 | readonly time: number; 102 | readonly teleportsUsed: number; 103 | readonly steamId: number; 104 | readonly steamId2: string; 105 | readonly playerName: string; 106 | readonly tickCount: number; 107 | readonly tickRate: number; 108 | 109 | constructor(data: ArrayBuffer) { 110 | const reader = this.reader = new BinaryReader(data); 111 | 112 | const magic = reader.readInt32(); 113 | if (magic !== ReplayFile.MAGIC) { 114 | throw "Unrecognised replay file format."; 115 | } 116 | 117 | this.formatVersion = reader.readUint8(); 118 | this.pluginVersion = reader.readString(); 119 | 120 | this.mapName = reader.readString(); 121 | this.course = reader.readInt32(); 122 | this.mode = reader.readInt32() as GlobalMode; 123 | this.style = reader.readInt32() as GlobalStyle; 124 | this.time = reader.readFloat32(); 125 | this.teleportsUsed = reader.readInt32(); 126 | this.steamId = reader.readInt32(); 127 | this.steamId2 = reader.readString(); 128 | reader.readString(); 129 | this.playerName = reader.readString(); 130 | this.tickCount = reader.readInt32(); 131 | this.tickRate = Math.round(this.tickCount / this.time); // todo 132 | 133 | this.firstTickOffset = reader.getOffset(); 134 | this.tickSize = 7 * 4; 135 | } 136 | 137 | getTickData(tick: number, data?: TickData): TickData { 138 | if (data === undefined) data = new TickData(); 139 | 140 | data.tick = tick; 141 | 142 | const reader = this.reader; 143 | reader.seek(this.firstTickOffset + this.tickSize * tick, SeekOrigin.Begin); 144 | 145 | reader.readVector3(data.position); 146 | reader.readVector2(data.angles); 147 | data.buttons = reader.readInt32(); 148 | data.flags = reader.readInt32(); 149 | 150 | return data; 151 | } 152 | 153 | clampTick(tick: number): number { 154 | return tick < 0 ? 0 : tick >= this.tickCount ? this.tickCount - 1 : tick; 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /styles/replayviewer.css: -------------------------------------------------------------------------------- 1 | .map-viewer { 2 | overflow: hidden; 3 | } 4 | 5 | .map-viewer .crosshair { 6 | position: absolute; 7 | width: 15px; 8 | height: 15px; 9 | margin-left: -8px; 10 | margin-right: -8px; 11 | left: 50%; 12 | top: 50%; 13 | background-image: url(../images/crosshair.png); 14 | } 15 | 16 | .map-viewer .playback-bar { 17 | position: absolute; 18 | left: 0; 19 | right: 0; 20 | bottom: 0; 21 | height: 40px; 22 | line-height: 40px; 23 | 24 | background-color: rgba(15, 15, 15, 0.825); 25 | font-weight: 550; 26 | 27 | transition: bottom 0.1s ease-out; 28 | } 29 | 30 | .map-viewer .playback-bar.hidden { 31 | bottom: -40px; 32 | transition: bottom 0.2s ease-in; 33 | } 34 | 35 | .map-viewer .playback-bar .scrubber-container { 36 | position: absolute; 37 | left: 136px; 38 | right: 160px; 39 | top: 0; 40 | bottom: 0; 41 | vertical-align: middle; 42 | } 43 | 44 | .map-viewer .playback-bar .scrubber-container .scrubber { 45 | width: 100%; 46 | height: 36px; 47 | } 48 | 49 | .map-viewer .playback-bar .time { 50 | position: absolute; 51 | left: 40px; 52 | width: 96px; 53 | top: 0; 54 | bottom: 0; 55 | text-align: center; 56 | user-select: none; 57 | } 58 | 59 | .map-viewer .playback-bar .speed { 60 | position: absolute; 61 | right: 92px; 62 | width: 56px; 63 | top: 0; 64 | bottom: 0; 65 | text-align: center; 66 | user-select: none; 67 | } 68 | 69 | .map-viewer .playback-bar .speed:hover { 70 | cursor: pointer; 71 | text-shadow: 0px 0px 4px rgba(255, 255, 255, 0.25); 72 | } 73 | 74 | .map-viewer .playback-bar .speed::before { 75 | content: "x"; 76 | } 77 | 78 | .map-viewer .playback-bar .control { 79 | position: absolute; 80 | width: 24px; 81 | height: 24px; 82 | top: 8px; 83 | } 84 | 85 | .map-viewer .playback-bar .control:hover { 86 | cursor: pointer; 87 | -webkit-filter: drop-shadow(0px 0px 4px rgba(255, 255, 255, 0.25)); 88 | filter: drop-shadow(0px 0px 4px rgba(255, 255, 255, 0.25)); 89 | } 90 | 91 | .map-viewer .playback-bar .pause { 92 | display: none; 93 | left: 16px; 94 | background-image: url(../images/pause.png); 95 | } 96 | 97 | .map-viewer .playback-bar .play { 98 | left: 16px; 99 | background-image: url(../images/play.png); 100 | } 101 | 102 | .map-viewer .playback-bar .settings { 103 | right: 56px; 104 | background-image: url(../images/settings.png); 105 | } 106 | 107 | .map-viewer .playback-bar .fullscreen { 108 | right: 16px; 109 | background-image: url(../images/fullscreen.png); 110 | } 111 | 112 | .map-viewer .speed-control { 113 | display: none; 114 | position: absolute; 115 | bottom: 48px; 116 | right: 8px; 117 | width: 256px; 118 | padding-left: 16px; 119 | padding-right: 16px; 120 | padding-top: 8px; 121 | padding-bottom: 8px; 122 | background-color: rgba(15, 15, 15, 0.825); 123 | } 124 | 125 | .map-viewer .speed-control .speed-slider { 126 | width: 100%; 127 | } 128 | 129 | .map-viewer .message { 130 | position: absolute; 131 | width: 512px; 132 | height: 64px; 133 | left: 50%; 134 | top: 50%; 135 | margin-left: -256px; 136 | margin-top: -32px; 137 | 138 | background-color: rgba(15, 15, 15, 0.825); 139 | color: white; 140 | font-family: sans-serif; 141 | text-align: center; 142 | line-height: 64px; 143 | user-select: none; 144 | } 145 | 146 | .map-viewer .key-display { 147 | position: absolute; 148 | width: 296px; 149 | height: 248px; 150 | left: 8px; 151 | bottom: 48px; 152 | background-color: rgba(0, 0, 0, 0.25); 153 | } 154 | 155 | .map-viewer .key-display .stat, .map-viewer .key-display .key { 156 | position: absolute; 157 | text-align: center; 158 | background-color: rgba(0, 0, 0, 0.5); 159 | color: #666; 160 | } 161 | 162 | .map-viewer .key-display .stat { 163 | top: 8px; 164 | width: 136px; 165 | height: 40px; 166 | line-height: 40px; 167 | font-size: 10pt; 168 | } 169 | 170 | .map-viewer .key-display .sync-outer { 171 | left: 8px; 172 | } 173 | 174 | .map-viewer .key-display .speed-outer { 175 | right: 8px; 176 | } 177 | 178 | .map-viewer .key-display .stat .value { 179 | color: #fff; 180 | font-size: 12pt; 181 | } 182 | 183 | .map-viewer .key-display .key { 184 | width: 64px; 185 | height: 56px; 186 | line-height: 56px; 187 | font-weight: bold; 188 | } 189 | 190 | .map-viewer .key-display .pressed { 191 | background-color: rgba(0, 0, 0, 0.875); 192 | color: #fff; 193 | text-shadow: 0px 0px 4px rgba(255, 255, 255, 0.5); 194 | } 195 | 196 | .map-viewer .key-display .key-w { 197 | left: 152px; 198 | bottom: 136px; 199 | } 200 | 201 | .map-viewer .key-display .key-a { 202 | left: 80px; 203 | bottom: 72px; 204 | } 205 | 206 | .map-viewer .key-display .key-s { 207 | left: 152px; 208 | bottom: 72px; 209 | } 210 | 211 | .map-viewer .key-display .key-d { 212 | left: 224px; 213 | bottom: 72px; 214 | } 215 | 216 | .map-viewer .key-display .key-walk { 217 | left: 8px; 218 | bottom: 136px; 219 | } 220 | 221 | .map-viewer .key-display .key-duck { 222 | left: 8px; 223 | bottom: 72px; 224 | } 225 | 226 | .map-viewer .key-display .key-jump { 227 | left: 8px; 228 | width: 280px; 229 | bottom: 8px; 230 | } 231 | 232 | .map-viewer .options-menu { 233 | position: absolute; 234 | width: 256px; 235 | right: 8px; 236 | bottom: 48px; 237 | background-color: rgba(0, 0, 0, 0.25); 238 | line-height: 24px; 239 | } 240 | 241 | .map-viewer .options-title { 242 | margin: 8px; 243 | text-align: center; 244 | color: #999; 245 | } 246 | 247 | .map-viewer .options-list { 248 | margin: 8px; 249 | } 250 | 251 | .map-viewer .options-list .option { 252 | user-select: none; 253 | padding: 4px 4px 4px 8px; 254 | background-color: rgba(0, 0, 0, 0.25); 255 | color: #ccc; 256 | cursor: pointer; 257 | } 258 | 259 | .map-viewer .options-list .option:hover { 260 | background-color: rgba(0, 0, 0, 0.75); 261 | } 262 | 263 | .map-viewer .options-list .option .value { 264 | float: right; 265 | text-align: center; 266 | width: 80px; 267 | color: #fff; 268 | } 269 | 270 | .map-viewer .options-list .option .toggle { 271 | position: relative; 272 | float: right; 273 | margin: 2px; 274 | width: 48px; 275 | height: 20px; 276 | border-radius: 10px; 277 | background-color: #333; 278 | } 279 | 280 | .map-viewer .options-list .option .toggle .knob { 281 | position: absolute; 282 | top: 4px; 283 | left: 4px; 284 | width: 16px; 285 | height: 12px; 286 | border-radius: 6px; 287 | background-color: #666; 288 | transition: all 0.06667s ease-in-out; 289 | } 290 | 291 | .map-viewer .options-list .option .toggle.on .knob { 292 | left: 28px; 293 | background-color: #ccc; 294 | } 295 | -------------------------------------------------------------------------------- /src/KeyDisplay.ts: -------------------------------------------------------------------------------- 1 | namespace Gokz { 2 | export class KeyDisplay { 3 | private readonly viewer: ReplayViewer; 4 | 5 | private readonly element: HTMLElement; 6 | private readonly buttonMap: {[button: number]: HTMLElement} = {}; 7 | 8 | private readonly syncValueElem: HTMLElement; 9 | private readonly speedValueElem: HTMLElement; 10 | 11 | syncSampleRange = 4; 12 | speedSampleRange = 1 / 8; 13 | 14 | constructor(viewer: ReplayViewer, container?: HTMLElement) { 15 | this.viewer = viewer; 16 | 17 | if (container === undefined) container = viewer.container; 18 | 19 | const element = this.element = document.createElement("div"); 20 | element.classList.add("key-display"); 21 | element.innerHTML = ` 22 |
Sync: 0.0 %
23 |
Speed: 000 u/s
24 |
W
25 |
A
26 |
S
27 |
D
28 |
Walk
29 |
Duck
30 |
Jump
`; 31 | 32 | container.appendChild(element); 33 | 34 | this.buttonMap[Button.Forward] = element.getElementsByClassName("key-w")[0] as HTMLElement; 35 | this.buttonMap[Button.MoveLeft] = element.getElementsByClassName("key-a")[0] as HTMLElement; 36 | this.buttonMap[Button.Back] = element.getElementsByClassName("key-s")[0] as HTMLElement; 37 | this.buttonMap[Button.MoveRight] = element.getElementsByClassName("key-d")[0] as HTMLElement; 38 | this.buttonMap[Button.Walk] = element.getElementsByClassName("key-walk")[0] as HTMLElement; 39 | this.buttonMap[Button.Duck] = element.getElementsByClassName("key-duck")[0] as HTMLElement; 40 | this.buttonMap[Button.Jump] = element.getElementsByClassName("key-jump")[0] as HTMLElement; 41 | 42 | this.syncValueElem = element.getElementsByClassName("sync-value")[0] as HTMLElement; 43 | this.speedValueElem = element.getElementsByClassName("speed-value")[0] as HTMLElement; 44 | 45 | viewer.showKeyDisplayChanged.addListener(showKeyDisplay => { 46 | if (showKeyDisplay && viewer.cameraMode === SourceUtils.CameraMode.Fixed) this.show(); 47 | else this.hide(); 48 | }); 49 | 50 | viewer.cameraModeChanged.addListener(cameraMode => { 51 | if (viewer.showKeyDisplay && cameraMode === SourceUtils.CameraMode.Fixed) this.show(); 52 | else this.hide(); 53 | }); 54 | 55 | viewer.playbackSkipped.addListener(oldTick => { 56 | this.syncIndex = 0; 57 | this.syncSampleCount = 0; 58 | 59 | this.lastTick = viewer.replay.clampTick(viewer.playbackRate > 0 60 | ? viewer.tick - 32 61 | : viewer.tick + 32); 62 | }); 63 | 64 | viewer.tickChanged.addListener(tickData => { 65 | this.updateButtons(tickData); 66 | this.updateSpeed(); 67 | this.updateSync(); 68 | }); 69 | } 70 | 71 | private updateButtons(tickData: TickData): void { 72 | for (let key in this.buttonMap) { 73 | const pressed = (tickData.buttons & (parseInt(key) as Button)) !== 0; 74 | 75 | if (pressed) { 76 | this.buttonMap[key].classList.add("pressed"); 77 | } else { 78 | this.buttonMap[key].classList.remove("pressed"); 79 | } 80 | } 81 | } 82 | 83 | private readonly tempTickData = new TickData(); 84 | private readonly tempPosition = new Facepunch.Vector3(); 85 | 86 | private syncBuffer: boolean[] = []; 87 | private syncIndex = 0; 88 | private syncSampleCount = 0; 89 | 90 | private lastTick = 0; 91 | 92 | private updateSync(): void { 93 | if (this.lastTick === this.viewer.tick) return; 94 | 95 | const replay = this.viewer.replay; 96 | const maxSamples = Math.ceil(this.syncSampleRange * replay.tickRate); 97 | let syncBuffer = this.syncBuffer; 98 | 99 | if (syncBuffer.length < maxSamples) { 100 | syncBuffer = this.syncBuffer = new Array(maxSamples); 101 | this.syncIndex = 0; 102 | this.syncSampleCount = 0; 103 | } 104 | 105 | const min = replay.clampTick(Math.min(this.lastTick, this.viewer.tick) - 1); 106 | const max = replay.clampTick(Math.max(this.lastTick, this.viewer.tick)); 107 | 108 | let prevSpeed = this.getSpeedAtTick(min, 1); 109 | for (let i = min + 1; i <= max; ++i) { 110 | const nextSpeed = this.getSpeedAtTick(i, 1); 111 | 112 | // A bit gross 113 | if ((this.tempTickData.flags & (EntityFlag.OnGround | EntityFlag.PartialGround)) === 0) { 114 | syncBuffer[this.syncIndex] = nextSpeed > prevSpeed; 115 | this.syncIndex = this.syncIndex >= maxSamples - 1 ? 0 : this.syncIndex + 1; 116 | this.syncSampleCount = Math.min(this.syncSampleCount + 1, maxSamples); 117 | } 118 | 119 | prevSpeed = nextSpeed; 120 | } 121 | 122 | this.lastTick = this.viewer.tick; 123 | 124 | let syncFraction = 0.0; 125 | for (let i = 0; i < this.syncSampleCount; ++i) { 126 | if (syncBuffer[i]) ++syncFraction; 127 | } 128 | 129 | syncFraction /= Math.max(this.syncSampleCount, 1); 130 | this.syncValueElem.innerText = (syncFraction * 100).toFixed(1); 131 | } 132 | 133 | private getSpeedAtTick(tick: number, tickRange: number): number { 134 | const replay = this.viewer.replay; 135 | const firstTick = replay.clampTick(tick - Math.ceil(tickRange / 2)); 136 | const lastTick = replay.clampTick(firstTick + tickRange); 137 | tickRange = lastTick - firstTick; 138 | 139 | const tickData = this.tempTickData; 140 | const position = this.tempPosition; 141 | 142 | replay.getTickData(lastTick, tickData); 143 | position.copy(tickData.position); 144 | 145 | replay.getTickData(firstTick, tickData); 146 | position.sub(tickData.position); 147 | 148 | // Ignore vertical speed 149 | position.z = 0; 150 | 151 | return position.length() * replay.tickRate / Math.max(1, lastTick - firstTick); 152 | } 153 | 154 | private updateSpeed(): void { 155 | // TODO: cache 156 | 157 | const replay = this.viewer.replay; 158 | const maxTickRange = Math.ceil(this.speedSampleRange * replay.tickRate); 159 | 160 | let speedString = Math.round(this.getSpeedAtTick(this.viewer.tick, maxTickRange)).toString(); 161 | 162 | for (; speedString.length < 3; speedString = "0" + speedString); 163 | 164 | this.speedValueElem.innerText = speedString; 165 | } 166 | 167 | show(): void { 168 | this.element.style.display = "block"; 169 | } 170 | 171 | hide(): void { 172 | this.element.style.display = "none"; 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /src/ReplayControls.ts: -------------------------------------------------------------------------------- 1 | namespace Gokz { 2 | export class ReplayControls { 3 | private static readonly speedSliderValues = [-5, -1, 0.1, 0.25, 1, 2, 5, 10]; 4 | 5 | private readonly viewer: ReplayViewer; 6 | private readonly container: HTMLElement; 7 | 8 | readonly playbackBarElem: HTMLElement; 9 | readonly timeElem: HTMLElement; 10 | readonly speedElem: HTMLElement; 11 | readonly pauseElem: HTMLElement; 12 | readonly resumeElem: HTMLElement; 13 | readonly settingsElem: HTMLElement; 14 | readonly fullscreenElem: HTMLElement; 15 | readonly scrubberElem: HTMLInputElement; 16 | 17 | readonly speedControlElem: HTMLElement; 18 | readonly speedSliderElem: HTMLInputElement; 19 | 20 | private playbackBarVisible = true; 21 | private mouseOverPlaybackBar = false; 22 | private speedControlVisible = false; 23 | private lastActionTime: number; 24 | 25 | autoHidePeriod = 2; 26 | 27 | constructor(viewer: ReplayViewer) { 28 | this.viewer = viewer; 29 | this.container = viewer.container; 30 | 31 | const playbackBar = this.playbackBarElem = document.createElement("div"); 32 | playbackBar.classList.add("playback-bar"); 33 | playbackBar.innerHTML = ` 34 |
35 | 36 |
`; 37 | 38 | playbackBar.addEventListener("mouseover", ev => { 39 | this.mouseOverPlaybackBar = true; 40 | }); 41 | 42 | playbackBar.addEventListener("mouseout", ev => { 43 | this.mouseOverPlaybackBar = false; 44 | }); 45 | 46 | this.container.appendChild(playbackBar); 47 | 48 | this.scrubberElem = playbackBar.getElementsByClassName("scrubber")[0] as HTMLInputElement; 49 | this.scrubberElem.addEventListener("input", ev => { 50 | viewer.tick = this.scrubberElem.valueAsNumber; 51 | }); 52 | 53 | this.scrubberElem.addEventListener("mousedown", ev => { 54 | this.viewer.isScrubbing = true; 55 | }); 56 | 57 | this.scrubberElem.addEventListener("mouseup", ev => { 58 | this.viewer.updateTickHash(); 59 | this.viewer.isScrubbing = false; 60 | }); 61 | 62 | this.timeElem = document.createElement("div"); 63 | this.timeElem.classList.add("time"); 64 | playbackBar.appendChild(this.timeElem); 65 | 66 | this.speedElem = document.createElement("div"); 67 | this.speedElem.classList.add("speed"); 68 | this.speedElem.addEventListener("click", ev => { 69 | if (this.speedControlVisible) this.hideSpeedControl(); 70 | else this.showSpeedControl(); 71 | }); 72 | playbackBar.appendChild(this.speedElem); 73 | 74 | this.pauseElem = document.createElement("div"); 75 | this.pauseElem.classList.add("pause"); 76 | this.pauseElem.classList.add("control"); 77 | this.pauseElem.addEventListener("click", ev => this.viewer.isPlaying = false); 78 | playbackBar.appendChild(this.pauseElem); 79 | 80 | this.resumeElem = document.createElement("div"); 81 | this.resumeElem.classList.add("play"); 82 | this.resumeElem.classList.add("control"); 83 | this.resumeElem.addEventListener("click", ev => this.viewer.isPlaying = true); 84 | playbackBar.appendChild(this.resumeElem); 85 | 86 | this.settingsElem = document.createElement("div"); 87 | this.settingsElem.classList.add("settings"); 88 | this.settingsElem.classList.add("control"); 89 | this.settingsElem.addEventListener("click", ev => viewer.showOptions = !viewer.showOptions); 90 | playbackBar.appendChild(this.settingsElem); 91 | 92 | this.fullscreenElem = document.createElement("div"); 93 | this.fullscreenElem.classList.add("fullscreen"); 94 | this.fullscreenElem.classList.add("control"); 95 | this.fullscreenElem.addEventListener("click", ev => this.viewer.toggleFullscreen()); 96 | playbackBar.appendChild(this.fullscreenElem); 97 | 98 | this.speedControlElem = document.createElement("div"); 99 | this.speedControlElem.classList.add("speed-control"); 100 | this.speedControlElem.innerHTML = ``; 101 | this.container.appendChild(this.speedControlElem); 102 | 103 | this.speedSliderElem = this.speedControlElem.getElementsByClassName("speed-slider")[0] as HTMLInputElement; 104 | this.speedSliderElem.addEventListener("input", ev => { 105 | this.viewer.playbackRate = ReplayControls.speedSliderValues[this.speedSliderElem.valueAsNumber]; 106 | }); 107 | 108 | viewer.replayLoaded.addListener(replay => { 109 | this.scrubberElem.max = replay.tickCount.toString(); 110 | }); 111 | 112 | viewer.isPlayingChanged.addListener(isPlaying => { 113 | this.pauseElem.style.display = isPlaying ? "block" : "none"; 114 | this.resumeElem.style.display = isPlaying ? "none" : "block"; 115 | 116 | this.showPlaybackBar(); 117 | }); 118 | 119 | viewer.playbackRateChanged.addListener(playbackRate => { 120 | this.speedElem.innerText = playbackRate.toString(); 121 | this.speedSliderElem.valueAsNumber = ReplayControls.speedSliderValues.indexOf(playbackRate); 122 | }); 123 | 124 | viewer.tickChanged.addListener(tickData => { 125 | const replay = this.viewer.replay; 126 | if (replay != null) { 127 | const totalSeconds = replay.clampTick(tickData.tick) / replay.tickRate; 128 | const minutes = Math.floor(totalSeconds / 60); 129 | const seconds = totalSeconds - minutes * 60; 130 | const secondsString = seconds.toFixed(1); 131 | this.timeElem.innerText = `${minutes}:${secondsString.indexOf(".") === 1 ? "0" : ""}${secondsString}`; 132 | } 133 | 134 | this.scrubberElem.valueAsNumber = tickData.tick; 135 | }); 136 | 137 | viewer.updated.addListener(dt => { 138 | if ((viewer.isPlaying && !this.mouseOverPlaybackBar) || viewer.isPointerLocked()) { 139 | const sinceLastAction = (performance.now() - this.lastActionTime) / 1000; 140 | const hidePeriod = viewer.isPointerLocked() ? 0 : this.autoHidePeriod; 141 | if (sinceLastAction >= hidePeriod) { 142 | this.hidePlaybackBar(); 143 | } 144 | } 145 | }); 146 | 147 | viewer.container.addEventListener("mousemove", ev => { 148 | if (!viewer.isPointerLocked()) { 149 | this.showPlaybackBar(); 150 | } 151 | }); 152 | } 153 | 154 | showPlaybackBar(): void { 155 | if (this.playbackBarVisible) { 156 | this.lastActionTime = performance.now(); 157 | return; 158 | } 159 | 160 | this.playbackBarVisible = true; 161 | this.playbackBarElem.classList.remove("hidden"); 162 | } 163 | 164 | hidePlaybackBar(): void { 165 | if (!this.playbackBarVisible) return; 166 | this.playbackBarVisible = false; 167 | this.playbackBarElem.classList.add("hidden"); 168 | this.lastActionTime = undefined; 169 | 170 | this.hideSpeedControl(); 171 | } 172 | 173 | showSpeedControl(): boolean { 174 | if (this.speedControlVisible) return false; 175 | 176 | this.speedControlVisible = true; 177 | this.speedControlElem.style.display = "block"; 178 | 179 | this.viewer.showOptions = false; 180 | 181 | return true; 182 | } 183 | 184 | hideSpeedControl(): boolean { 185 | if (!this.speedControlVisible) return false; 186 | 187 | this.speedControlVisible = false; 188 | this.speedControlElem.style.display = "none"; 189 | 190 | return true; 191 | } 192 | 193 | showSettings(): void { 194 | // TODO 195 | this.viewer.showDebugPanel = !this.viewer.showDebugPanel; 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /js/replayviewer.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | declare namespace Gokz { 4 | enum SeekOrigin { 5 | Begin = 0, 6 | Current = 1, 7 | End = 2, 8 | } 9 | class BinaryReader { 10 | private readonly buffer; 11 | private readonly view; 12 | private offset; 13 | constructor(buffer: ArrayBuffer); 14 | seek(offset: number, origin: SeekOrigin): number; 15 | getOffset(): number; 16 | readUint8(): number; 17 | readInt32(): number; 18 | readUint32(): number; 19 | readFloat32(): number; 20 | static utf8ArrayToStr(array: number[]): string; 21 | readString(length?: number): string; 22 | readVector2(vec?: Facepunch.Vector2): Facepunch.Vector2; 23 | readVector3(vec?: Facepunch.Vector3): Facepunch.Vector3; 24 | } 25 | } 26 | declare namespace Gokz { 27 | type Handler = (args: TEventArgs, sender: TSender) => void; 28 | class Event { 29 | private readonly sender; 30 | private handlers; 31 | constructor(sender: TSender); 32 | addListener(handler: Handler): void; 33 | removeListener(handler: Handler): boolean; 34 | clearListeners(): void; 35 | dispatch(args: TEventArgs): void; 36 | } 37 | class ChangedEvent extends Event { 38 | private prevValue; 39 | private equalityComparison; 40 | constructor(sender: TSender, equalityComparison?: (a: TValue, b: TValue) => boolean); 41 | reset(): void; 42 | update(value: TValue, args?: TEventArgs): void; 43 | } 44 | } 45 | declare namespace Gokz { 46 | class KeyDisplay { 47 | private readonly viewer; 48 | private readonly element; 49 | private readonly buttonMap; 50 | private readonly syncValueElem; 51 | private readonly speedValueElem; 52 | syncSampleRange: number; 53 | speedSampleRange: number; 54 | constructor(viewer: ReplayViewer, container?: HTMLElement); 55 | private updateButtons(tickData); 56 | private readonly tempTickData; 57 | private readonly tempPosition; 58 | private syncBuffer; 59 | private syncIndex; 60 | private syncSampleCount; 61 | private lastTick; 62 | private updateSync(); 63 | private getSpeedAtTick(tick, tickRange); 64 | private updateSpeed(); 65 | show(): void; 66 | hide(): void; 67 | } 68 | } 69 | declare namespace Gokz { 70 | class OptionsMenu { 71 | private readonly viewer; 72 | readonly element: HTMLElement; 73 | readonly titleElem: HTMLSpanElement; 74 | readonly optionContainer: HTMLElement; 75 | constructor(viewer: ReplayViewer, container?: HTMLElement); 76 | show(): void; 77 | hide(): void; 78 | private clear(); 79 | private showMainPage(); 80 | private setTitle(title); 81 | private addToggleOption(label, getter, setter, changed?); 82 | } 83 | } 84 | declare namespace Gokz { 85 | class ReplayControls { 86 | private static readonly speedSliderValues; 87 | private readonly viewer; 88 | private readonly container; 89 | readonly playbackBarElem: HTMLElement; 90 | readonly timeElem: HTMLElement; 91 | readonly speedElem: HTMLElement; 92 | readonly pauseElem: HTMLElement; 93 | readonly resumeElem: HTMLElement; 94 | readonly settingsElem: HTMLElement; 95 | readonly fullscreenElem: HTMLElement; 96 | readonly scrubberElem: HTMLInputElement; 97 | readonly speedControlElem: HTMLElement; 98 | readonly speedSliderElem: HTMLInputElement; 99 | private playbackBarVisible; 100 | private mouseOverPlaybackBar; 101 | private speedControlVisible; 102 | private lastActionTime; 103 | autoHidePeriod: number; 104 | constructor(viewer: ReplayViewer); 105 | showPlaybackBar(): void; 106 | hidePlaybackBar(): void; 107 | showSpeedControl(): boolean; 108 | hideSpeedControl(): boolean; 109 | showSettings(): void; 110 | } 111 | } 112 | declare namespace Gokz { 113 | enum GlobalMode { 114 | Vanilla = 0, 115 | KzSimple = 1, 116 | KzTimer = 2, 117 | } 118 | enum GlobalStyle { 119 | Normal = 0, 120 | } 121 | enum Button { 122 | Attack = 1, 123 | Jump = 2, 124 | Duck = 4, 125 | Forward = 8, 126 | Back = 16, 127 | Use = 32, 128 | Cancel = 64, 129 | Left = 128, 130 | Right = 256, 131 | MoveLeft = 512, 132 | MoveRight = 1024, 133 | Attack2 = 2048, 134 | Run = 4096, 135 | Reload = 8192, 136 | Alt1 = 16384, 137 | Alt2 = 32768, 138 | Score = 65536, 139 | Speed = 131072, 140 | Walk = 262144, 141 | Zoom = 524288, 142 | Weapon1 = 1048576, 143 | Weapon2 = 2097152, 144 | BullRush = 4194304, 145 | Grenade1 = 8388608, 146 | Grenade2 = 16777216, 147 | } 148 | enum EntityFlag { 149 | OnGround = 1, 150 | Ducking = 2, 151 | WaterJump = 4, 152 | OnTrain = 8, 153 | InRain = 16, 154 | Frozen = 32, 155 | AtControls = 64, 156 | Client = 128, 157 | FakeClient = 256, 158 | InWater = 512, 159 | Fly = 1024, 160 | Swim = 2048, 161 | Conveyor = 4096, 162 | Npc = 8192, 163 | GodMode = 16384, 164 | NoTarget = 32768, 165 | AimTarget = 65536, 166 | PartialGround = 131072, 167 | StaticProp = 262144, 168 | Graphed = 524288, 169 | Grenade = 1048576, 170 | StepMovement = 2097152, 171 | DontTouch = 4194304, 172 | BaseVelocity = 8388608, 173 | WorldBrush = 16777216, 174 | Object = 33554432, 175 | KillMe = 67108864, 176 | OnFire = 134217728, 177 | Dissolving = 268435456, 178 | TransRagdoll = 536870912, 179 | UnblockableByPlayer = 1073741824, 180 | Freezing = -2147483648, 181 | } 182 | class TickData { 183 | readonly position: Facepunch.Vector3; 184 | readonly angles: Facepunch.Vector2; 185 | tick: number; 186 | buttons: Button; 187 | flags: EntityFlag; 188 | getEyeHeight(): number; 189 | } 190 | class ReplayFile { 191 | static readonly MAGIC: number; 192 | private readonly reader; 193 | private readonly firstTickOffset; 194 | private readonly tickSize; 195 | readonly formatVersion: number; 196 | readonly pluginVersion: string; 197 | readonly mapName: string; 198 | readonly course: number; 199 | readonly mode: GlobalMode; 200 | readonly style: GlobalStyle; 201 | readonly time: number; 202 | readonly teleportsUsed: number; 203 | readonly steamId: number; 204 | readonly steamId2: string; 205 | readonly playerName: string; 206 | readonly tickCount: number; 207 | readonly tickRate: number; 208 | constructor(data: ArrayBuffer); 209 | getTickData(tick: number, data?: TickData): TickData; 210 | clampTick(tick: number): number; 211 | } 212 | } 213 | import WebGame = Facepunch.WebGame; 214 | declare namespace Gokz { 215 | /** 216 | * Address hash format for the ReplayViewer. 217 | */ 218 | interface IHashData { 219 | /** Tick number, starting from 1 for the first tick. */ 220 | t?: number; 221 | } 222 | /** 223 | * Creates a GOKZ replay viewer applet. 224 | */ 225 | class ReplayViewer extends SourceUtils.MapViewer { 226 | /** 227 | * Handles a key input display overlay that also shows some stats like 228 | * speed and sync. 229 | */ 230 | readonly keyDisplay: KeyDisplay; 231 | /** 232 | * Handles replay controls such as a playback bar. 233 | */ 234 | readonly controls: ReplayControls; 235 | /** 236 | * Handles options menu. 237 | */ 238 | readonly options: OptionsMenu; 239 | /** 240 | * The URL to look for exported maps at. The directory at the URL 241 | * should contain sub-folders for each map, inside each of which is the 242 | * index.json for that map. 243 | * @example `viewer.mapBaseUrl = "http://my-website.com/maps";` 244 | */ 245 | mapBaseUrl: string; 246 | /** 247 | * The currently loaded replay. Will be automatically set after a 248 | * replay is loaded with `loadReplay(url)`. You can also set this 249 | * manually to switch between replays. 250 | */ 251 | replay: ReplayFile; 252 | /** 253 | * If true, the current tick will be stored in the address hash when 254 | * playback is paused or the viewer uses the playback bar to skip 255 | * around. 256 | * @default `true` 257 | */ 258 | saveTickInHash: boolean; 259 | /** 260 | * The current tick being shown during playback, starting with 0 for 261 | * the first tick. Will automatically be increased while playing, 262 | * although some ticks might be skipped depending on playback speed and 263 | * frame rate. Can be set to skip to a particular tick. 264 | */ 265 | tick: number; 266 | /** 267 | * Current playback rate, measured in seconds per second. Can support 268 | * negative values for rewinding. 269 | * @default `1.0` 270 | */ 271 | playbackRate: number; 272 | /** 273 | * If true, the replay will automatically loop back to the first tick 274 | * when it reaches the end. 275 | * @default `true` 276 | */ 277 | autoRepeat: boolean; 278 | /** 279 | * Used internally to temporarily pause playback while the user is 280 | * dragging the scrubber in the playback bar. 281 | */ 282 | isScrubbing: boolean; 283 | /** 284 | * If true, the currently displayed tick will advance based on the 285 | * value of `playbackRate`. 286 | * @default `false` 287 | */ 288 | isPlaying: boolean; 289 | /** 290 | * If true, a crosshair graphic will be displayed in the middle of the 291 | * viewer. 292 | * @default `true` 293 | */ 294 | showCrosshair: boolean; 295 | /** 296 | * If true, makes the key press display visible. 297 | * @default `true` 298 | */ 299 | showKeyDisplay: boolean; 300 | /** 301 | * If true, makes the options menu visible. 302 | * @default `false` 303 | */ 304 | showOptions: boolean; 305 | /** 306 | * Event invoked when a new replay is loaded. Will be invoked before 307 | * the map for the replay is loaded (if required). 308 | * 309 | * **Available event arguments**: 310 | * * `replay: Gokz.ReplayFile` - The newly loaded ReplayFile 311 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 312 | */ 313 | readonly replayLoaded: Event; 314 | /** 315 | * Event invoked after each update. 316 | * 317 | * **Available event arguments**: 318 | * * `dt: number` - Time since the last update 319 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 320 | */ 321 | readonly updated: Event; 322 | /** 323 | * Event invoked when the current tick has changed. 324 | * 325 | * **Available event arguments**: 326 | * * `tickData: Gokz.TickData` - Recorded data for the current tick 327 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 328 | */ 329 | readonly tickChanged: ChangedEvent; 330 | /** 331 | * Event invoked when playback has skipped to a different tick, for 332 | * example when the user uses the scrubber. 333 | * 334 | * **Available event arguments**: 335 | * * `oldTick: number` - The previous value of `tick` before skipping 336 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 337 | */ 338 | readonly playbackSkipped: Event; 339 | /** 340 | * Event invoked when `playbackRate` changes. 341 | * 342 | * **Available event arguments**: 343 | * * `playbackRate: number` - The new playback rate 344 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 345 | */ 346 | readonly playbackRateChanged: ChangedEvent; 347 | /** 348 | * Event invoked when `isPlaying` changes, for example when the user 349 | * pauses or resumes playback. 350 | * 351 | * **Available event arguments**: 352 | * * `isPlaying: boolean` - True if currently playing 353 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 354 | */ 355 | readonly isPlayingChanged: ChangedEvent; 356 | /** 357 | * Event invoked when `showCrosshair` changes. 358 | * 359 | * **Available event arguments**: 360 | * * `showCrosshair: boolean` - True if crosshair is now visible 361 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 362 | */ 363 | readonly showCrosshairChanged: ChangedEvent; 364 | /** 365 | * Event invoked when `showKeyDisplay` changes. 366 | * 367 | * **Available event arguments**: 368 | * * `showKeyDisplay: boolean` - True if keyDisplay is now visible 369 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 370 | */ 371 | readonly showKeyDisplayChanged: ChangedEvent; 372 | /** 373 | * Event invoked when `showOptions` changes. 374 | * 375 | * **Available event arguments**: 376 | * * `showOptions: boolean` - True if options menu is now visible 377 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 378 | */ 379 | readonly showOptionsChanged: ChangedEvent; 380 | /** 381 | * Event invoked when `cameraMode` changes. 382 | * 383 | * **Available event arguments**: 384 | * * `cameraMode: SourceUtils.CameraMode` - Camera mode value 385 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 386 | */ 387 | readonly cameraModeChanged: ChangedEvent; 388 | private messageElem; 389 | private lastReplay; 390 | private currentMapName; 391 | private pauseTime; 392 | private pauseTicks; 393 | private wakeLock; 394 | private spareTime; 395 | private prevTick; 396 | private tickData; 397 | private tempTickData0; 398 | private tempTickData1; 399 | private tempTickData2; 400 | private routeLine; 401 | /** 402 | * Creates a new ReplayViewer inside the given `container` element. 403 | * @param container Element that should contain the viewer. 404 | */ 405 | constructor(container: HTMLElement); 406 | /** 407 | * Used to display an error message in the middle of the viewer. 408 | * @param message Message to display 409 | */ 410 | showMessage(message: string): void; 411 | /** 412 | * Attempt to load a GOKZ replay from the given URL. When loaded, the 413 | * replay will be stored in the `replay` property in this viewer. 414 | * @param url Url of the replay to download. 415 | */ 416 | loadReplay(url: string): void; 417 | /** 418 | * If `saveTickInHash` is true, will set the address hash to include 419 | * the current tick number. 420 | */ 421 | updateTickHash(): void; 422 | protected onCreateMessagePanel(): HTMLElement; 423 | protected onInitialize(): void; 424 | protected onHashChange(hash: string | Object): void; 425 | private ignoreMouseUp; 426 | protected onMouseDown(button: WebGame.MouseButton, screenPos: Facepunch.Vector2, target: EventTarget): boolean; 427 | protected onMouseUp(button: WebGame.MouseButton, screenPos: Facepunch.Vector2, target: EventTarget): boolean; 428 | protected onKeyDown(key: WebGame.Key): boolean; 429 | protected onChangeReplay(replay: ReplayFile): void; 430 | protected onUpdateFrame(dt: number): void; 431 | } 432 | } 433 | declare namespace Gokz { 434 | class RouteLine extends SourceUtils.Entities.PvsEntity { 435 | private static readonly segmentTicks; 436 | private readonly segments; 437 | private isVisible; 438 | visible: boolean; 439 | constructor(map: SourceUtils.Map, replay: ReplayFile); 440 | protected onPopulateDrawList(drawList: WebGame.DrawList, clusters: number[]): void; 441 | dispose(): void; 442 | } 443 | } 444 | declare namespace Gokz { 445 | class Utils { 446 | static deltaAngle(a: number, b: number): number; 447 | static hermiteValue(p0: number, p1: number, p2: number, p3: number, t: number): number; 448 | static hermitePosition(p0: Facepunch.Vector3, p1: Facepunch.Vector3, p2: Facepunch.Vector3, p3: Facepunch.Vector3, t: number, out: Facepunch.Vector3): void; 449 | static hermiteAngles(a0: Facepunch.Vector2, a1: Facepunch.Vector2, a2: Facepunch.Vector2, a3: Facepunch.Vector2, t: number, out: Facepunch.Vector2): void; 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /src/ReplayViewer.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import WebGame = Facepunch.WebGame; 5 | 6 | namespace Gokz { 7 | /** 8 | * Address hash format for the ReplayViewer. 9 | */ 10 | export interface IHashData { 11 | /** Tick number, starting from 1 for the first tick. */ 12 | t?: number; 13 | } 14 | 15 | /** 16 | * Creates a GOKZ replay viewer applet. 17 | */ 18 | export class ReplayViewer extends SourceUtils.MapViewer { 19 | /** 20 | * Handles a key input display overlay that also shows some stats like 21 | * speed and sync. 22 | */ 23 | readonly keyDisplay: KeyDisplay; 24 | 25 | /** 26 | * Handles replay controls such as a playback bar. 27 | */ 28 | readonly controls: ReplayControls; 29 | 30 | /** 31 | * Handles options menu. 32 | */ 33 | readonly options: OptionsMenu; 34 | 35 | /** 36 | * The URL to look for exported maps at. The directory at the URL 37 | * should contain sub-folders for each map, inside each of which is the 38 | * index.json for that map. 39 | * @example `viewer.mapBaseUrl = "http://my-website.com/maps";` 40 | */ 41 | mapBaseUrl: string; 42 | 43 | /** 44 | * The currently loaded replay. Will be automatically set after a 45 | * replay is loaded with `loadReplay(url)`. You can also set this 46 | * manually to switch between replays. 47 | */ 48 | replay: ReplayFile; 49 | 50 | /** 51 | * If true, the current tick will be stored in the address hash when 52 | * playback is paused or the viewer uses the playback bar to skip 53 | * around. 54 | * @default `true` 55 | */ 56 | saveTickInHash = true; 57 | 58 | /** 59 | * The current tick being shown during playback, starting with 0 for 60 | * the first tick. Will automatically be increased while playing, 61 | * although some ticks might be skipped depending on playback speed and 62 | * frame rate. Can be set to skip to a particular tick. 63 | */ 64 | tick = -1; 65 | 66 | /** 67 | * Current playback rate, measured in seconds per second. Can support 68 | * negative values for rewinding. 69 | * @default `1.0` 70 | */ 71 | playbackRate = 1.0; 72 | 73 | /** 74 | * If true, the replay will automatically loop back to the first tick 75 | * when it reaches the end. 76 | * @default `true` 77 | */ 78 | autoRepeat = true; 79 | 80 | /** 81 | * Used internally to temporarily pause playback while the user is 82 | * dragging the scrubber in the playback bar. 83 | */ 84 | isScrubbing = false; 85 | 86 | /** 87 | * If true, the currently displayed tick will advance based on the 88 | * value of `playbackRate`. 89 | * @default `false` 90 | */ 91 | isPlaying = false; 92 | 93 | /** 94 | * If true, a crosshair graphic will be displayed in the middle of the 95 | * viewer. 96 | * @default `true` 97 | */ 98 | showCrosshair = true; 99 | 100 | /** 101 | * If true, makes the key press display visible. 102 | * @default `true` 103 | */ 104 | showKeyDisplay = true; 105 | 106 | /** 107 | * If true, makes the options menu visible. 108 | * @default `false` 109 | */ 110 | showOptions = false; 111 | 112 | /** 113 | * Event invoked when a new replay is loaded. Will be invoked before 114 | * the map for the replay is loaded (if required). 115 | * 116 | * **Available event arguments**: 117 | * * `replay: Gokz.ReplayFile` - The newly loaded ReplayFile 118 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 119 | */ 120 | readonly replayLoaded = new Event(this); 121 | 122 | /** 123 | * Event invoked after each update. 124 | * 125 | * **Available event arguments**: 126 | * * `dt: number` - Time since the last update 127 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 128 | */ 129 | readonly updated = new Event(this); 130 | 131 | /** 132 | * Event invoked when the current tick has changed. 133 | * 134 | * **Available event arguments**: 135 | * * `tickData: Gokz.TickData` - Recorded data for the current tick 136 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 137 | */ 138 | readonly tickChanged = new ChangedEvent(this); 139 | 140 | /** 141 | * Event invoked when playback has skipped to a different tick, for 142 | * example when the user uses the scrubber. 143 | * 144 | * **Available event arguments**: 145 | * * `oldTick: number` - The previous value of `tick` before skipping 146 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 147 | */ 148 | readonly playbackSkipped = new Event(this); 149 | 150 | /** 151 | * Event invoked when `playbackRate` changes. 152 | * 153 | * **Available event arguments**: 154 | * * `playbackRate: number` - The new playback rate 155 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 156 | */ 157 | readonly playbackRateChanged = new ChangedEvent(this); 158 | 159 | /** 160 | * Event invoked when `isPlaying` changes, for example when the user 161 | * pauses or resumes playback. 162 | * 163 | * **Available event arguments**: 164 | * * `isPlaying: boolean` - True if currently playing 165 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 166 | */ 167 | readonly isPlayingChanged = new ChangedEvent(this); 168 | 169 | /** 170 | * Event invoked when `showCrosshair` changes. 171 | * 172 | * **Available event arguments**: 173 | * * `showCrosshair: boolean` - True if crosshair is now visible 174 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 175 | */ 176 | readonly showCrosshairChanged = new ChangedEvent(this); 177 | 178 | /** 179 | * Event invoked when `showKeyDisplay` changes. 180 | * 181 | * **Available event arguments**: 182 | * * `showKeyDisplay: boolean` - True if keyDisplay is now visible 183 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 184 | */ 185 | readonly showKeyDisplayChanged = new ChangedEvent(this); 186 | 187 | /** 188 | * Event invoked when `showOptions` changes. 189 | * 190 | * **Available event arguments**: 191 | * * `showOptions: boolean` - True if options menu is now visible 192 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 193 | */ 194 | readonly showOptionsChanged = new ChangedEvent(this); 195 | 196 | /** 197 | * Event invoked when `cameraMode` changes. 198 | * 199 | * **Available event arguments**: 200 | * * `cameraMode: SourceUtils.CameraMode` - Camera mode value 201 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 202 | */ 203 | readonly cameraModeChanged = new ChangedEvent(this); 204 | 205 | private messageElem: HTMLElement; 206 | 207 | private lastReplay: ReplayFile; 208 | private currentMapName: string; 209 | 210 | private pauseTime = 1.0; 211 | private pauseTicks: number; 212 | 213 | private wakeLock: any; 214 | 215 | private spareTime = 0; 216 | private prevTick: number = undefined; 217 | 218 | private tickData = new TickData(); 219 | 220 | private tempTickData0 = new TickData(); 221 | private tempTickData1 = new TickData(); 222 | private tempTickData2 = new TickData(); 223 | 224 | private routeLine: RouteLine; 225 | 226 | /** 227 | * Creates a new ReplayViewer inside the given `container` element. 228 | * @param container Element that should contain the viewer. 229 | */ 230 | constructor(container: HTMLElement) { 231 | super(container); 232 | 233 | this.saveCameraPosInHash = false; 234 | 235 | this.controls = new ReplayControls(this); 236 | this.keyDisplay = new KeyDisplay(this, this.controls.playbackBarElem); 237 | this.options = new OptionsMenu(this, this.controls.playbackBarElem); 238 | 239 | const crosshair = document.createElement("div"); 240 | crosshair.classList.add("crosshair"); 241 | container.appendChild(crosshair); 242 | 243 | this.showCrosshairChanged.addListener(showCrosshair => { 244 | crosshair.hidden = !showCrosshair; 245 | }); 246 | 247 | this.isPlayingChanged.addListener(isPlaying => { 248 | if (!isPlaying && this.saveTickInHash) this.updateTickHash(); 249 | 250 | if (isPlaying) { 251 | this.wakeLock = (navigator as any).wakeLock; 252 | if (this.wakeLock != null) { 253 | this.wakeLock.request("display"); 254 | } 255 | 256 | this.cameraMode = SourceUtils.CameraMode.Fixed; 257 | } else if (this.wakeLock != null) { 258 | this.wakeLock.release("display"); 259 | this.wakeLock = null; 260 | } 261 | }); 262 | 263 | this.cameraModeChanged.addListener(mode => { 264 | if (mode === SourceUtils.CameraMode.FreeCam) { 265 | this.isPlaying = false; 266 | } 267 | 268 | if (this.routeLine != null) { 269 | this.routeLine.visible = mode === SourceUtils.CameraMode.FreeCam; 270 | } 271 | 272 | this.canLockPointer = mode === SourceUtils.CameraMode.FreeCam; 273 | if (!this.canLockPointer && this.isPointerLocked()) { 274 | document.exitPointerLock(); 275 | } 276 | }); 277 | } 278 | 279 | /** 280 | * Used to display an error message in the middle of the viewer. 281 | * @param message Message to display 282 | */ 283 | showMessage(message: string): void { 284 | if (this.messageElem === undefined) { 285 | this.messageElem = this.onCreateMessagePanel(); 286 | } 287 | 288 | if (this.messageElem == null) return; 289 | 290 | this.messageElem.innerText = message; 291 | } 292 | 293 | /** 294 | * Attempt to load a GOKZ replay from the given URL. When loaded, the 295 | * replay will be stored in the `replay` property in this viewer. 296 | * @param url Url of the replay to download. 297 | */ 298 | loadReplay(url: string): void { 299 | console.log(`Downloading: ${url}`); 300 | 301 | const req = new XMLHttpRequest(); 302 | req.open("GET", url, true); 303 | req.responseType = "arraybuffer"; 304 | req.onload = ev => { 305 | if (req.status !== 200) { 306 | this.showMessage(`Unable to download replay: ${req.statusText}`); 307 | return; 308 | } 309 | 310 | const arrayBuffer = req.response; 311 | if (arrayBuffer) { 312 | 313 | if (this.routeLine != null) { 314 | this.routeLine.dispose(); 315 | this.routeLine = null; 316 | } 317 | 318 | try { 319 | this.replay = new ReplayFile(arrayBuffer); 320 | } catch (e) { 321 | this.showMessage(`Unable to read replay: ${e}`); 322 | } 323 | } 324 | }; 325 | req.send(null); 326 | } 327 | 328 | /** 329 | * If `saveTickInHash` is true, will set the address hash to include 330 | * the current tick number. 331 | */ 332 | updateTickHash(): void { 333 | if (this.replay == null || !this.saveTickInHash) return; 334 | this.setHash({ t: this.replay.clampTick(this.tick) + 1 }); 335 | } 336 | 337 | protected onCreateMessagePanel(): HTMLElement { 338 | const elem = document.createElement("div"); 339 | elem.classList.add("message"); 340 | 341 | this.container.appendChild(elem); 342 | 343 | return elem; 344 | } 345 | 346 | protected onInitialize(): void { 347 | super.onInitialize(); 348 | 349 | this.canLockPointer = false; 350 | this.cameraMode = SourceUtils.CameraMode.Fixed; 351 | } 352 | 353 | protected onHashChange(hash: string | Object): void { 354 | if (typeof hash === "string") return; 355 | if (!this.saveTickInHash) return; 356 | 357 | const data = hash as IHashData; 358 | 359 | if (data.t !== undefined && this.tick !== data.t) { 360 | this.tick = data.t - 1; 361 | this.isPlaying = false; 362 | } 363 | } 364 | 365 | private ignoreMouseUp = true; 366 | 367 | protected onMouseDown(button: WebGame.MouseButton, screenPos: Facepunch.Vector2, target: EventTarget): boolean { 368 | this.ignoreMouseUp = event.target !== this.canvas; 369 | if (super.onMouseDown(button, screenPos, target)) { 370 | this.showOptions = false; 371 | return true; 372 | } 373 | 374 | return false; 375 | } 376 | 377 | protected onMouseUp(button: WebGame.MouseButton, screenPos: Facepunch.Vector2, target: EventTarget): boolean { 378 | const ignored = this.ignoreMouseUp || event.target !== this.canvas; 379 | this.ignoreMouseUp = true; 380 | 381 | if (ignored) return false; 382 | 383 | if (this.controls.hideSpeedControl() || this.showOptions) { 384 | this.showOptions = false; 385 | return true; 386 | } 387 | 388 | if (super.onMouseUp(button, screenPos, target)) return true; 389 | 390 | if (button === WebGame.MouseButton.Left && this.replay != null && this.map.isReady()) { 391 | this.isPlaying = !this.isPlaying; 392 | return true; 393 | } 394 | 395 | return false; 396 | } 397 | 398 | protected onKeyDown(key: WebGame.Key): boolean { 399 | switch (key) { 400 | case WebGame.Key.X: 401 | this.cameraMode = this.cameraMode === SourceUtils.CameraMode.FreeCam 402 | ? SourceUtils.CameraMode.Fixed : SourceUtils.CameraMode.FreeCam; 403 | 404 | if (this.cameraMode === SourceUtils.CameraMode.FreeCam) { 405 | this.container.requestPointerLock(); 406 | } 407 | return true; 408 | case WebGame.Key.F: 409 | this.toggleFullscreen(); 410 | return true; 411 | case WebGame.Key.Space: 412 | if (this.replay != null && this.map.isReady()) { 413 | this.isPlaying = !this.isPlaying; 414 | } 415 | return true; 416 | } 417 | 418 | return super.onKeyDown(key); 419 | } 420 | 421 | protected onChangeReplay(replay: ReplayFile): void { 422 | this.pauseTicks = Math.round(replay.tickRate * this.pauseTime); 423 | this.tick = this.tick === -1 ? 0 : this.tick; 424 | this.spareTime = 0; 425 | this.prevTick = undefined; 426 | 427 | this.replayLoaded.dispatch(this.replay); 428 | 429 | if (this.currentMapName !== replay.mapName) { 430 | if (this.currentMapName != null) { 431 | this.map.unload(); 432 | } 433 | 434 | if (this.mapBaseUrl == null) { 435 | throw "Cannot load a map when mapBaseUrl is unspecified."; 436 | } 437 | 438 | const version = new Date().getTime().toString(16); 439 | 440 | this.currentMapName = replay.mapName; 441 | this.loadMap(`${this.mapBaseUrl}/${replay.mapName}/index.json?v=${version}`); 442 | } 443 | } 444 | 445 | protected onUpdateFrame(dt: number): void { 446 | super.onUpdateFrame(dt); 447 | 448 | if (this.replay != this.lastReplay) { 449 | this.lastReplay = this.replay; 450 | 451 | if (this.replay != null) { 452 | this.onChangeReplay(this.replay); 453 | } 454 | } 455 | 456 | this.showCrosshairChanged.update(this.showCrosshair); 457 | this.showKeyDisplayChanged.update(this.showKeyDisplay); 458 | this.showOptionsChanged.update(this.showOptions); 459 | this.playbackRateChanged.update(this.playbackRate); 460 | this.cameraModeChanged.update(this.cameraMode); 461 | 462 | if (this.replay == null) { 463 | this.updated.dispatch(dt); 464 | return; 465 | } 466 | 467 | const replay = this.replay; 468 | const tickPeriod = 1.0 / replay.tickRate; 469 | 470 | this.isPlayingChanged.update(this.isPlaying); 471 | 472 | if (this.prevTick !== undefined && this.tick !== this.prevTick) { 473 | this.playbackSkipped.dispatch(this.prevTick); 474 | } 475 | 476 | if (this.routeLine == null && this.map.isReady()) { 477 | this.routeLine = new RouteLine(this.map, this.replay); 478 | } 479 | 480 | if (this.map.isReady() && this.isPlaying && !this.isScrubbing) { 481 | this.spareTime += dt * this.playbackRate; 482 | 483 | const oldTick = this.tick; 484 | 485 | // Forward playback 486 | while (this.spareTime > tickPeriod) { 487 | this.spareTime -= tickPeriod; 488 | this.tick += 1; 489 | 490 | if (this.tick > replay.tickCount + this.pauseTicks * 2) { 491 | this.tick = -this.pauseTicks; 492 | } 493 | } 494 | 495 | // Rewinding 496 | while (this.spareTime < 0) { 497 | this.spareTime += tickPeriod; 498 | this.tick -= 1; 499 | 500 | if (this.tick < -this.pauseTicks * 2) { 501 | this.tick = replay.tickCount + this.pauseTicks; 502 | } 503 | } 504 | } else { 505 | this.spareTime = 0; 506 | } 507 | 508 | this.prevTick = this.tick; 509 | 510 | replay.getTickData(replay.clampTick(this.tick), this.tickData); 511 | let eyeHeight = this.tickData.getEyeHeight(); 512 | 513 | this.tickChanged.update(this.tick, this.tickData); 514 | 515 | if (this.spareTime >= 0 && this.spareTime <= tickPeriod) { 516 | const t = this.spareTime / tickPeriod; 517 | 518 | const d0 = replay.getTickData(replay.clampTick(this.tick - 1), this.tempTickData0); 519 | const d1 = this.tickData; 520 | const d2 = replay.getTickData(replay.clampTick(this.tick + 1), this.tempTickData1); 521 | const d3 = replay.getTickData(replay.clampTick(this.tick + 2), this.tempTickData2); 522 | 523 | Utils.hermitePosition(d0.position, d1.position, 524 | d2.position, d3.position, t, this.tickData.position); 525 | Utils.hermiteAngles(d0.angles, d1.angles, 526 | d2.angles, d3.angles, t, this.tickData.angles); 527 | 528 | eyeHeight = Utils.hermiteValue( 529 | d0.getEyeHeight(), d1.getEyeHeight(), 530 | d2.getEyeHeight(), d3.getEyeHeight(), t); 531 | } 532 | 533 | if (this.cameraMode === SourceUtils.CameraMode.Fixed) { 534 | this.mainCamera.setPosition( 535 | this.tickData.position.x, 536 | this.tickData.position.y, 537 | this.tickData.position.z + eyeHeight); 538 | 539 | this.setCameraAngles( 540 | (this.tickData.angles.y - 90) * Math.PI / 180, 541 | -this.tickData.angles.x * Math.PI / 180); 542 | } 543 | 544 | this.updated.dispatch(dt); 545 | } 546 | } 547 | } 548 | -------------------------------------------------------------------------------- /js/sourceutils.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare namespace SourceUtils { 3 | interface IPageRequest { 4 | index: number; 5 | callback: (payload: TValue, page: TPage) => void; 6 | } 7 | abstract class ResourcePage { 8 | readonly first: number; 9 | readonly count: number; 10 | readonly url: string; 11 | private readonly values; 12 | private toLoad; 13 | protected page: TPayload; 14 | constructor(info: IPageInfo); 15 | getLoadPriority(): number; 16 | getValue(index: number): TValue; 17 | protected abstract onGetValue(index: number): TValue; 18 | load(index: number, callback: (payload: TValue, page: ResourcePage) => void): TValue; 19 | onLoadValues(page: TPayload): void; 20 | } 21 | abstract class PagedLoader> implements Facepunch.ILoader { 22 | private pages; 23 | private readonly toLoad; 24 | private active; 25 | private loadProgress; 26 | protected abstract onCreatePage(page: IPageInfo): TPage; 27 | throwIfNotFound: boolean; 28 | getLoadProgress(): number; 29 | load(index: number, callback: (payload: TValue, page: TPage) => void): TValue; 30 | setPageLayout(pages: IPageInfo[]): void; 31 | private getNextToLoad(); 32 | update(requestQuota: number): number; 33 | } 34 | } 35 | declare namespace SourceUtils { 36 | interface IAmbientPage { 37 | values: IAmbientSample[][]; 38 | } 39 | interface IAmbientSample { 40 | position: Facepunch.IVector3; 41 | samples: number[]; 42 | } 43 | class AmbientPage extends ResourcePage { 44 | protected onGetValue(index: number): IAmbientSample[]; 45 | } 46 | class AmbientLoader extends PagedLoader { 47 | protected onCreatePage(page: IPageInfo): AmbientPage; 48 | } 49 | } 50 | declare namespace SourceUtils { 51 | import WebGame = Facepunch.WebGame; 52 | interface IPlane { 53 | norm: Facepunch.IVector3; 54 | dist: number; 55 | } 56 | interface IBspElement { 57 | min: Facepunch.IVector3; 58 | max: Facepunch.IVector3; 59 | } 60 | interface IBspNode extends IBspElement { 61 | plane: IPlane; 62 | children: IBspElement[]; 63 | } 64 | enum LeafFlags { 65 | Sky = 1, 66 | Radial = 2, 67 | Sky2D = 4, 68 | } 69 | interface IBspLeaf extends IBspElement { 70 | index: number; 71 | flags: LeafFlags; 72 | hasFaces: boolean; 73 | cluster?: number; 74 | } 75 | interface IBspModel { 76 | index: number; 77 | min: Facepunch.IVector3; 78 | max: Facepunch.IVector3; 79 | origin: Facepunch.IVector3; 80 | headNode: IBspNode; 81 | } 82 | class Plane { 83 | norm: Facepunch.Vector3; 84 | dist: number; 85 | copy(plane: IPlane): this; 86 | } 87 | interface INodeOrLeaf { 88 | readonly isLeaf: boolean; 89 | } 90 | class BspNode implements INodeOrLeaf { 91 | private readonly viewer; 92 | readonly isLeaf: boolean; 93 | readonly plane: Plane; 94 | readonly children: (BspLeaf | BspNode)[]; 95 | constructor(viewer: MapViewer, info: IBspNode); 96 | private loadChild(value); 97 | findLeaves(target: BspLeaf[]): void; 98 | } 99 | class BspLeaf extends WebGame.DrawListItem implements INodeOrLeaf { 100 | readonly isLeaf: boolean; 101 | private readonly viewer; 102 | readonly index: number; 103 | readonly flags: LeafFlags; 104 | readonly cluster: number; 105 | readonly hasFaces: boolean; 106 | private hasLoaded; 107 | private ambientSamples; 108 | private hasLoadedAmbient; 109 | constructor(viewer: MapViewer, info: IBspLeaf); 110 | private static readonly getAmbientCube_temp; 111 | getAmbientCube(pos: Facepunch.IVector3, outSamples: Facepunch.IVector3[], callback?: (success: boolean) => void): boolean; 112 | getMeshHandles(): Facepunch.WebGame.MeshHandle[]; 113 | findLeaves(target: BspLeaf[]): void; 114 | } 115 | class BspModel extends WebGame.RenderResource { 116 | readonly viewer: MapViewer; 117 | private info; 118 | private headNode; 119 | private leaves; 120 | constructor(viewer: MapViewer); 121 | loadFromInfo(info: IBspModel): void; 122 | getLeafAt(pos: Facepunch.IVector3): BspLeaf; 123 | getLeaves(): BspLeaf[]; 124 | isLoaded(): boolean; 125 | } 126 | interface IBspModelPage { 127 | models: IBspModel[]; 128 | } 129 | class BspModelPage extends ResourcePage { 130 | private readonly viewer; 131 | private models; 132 | constructor(viewer: MapViewer, page: IPageInfo); 133 | onLoadValues(page: IBspModelPage): void; 134 | protected onGetValue(index: number): IBspModel; 135 | } 136 | class BspModelLoader extends PagedLoader { 137 | readonly viewer: MapViewer; 138 | private readonly models; 139 | constructor(viewer: MapViewer); 140 | loadModel(index: number): BspModel; 141 | onCreatePage(page: IPageInfo): BspModelPage; 142 | } 143 | } 144 | declare namespace SourceUtils { 145 | import WebGame = Facepunch.WebGame; 146 | interface IDispGeometryPage { 147 | displacements: IFace[]; 148 | materials: IMaterialGroup[]; 149 | } 150 | class DispGeometryPage extends ResourcePage { 151 | private readonly viewer; 152 | private matGroups; 153 | private dispFaces; 154 | constructor(viewer: MapViewer, page: IPageInfo); 155 | onLoadValues(page: IDispGeometryPage): void; 156 | protected onGetValue(index: number): Facepunch.WebGame.MeshHandle; 157 | } 158 | class DispGeometryLoader extends PagedLoader { 159 | readonly viewer: MapViewer; 160 | constructor(viewer: MapViewer); 161 | protected onCreatePage(page: IPageInfo): DispGeometryPage; 162 | } 163 | } 164 | declare namespace SourceUtils { 165 | import WebGame = Facepunch.WebGame; 166 | interface IFace { 167 | material: number; 168 | element: number; 169 | } 170 | interface IMaterialGroup { 171 | material: number; 172 | meshData: WebGame.ICompressedMeshData; 173 | } 174 | interface ILeafGeometryPage { 175 | leaves: IFace[][]; 176 | materials: IMaterialGroup[]; 177 | } 178 | class LeafGeometryPage extends ResourcePage { 179 | private readonly viewer; 180 | private matGroups; 181 | private leafFaces; 182 | constructor(viewer: MapViewer, page: IPageInfo); 183 | onLoadValues(page: ILeafGeometryPage): void; 184 | protected onGetValue(index: number): Facepunch.WebGame.MeshHandle[]; 185 | } 186 | class LeafGeometryLoader extends PagedLoader { 187 | readonly viewer: MapViewer; 188 | constructor(viewer: MapViewer); 189 | protected onCreatePage(page: IPageInfo): LeafGeometryPage; 190 | } 191 | } 192 | declare namespace SourceUtils { 193 | import WebGame = Facepunch.WebGame; 194 | interface IPageInfo { 195 | first: number; 196 | count: number; 197 | url: string; 198 | } 199 | interface IMap { 200 | name: string; 201 | lightmapUrl: string; 202 | visPages: IPageInfo[]; 203 | leafPages: IPageInfo[]; 204 | dispPages: IPageInfo[]; 205 | materialPages: IPageInfo[]; 206 | brushModelPages: IPageInfo[]; 207 | studioModelPages: IPageInfo[]; 208 | vertLightingPages: IPageInfo[]; 209 | ambientPages: IPageInfo[]; 210 | entities: Entities.IEntity[]; 211 | } 212 | class Map implements WebGame.ICommandBufferParameterProvider { 213 | static readonly lightmapParam: WebGame.CommandBufferParameter; 214 | readonly viewer: MapViewer; 215 | skyCamera: Entities.SkyCamera; 216 | private tSpawns; 217 | private ctSpawns; 218 | private playerSpawns; 219 | private namedEntities; 220 | private worldspawn; 221 | private pvsEntities; 222 | private lightmap; 223 | private skyCube; 224 | private info; 225 | private clusterVis; 226 | private clusterEnts; 227 | private worldspawnLoadedCallbacks; 228 | constructor(viewer: MapViewer); 229 | isReady(): boolean; 230 | unload(): void; 231 | load(url: string): void; 232 | getLightmapLoadProgress(): number; 233 | private onLoad(info); 234 | addNamedEntity(targetname: string, entity: Entities.Entity): void; 235 | getNamedEntity(targetname: string): Entities.Entity; 236 | addPvsEntity(entity: Entities.PvsEntity): void; 237 | removePvsEntity(entity: Entities.PvsEntity): void; 238 | getPvsEntitiesInCluster(cluster: number): Entities.PvsEntity[]; 239 | getLeafAt(pos: Facepunch.IVector3, callback?: (leaf: BspLeaf) => void): BspLeaf; 240 | update(dt: number): void; 241 | populateDrawList(drawList: WebGame.DrawList, pvsRoot: BspLeaf): void; 242 | populateCommandBufferParameters(buf: Facepunch.WebGame.CommandBuffer): void; 243 | } 244 | } 245 | declare namespace SourceUtils { 246 | import WebGame = Facepunch.WebGame; 247 | interface IMapMaterialPage { 248 | textures: WebGame.ITextureInfo[]; 249 | materials: WebGame.IMaterialInfo[]; 250 | } 251 | class MapMaterialPage extends ResourcePage { 252 | private readonly viewer; 253 | private materials; 254 | constructor(viewer: MapViewer, page: IPageInfo); 255 | onLoadValues(page: IMapMaterialPage): void; 256 | protected onGetValue(index: number): WebGame.IMaterialInfo; 257 | } 258 | class MapMaterialLoader extends PagedLoader { 259 | readonly viewer: MapViewer; 260 | private readonly materials; 261 | constructor(viewer: MapViewer); 262 | loadMaterial(index: number): WebGame.Material; 263 | protected onCreatePage(page: IPageInfo): MapMaterialPage; 264 | } 265 | } 266 | declare namespace SourceUtils { 267 | import WebGame = Facepunch.WebGame; 268 | enum CameraMode { 269 | Fixed = 0, 270 | CanLook = 1, 271 | CanMove = 2, 272 | FreeCam = 3, 273 | } 274 | interface IPositionHash { 275 | x?: number; 276 | y?: number; 277 | z?: number; 278 | r?: number; 279 | s?: number; 280 | } 281 | class MapViewer extends WebGame.Game { 282 | mainCamera: Entities.Camera; 283 | debugPanel: HTMLElement; 284 | readonly map: Map; 285 | readonly visLoader: VisLoader; 286 | readonly bspModelLoader: BspModelLoader; 287 | readonly mapMaterialLoader: MapMaterialLoader; 288 | readonly leafGeometryLoader: LeafGeometryLoader; 289 | readonly dispGeometryLoader: DispGeometryLoader; 290 | readonly studioModelLoader: StudioModelLoader; 291 | readonly vertLightingLoader: VertexLightingLoader; 292 | readonly ambientLoader: AmbientLoader; 293 | private debugPanelVisible; 294 | cameraMode: CameraMode; 295 | saveCameraPosInHash: boolean; 296 | showDebugPanel: boolean; 297 | totalLoadProgress: number; 298 | avgFrameTime: number; 299 | avgFrameRate: number; 300 | notMovedTime: number; 301 | constructor(container: HTMLElement); 302 | loadMap(url: string): void; 303 | protected onInitialize(): void; 304 | private static readonly hashKeyRegex; 305 | private static readonly hashObjectRegex; 306 | protected setHash(value: string | Object): void; 307 | private oldHash; 308 | private hashChange(); 309 | private readonly onHashChange_temp; 310 | protected onHashChange(value: string | Object): void; 311 | protected onCreateDebugPanel(): HTMLElement; 312 | protected onDeviceRotate(deltaAngles: Facepunch.Vector3): void; 313 | protected onResize(): void; 314 | private readonly lookAngs; 315 | private readonly tempQuat; 316 | private readonly lookQuat; 317 | setCameraAngles(yaw: number, pitch: number): void; 318 | private updateCameraAngles(); 319 | protected onMouseLook(delta: Facepunch.Vector2): void; 320 | toggleFullscreen(): void; 321 | protected onKeyDown(key: WebGame.Key): boolean; 322 | private lastProfileTime; 323 | private frameCount; 324 | private lastDrawCalls; 325 | private allLoaded; 326 | protected onSetDebugText(className: string, value: string): void; 327 | private readonly onUpdateFrame_temp; 328 | protected onUpdateFrame(dt: number): void; 329 | protected onRenderFrame(dt: number): void; 330 | populateCommandBufferParameters(buf: WebGame.CommandBuffer): void; 331 | } 332 | } 333 | declare namespace SourceUtils { 334 | import WebGame = Facepunch.WebGame; 335 | class SkyCube extends WebGame.DrawListItem { 336 | constructor(viewer: MapViewer, material: WebGame.Material); 337 | } 338 | } 339 | declare namespace SourceUtils { 340 | import WebGame = Facepunch.WebGame; 341 | interface ISmdMesh { 342 | meshId: number; 343 | material: number; 344 | element: number; 345 | } 346 | interface ISmdModel { 347 | meshes: ISmdMesh[]; 348 | } 349 | interface ISmdBodyPart { 350 | name: string; 351 | models: ISmdModel[]; 352 | } 353 | interface IStudioModel { 354 | bodyParts: ISmdBodyPart[]; 355 | } 356 | class StudioModel extends WebGame.RenderResource { 357 | readonly viewer: MapViewer; 358 | private info; 359 | private page; 360 | constructor(viewer: MapViewer); 361 | private static getOrCreateMatGroup(matGroups, attribs); 362 | private static encode2CompColor(vertLit, albedoMod); 363 | private static readonly sampleAmbientCube_samples; 364 | private static readonly sampleAmbientCube_temp; 365 | private static sampleAmbientCube(leaf, pos, normal); 366 | createMeshHandles(bodyPartIndex: number, transform: Facepunch.Matrix4, lighting?: (number[][] | BspLeaf), albedoModulation?: number): WebGame.MeshHandle[]; 367 | loadFromInfo(info: IStudioModel, page: StudioModelPage): void; 368 | isLoaded(): boolean; 369 | } 370 | interface IStudioModelPage { 371 | models: IStudioModel[]; 372 | materials: IMaterialGroup[]; 373 | } 374 | class StudioModelPage extends ResourcePage { 375 | private matGroups; 376 | private models; 377 | constructor(page: IPageInfo); 378 | getMaterialGroup(index: number): WebGame.IMeshData; 379 | onLoadValues(page: IStudioModelPage): void; 380 | protected onGetValue(index: number): IStudioModel; 381 | } 382 | class StudioModelLoader extends PagedLoader { 383 | readonly viewer: MapViewer; 384 | private readonly models; 385 | constructor(viewer: MapViewer); 386 | update(requestQuota: number): number; 387 | loadModel(index: number): StudioModel; 388 | onCreatePage(page: IPageInfo): StudioModelPage; 389 | } 390 | interface IVertexLightingPage { 391 | props: (string | number[])[][]; 392 | } 393 | class VertexLightingPage extends ResourcePage { 394 | private props; 395 | onLoadValues(page: IVertexLightingPage): void; 396 | protected onGetValue(index: number): number[][]; 397 | } 398 | class VertexLightingLoader extends PagedLoader { 399 | readonly viewer: MapViewer; 400 | constructor(viewer: MapViewer); 401 | update(requestQuota: number): number; 402 | onCreatePage(page: IPageInfo): VertexLightingPage; 403 | } 404 | } 405 | declare namespace SourceUtils { 406 | class ColorConversion { 407 | private static lastScreenGamma; 408 | private static linearToScreenGammaTable; 409 | private static exponentTable; 410 | static initialize(screenGamma: number): void; 411 | static rgbExp32ToVector3(rgbExp: number, out: Facepunch.IVector3): Facepunch.IVector3; 412 | static linearToScreenGamma(f: number): number; 413 | } 414 | } 415 | declare namespace SourceUtils { 416 | interface IVisPage { 417 | values: (number[] | string)[]; 418 | } 419 | class VisPage extends ResourcePage { 420 | protected onGetValue(index: number): number[]; 421 | } 422 | class VisLoader extends PagedLoader { 423 | constructor(); 424 | protected onCreatePage(page: IPageInfo): VisPage; 425 | } 426 | } 427 | declare namespace SourceUtils { 428 | import WebGame = Facepunch.WebGame; 429 | namespace Entities { 430 | interface IEntity { 431 | classname: string; 432 | targetname?: string; 433 | origin?: Facepunch.IVector3; 434 | angles?: Facepunch.IVector3; 435 | scale?: number; 436 | } 437 | interface IColor { 438 | r: number; 439 | g: number; 440 | b: number; 441 | } 442 | interface IEnvFogController extends IEntity { 443 | fogEnabled: boolean; 444 | fogStart: number; 445 | fogEnd: number; 446 | fogMaxDensity: number; 447 | farZ: number; 448 | fogColor: IColor; 449 | } 450 | class Entity extends WebGame.DrawableEntity { 451 | readonly map: Map; 452 | readonly targetname: string; 453 | constructor(map: Map, info: IEntity); 454 | } 455 | interface IPvsEntity extends IEntity { 456 | clusters: number[]; 457 | } 458 | class PvsEntity extends Entity { 459 | private readonly clusters; 460 | constructor(map: Map, info: IPvsEntity); 461 | isInCluster(cluster: number): boolean; 462 | isInAnyCluster(clusters: number[]): boolean; 463 | populateDrawList(drawList: WebGame.DrawList, clusters: number[]): void; 464 | protected onPopulateDrawList(drawList: WebGame.DrawList, clusters: number[]): void; 465 | } 466 | } 467 | } 468 | declare namespace SourceUtils { 469 | namespace Entities { 470 | interface IBrushEntity extends IPvsEntity { 471 | model: number; 472 | } 473 | class BrushEntity extends PvsEntity { 474 | readonly model: BspModel; 475 | readonly isWorldSpawn: boolean; 476 | constructor(map: Map, info: IBrushEntity); 477 | onAddToDrawList(list: Facepunch.WebGame.DrawList): void; 478 | } 479 | } 480 | } 481 | declare namespace SourceUtils { 482 | import WebGame = Facepunch.WebGame; 483 | namespace Entities { 484 | interface ISkyCamera extends IEnvFogController { 485 | scale: number; 486 | } 487 | class Camera extends WebGame.PerspectiveCamera { 488 | readonly viewer: MapViewer; 489 | private leaf; 490 | private leafInvalid; 491 | render3DSky: boolean; 492 | constructor(viewer: MapViewer, fov: number); 493 | protected onChangePosition(): void; 494 | private static readonly onGetLeaf_temp; 495 | protected onGetLeaf(): BspLeaf; 496 | getLeaf(): BspLeaf; 497 | protected onPopulateDrawList(drawList: Facepunch.WebGame.DrawList): void; 498 | render(): void; 499 | } 500 | class SkyCamera extends Camera { 501 | private readonly origin; 502 | private readonly skyScale; 503 | constructor(viewer: MapViewer, info: ISkyCamera); 504 | protected onChangePosition(): void; 505 | protected onGetLeaf(): BspLeaf; 506 | private static readonly renderRelativeTo_temp; 507 | renderRelativeTo(camera: Camera): void; 508 | } 509 | class ShadowCamera extends WebGame.OrthographicCamera { 510 | readonly viewer: MapViewer; 511 | private readonly targetCamera; 512 | constructor(viewer: MapViewer, targetCamera: Camera); 513 | protected onPopulateDrawList(drawList: Facepunch.WebGame.DrawList): void; 514 | private addToFrustumBounds(invLight, vec, bounds); 515 | private static readonly getFrustumBounds_vec; 516 | private static readonly getFrustumBounds_invLight; 517 | private getFrustumBounds(lightRotation, near, far, bounds); 518 | private static readonly renderShadows_bounds; 519 | renderShadows(lightRotation: Facepunch.Quaternion, near: number, far: number): void; 520 | } 521 | } 522 | } 523 | declare namespace SourceUtils { 524 | namespace Entities { 525 | interface IDisplacement extends IPvsEntity { 526 | index: number; 527 | } 528 | class Displacement extends PvsEntity { 529 | private readonly index; 530 | private isLoaded; 531 | constructor(map: Map, info: IDisplacement); 532 | onAddToDrawList(list: Facepunch.WebGame.DrawList): void; 533 | } 534 | } 535 | } 536 | declare namespace SourceUtils { 537 | namespace Entities { 538 | interface IKeyframeRope extends IPvsEntity { 539 | width: number; 540 | textureScale: number; 541 | subDivisions: number; 542 | slack: number; 543 | ropeMaterial: number; 544 | nextKey: string; 545 | moveSpeed: number; 546 | } 547 | class KeyframeRope extends PvsEntity { 548 | readonly nextKey: string; 549 | readonly width: number; 550 | readonly slack: number; 551 | readonly subDivisions: number; 552 | constructor(map: Map, info: IKeyframeRope); 553 | } 554 | enum PositionInterpolator { 555 | Linear = 0, 556 | CatmullRomSpline = 1, 557 | Rope = 2, 558 | } 559 | interface IMoveRope extends IKeyframeRope { 560 | positionInterp: PositionInterpolator; 561 | } 562 | class MoveRope extends KeyframeRope { 563 | private readonly info; 564 | private keyframes; 565 | private material; 566 | private meshHandles; 567 | constructor(map: Map, info: IMoveRope); 568 | private findKeyframes(); 569 | private generateMesh(); 570 | onAddToDrawList(list: Facepunch.WebGame.DrawList): void; 571 | getMeshHandles(): Facepunch.WebGame.MeshHandle[]; 572 | } 573 | } 574 | } 575 | declare namespace SourceUtils { 576 | namespace Entities { 577 | interface IStaticProp extends IPvsEntity { 578 | model: number; 579 | vertLighting?: number; 580 | albedoModulation?: number; 581 | } 582 | class StaticProp extends PvsEntity { 583 | readonly model: StudioModel; 584 | private readonly info; 585 | private lighting; 586 | private albedoModulation?; 587 | constructor(map: Map, info: IStaticProp); 588 | private checkLoaded(); 589 | } 590 | } 591 | } 592 | declare namespace SourceUtils { 593 | import WebGame = Facepunch.WebGame; 594 | namespace Entities { 595 | interface IWorldspawn extends IBrushEntity { 596 | skyMaterial: WebGame.IMaterialInfo; 597 | } 598 | class Worldspawn extends BrushEntity { 599 | private readonly clusterLeaves; 600 | constructor(map: Map, info: IWorldspawn); 601 | private onModelLoad(); 602 | isInAnyCluster(clusters: number[]): boolean; 603 | isInCluster(cluster: number): boolean; 604 | protected onPopulateDrawList(drawList: Facepunch.WebGame.DrawList, clusters: number[]): void; 605 | } 606 | } 607 | } 608 | declare namespace SourceUtils { 609 | import WebGame = Facepunch.WebGame; 610 | namespace Shaders { 611 | class BaseMaterial { 612 | cullFace: boolean; 613 | } 614 | class BaseShaderProgram extends WebGame.ShaderProgram { 615 | private readonly materialCtor; 616 | constructor(context: WebGLRenderingContext, ctor: { 617 | new (): TMaterial; 618 | }); 619 | createMaterialProperties(): any; 620 | bufferMaterial(buf: WebGame.CommandBuffer, material: WebGame.Material): void; 621 | bufferMaterialProps(buf: WebGame.CommandBuffer, props: TMaterial): void; 622 | } 623 | } 624 | } 625 | declare namespace SourceUtils { 626 | import WebGame = Facepunch.WebGame; 627 | namespace Shaders { 628 | class ModelBaseMaterial extends BaseMaterial { 629 | basetexture: WebGame.Texture; 630 | alphaTest: boolean; 631 | translucent: boolean; 632 | alpha: number; 633 | fogEnabled: boolean; 634 | emission: boolean; 635 | emissionTint: Facepunch.Vector3; 636 | } 637 | abstract class ModelBase extends BaseShaderProgram { 638 | readonly uProjection: WebGame.UniformMatrix4; 639 | readonly uView: WebGame.UniformMatrix4; 640 | readonly uModel: WebGame.UniformMatrix4; 641 | readonly uBaseTexture: WebGame.UniformSampler; 642 | readonly uAlphaTest: WebGame.Uniform1F; 643 | readonly uTranslucent: WebGame.Uniform1F; 644 | readonly uAlpha: WebGame.Uniform1F; 645 | readonly uFogParams: WebGame.Uniform4F; 646 | readonly uFogColor: WebGame.Uniform3F; 647 | readonly uFogEnabled: WebGame.Uniform1I; 648 | readonly uEmission: WebGame.Uniform1I; 649 | readonly uEmissionTint: WebGame.Uniform3F; 650 | constructor(context: WebGLRenderingContext, ctor: { 651 | new (): TMaterial; 652 | }); 653 | bufferSetup(buf: Facepunch.WebGame.CommandBuffer): void; 654 | bufferModelMatrix(buf: Facepunch.WebGame.CommandBuffer, value: Float32Array): void; 655 | bufferMaterialProps(buf: Facepunch.WebGame.CommandBuffer, props: TMaterial): void; 656 | } 657 | } 658 | } 659 | declare namespace SourceUtils { 660 | import WebGame = Facepunch.WebGame; 661 | namespace Shaders { 662 | class LightmappedBaseMaterial extends ModelBaseMaterial { 663 | } 664 | abstract class LightmappedBase extends ModelBase { 665 | readonly uLightmap: WebGame.UniformSampler; 666 | constructor(context: WebGLRenderingContext, ctor: { 667 | new (): TMaterial; 668 | }); 669 | bufferSetup(buf: Facepunch.WebGame.CommandBuffer): void; 670 | } 671 | } 672 | } 673 | declare namespace SourceUtils { 674 | import WebGame = Facepunch.WebGame; 675 | namespace Shaders { 676 | class Lightmapped2WayBlendMaterial extends LightmappedBaseMaterial { 677 | basetexture2: WebGame.Texture; 678 | blendModulateTexture: WebGame.Texture; 679 | } 680 | class Lightmapped2WayBlend extends LightmappedBase { 681 | readonly uBaseTexture2: WebGame.UniformSampler; 682 | readonly uBlendModulateTexture: WebGame.UniformSampler; 683 | readonly uBlendModulate: WebGame.Uniform1I; 684 | constructor(context: WebGLRenderingContext); 685 | bufferMaterialProps(buf: Facepunch.WebGame.CommandBuffer, props: Lightmapped2WayBlendMaterial): void; 686 | } 687 | } 688 | } 689 | declare namespace SourceUtils { 690 | namespace Shaders { 691 | class LightmappedGenericMaterial extends LightmappedBaseMaterial { 692 | } 693 | class LightmappedGeneric extends LightmappedBase { 694 | constructor(context: WebGLRenderingContext); 695 | } 696 | } 697 | } 698 | declare namespace SourceUtils { 699 | import WebGame = Facepunch.WebGame; 700 | namespace Shaders { 701 | class SkyMaterial extends BaseMaterial { 702 | facePosX: WebGame.Texture; 703 | faceNegX: WebGame.Texture; 704 | facePosY: WebGame.Texture; 705 | faceNegY: WebGame.Texture; 706 | facePosZ: WebGame.Texture; 707 | faceNegZ: WebGame.Texture; 708 | hdrCompressed: boolean; 709 | aspect: number; 710 | } 711 | class Sky extends BaseShaderProgram { 712 | readonly uProjection: WebGame.UniformMatrix4; 713 | readonly uView: WebGame.UniformMatrix4; 714 | readonly uFacePosX: WebGame.UniformSampler; 715 | readonly uFaceNegX: WebGame.UniformSampler; 716 | readonly uFacePosY: WebGame.UniformSampler; 717 | readonly uFaceNegY: WebGame.UniformSampler; 718 | readonly uFacePosZ: WebGame.UniformSampler; 719 | readonly uFaceNegZ: WebGame.UniformSampler; 720 | readonly uHdrCompressed: WebGame.Uniform1I; 721 | constructor(context: WebGLRenderingContext); 722 | bufferSetup(buf: Facepunch.WebGame.CommandBuffer): void; 723 | bufferMaterialProps(buf: Facepunch.WebGame.CommandBuffer, props: SkyMaterial): void; 724 | } 725 | } 726 | } 727 | declare namespace SourceUtils { 728 | import WebGame = Facepunch.WebGame; 729 | namespace Shaders { 730 | class SplineRopeMaterial extends ModelBaseMaterial { 731 | ambient: Facepunch.Vector3[]; 732 | } 733 | class SplineRope extends ModelBase { 734 | private uAmbient0; 735 | private uAmbient1; 736 | private uAmbient2; 737 | private uAmbient3; 738 | private uAmbient4; 739 | private uAmbient5; 740 | uAmbient: WebGame.Uniform3F[]; 741 | constructor(context: WebGLRenderingContext); 742 | bufferMaterialProps(buf: Facepunch.WebGame.CommandBuffer, props: SplineRopeMaterial): void; 743 | } 744 | } 745 | } 746 | declare namespace SourceUtils { 747 | namespace Shaders { 748 | class UnlitGenericMaterial extends ModelBaseMaterial { 749 | } 750 | class UnlitGeneric extends ModelBase { 751 | constructor(context: WebGLRenderingContext); 752 | } 753 | } 754 | } 755 | declare namespace SourceUtils { 756 | namespace Shaders { 757 | class VertexLitGenericMaterial extends ModelBaseMaterial { 758 | } 759 | class VertexLitGeneric extends ModelBase { 760 | constructor(context: WebGLRenderingContext); 761 | } 762 | } 763 | } 764 | declare namespace SourceUtils { 765 | import WebGame = Facepunch.WebGame; 766 | namespace Shaders { 767 | class WaterMaterial extends LightmappedBaseMaterial { 768 | fogStart: number; 769 | fogEnd: number; 770 | fogColor: Facepunch.Vector3; 771 | fogLightmapped: boolean; 772 | translucent: boolean; 773 | refract: boolean; 774 | refractTint: Facepunch.Vector3; 775 | normalMap: WebGame.Texture; 776 | cullFace: boolean; 777 | } 778 | class Water extends LightmappedBase { 779 | uCameraPos: WebGame.Uniform3F; 780 | uInverseProjection: WebGame.UniformMatrix4; 781 | uInverseView: WebGame.UniformMatrix4; 782 | uScreenParams: WebGame.Uniform4F; 783 | uOpaqueColor: WebGame.UniformSampler; 784 | uOpaqueDepth: WebGame.UniformSampler; 785 | uWaterFogParams: WebGame.Uniform4F; 786 | uWaterFogColor: WebGame.Uniform3F; 787 | uWaterFogLightmapped: WebGame.Uniform1F; 788 | uNormalMap: WebGame.UniformSampler; 789 | uRefractTint: WebGame.Uniform3F; 790 | constructor(context: WebGLRenderingContext); 791 | bufferSetup(buf: Facepunch.WebGame.CommandBuffer): void; 792 | bufferMaterialProps(buf: Facepunch.WebGame.CommandBuffer, props: WaterMaterial): void; 793 | } 794 | } 795 | } 796 | declare namespace SourceUtils { 797 | import WebGame = Facepunch.WebGame; 798 | namespace Shaders { 799 | class WorldTwoTextureBlendMaterial extends LightmappedBaseMaterial { 800 | detail: WebGame.Texture; 801 | detailScale: number; 802 | } 803 | class WorldTwoTextureBlend extends LightmappedBase { 804 | readonly uDetail: WebGame.UniformSampler; 805 | readonly uDetailScale: WebGame.Uniform1F; 806 | constructor(context: WebGLRenderingContext); 807 | bufferMaterialProps(buf: Facepunch.WebGame.CommandBuffer, props: WorldTwoTextureBlendMaterial): void; 808 | } 809 | } 810 | } 811 | -------------------------------------------------------------------------------- /js/replayviewer.js: -------------------------------------------------------------------------------- 1 | var __extends = (this && this.__extends) || function (d, b) { 2 | for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 3 | function __() { this.constructor = d; } 4 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 5 | }; 6 | var Gokz; 7 | (function (Gokz) { 8 | (function (SeekOrigin) { 9 | SeekOrigin[SeekOrigin["Begin"] = 0] = "Begin"; 10 | SeekOrigin[SeekOrigin["Current"] = 1] = "Current"; 11 | SeekOrigin[SeekOrigin["End"] = 2] = "End"; 12 | })(Gokz.SeekOrigin || (Gokz.SeekOrigin = {})); 13 | var SeekOrigin = Gokz.SeekOrigin; 14 | var BinaryReader = (function () { 15 | function BinaryReader(buffer) { 16 | this.buffer = buffer; 17 | this.view = new DataView(buffer); 18 | this.offset = 0; 19 | } 20 | BinaryReader.prototype.seek = function (offset, origin) { 21 | switch (origin) { 22 | case SeekOrigin.Begin: 23 | return this.offset = offset; 24 | case SeekOrigin.End: 25 | return this.offset = this.buffer.byteLength - offset; 26 | default: 27 | return this.offset = this.offset + offset; 28 | } 29 | }; 30 | BinaryReader.prototype.getOffset = function () { 31 | return this.offset; 32 | }; 33 | BinaryReader.prototype.readUint8 = function () { 34 | var value = this.view.getUint8(this.offset); 35 | this.offset += 1; 36 | return value; 37 | }; 38 | BinaryReader.prototype.readInt32 = function () { 39 | var value = this.view.getInt32(this.offset, true); 40 | this.offset += 4; 41 | return value; 42 | }; 43 | BinaryReader.prototype.readUint32 = function () { 44 | var value = this.view.getUint32(this.offset, true); 45 | this.offset += 4; 46 | return value; 47 | }; 48 | BinaryReader.prototype.readFloat32 = function () { 49 | var value = this.view.getFloat32(this.offset, true); 50 | this.offset += 4; 51 | return value; 52 | }; 53 | // http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt 54 | /* utf.js - UTF-8 <=> UTF-16 convertion 55 | * 56 | * Copyright (C) 1999 Masanao Izumo 57 | * Version: 1.0 58 | * LastModified: Dec 25 1999 59 | * This library is free. You can redistribute it and/or modify it. 60 | */ 61 | BinaryReader.utf8ArrayToStr = function (array) { 62 | var out, i, len, c; 63 | var char2, char3; 64 | out = ""; 65 | len = array.length; 66 | i = 0; 67 | while (i < len) { 68 | c = array[i++]; 69 | switch (c >> 4) { 70 | case 0: 71 | case 1: 72 | case 2: 73 | case 3: 74 | case 4: 75 | case 5: 76 | case 6: 77 | case 7: 78 | // 0xxxxxxx 79 | out += String.fromCharCode(c); 80 | break; 81 | case 12: 82 | case 13: 83 | // 110x xxxx 10xx xxxx 84 | char2 = array[i++]; 85 | out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); 86 | break; 87 | case 14: 88 | // 1110 xxxx 10xx xxxx 10xx xxxx 89 | char2 = array[i++]; 90 | char3 = array[i++]; 91 | out += String.fromCharCode(((c & 0x0F) << 12) | 92 | ((char2 & 0x3F) << 6) | 93 | ((char3 & 0x3F) << 0)); 94 | break; 95 | } 96 | } 97 | return out; 98 | }; 99 | BinaryReader.prototype.readString = function (length) { 100 | if (length === undefined) { 101 | length = this.readUint8(); 102 | } 103 | var chars = new Array(length); 104 | for (var i = 0; i < length; ++i) { 105 | chars[i] = this.readUint8(); 106 | } 107 | return BinaryReader.utf8ArrayToStr(chars); 108 | }; 109 | BinaryReader.prototype.readVector2 = function (vec) { 110 | if (vec === undefined) 111 | vec = new Facepunch.Vector2(); 112 | vec.set(this.readFloat32(), this.readFloat32()); 113 | return vec; 114 | }; 115 | BinaryReader.prototype.readVector3 = function (vec) { 116 | if (vec === undefined) 117 | vec = new Facepunch.Vector3(); 118 | vec.set(this.readFloat32(), this.readFloat32(), this.readFloat32()); 119 | return vec; 120 | }; 121 | return BinaryReader; 122 | }()); 123 | Gokz.BinaryReader = BinaryReader; 124 | })(Gokz || (Gokz = {})); 125 | var Gokz; 126 | (function (Gokz) { 127 | var Event = (function () { 128 | function Event(sender) { 129 | this.handlers = []; 130 | this.sender = sender; 131 | } 132 | Event.prototype.addListener = function (handler) { 133 | this.handlers.push(handler); 134 | }; 135 | Event.prototype.removeListener = function (handler) { 136 | var index = this.handlers.indexOf(handler); 137 | if (index === -1) 138 | return false; 139 | this.handlers.splice(index, 1); 140 | return true; 141 | }; 142 | Event.prototype.clearListeners = function () { 143 | this.handlers = []; 144 | }; 145 | Event.prototype.dispatch = function (args) { 146 | var count = this.handlers.length; 147 | for (var i = 0; i < count; ++i) { 148 | this.handlers[i](args, this.sender); 149 | } 150 | }; 151 | return Event; 152 | }()); 153 | Gokz.Event = Event; 154 | var ChangedEvent = (function (_super) { 155 | __extends(ChangedEvent, _super); 156 | function ChangedEvent(sender, equalityComparison) { 157 | _super.call(this, sender); 158 | if (equalityComparison != null) { 159 | this.equalityComparison = equalityComparison; 160 | } 161 | else { 162 | this.equalityComparison = function (a, b) { return a === b; }; 163 | } 164 | } 165 | ChangedEvent.prototype.reset = function () { 166 | this.prevValue = undefined; 167 | }; 168 | ChangedEvent.prototype.update = function (value, args) { 169 | if (this.equalityComparison(this.prevValue, value)) 170 | return; 171 | this.prevValue = value; 172 | this.dispatch(args === undefined ? value : args); 173 | }; 174 | return ChangedEvent; 175 | }(Event)); 176 | Gokz.ChangedEvent = ChangedEvent; 177 | })(Gokz || (Gokz = {})); 178 | var Gokz; 179 | (function (Gokz) { 180 | var KeyDisplay = (function () { 181 | function KeyDisplay(viewer, container) { 182 | var _this = this; 183 | this.buttonMap = {}; 184 | this.syncSampleRange = 4; 185 | this.speedSampleRange = 1 / 8; 186 | this.tempTickData = new Gokz.TickData(); 187 | this.tempPosition = new Facepunch.Vector3(); 188 | this.syncBuffer = []; 189 | this.syncIndex = 0; 190 | this.syncSampleCount = 0; 191 | this.lastTick = 0; 192 | this.viewer = viewer; 193 | if (container === undefined) 194 | container = viewer.container; 195 | var element = this.element = document.createElement("div"); 196 | element.classList.add("key-display"); 197 | element.innerHTML = "\n
Sync: 0.0 %
\n
Speed: 000 u/s
\n
W
\n
A
\n
S
\n
D
\n
Walk
\n
Duck
\n
Jump
"; 198 | container.appendChild(element); 199 | this.buttonMap[Gokz.Button.Forward] = element.getElementsByClassName("key-w")[0]; 200 | this.buttonMap[Gokz.Button.MoveLeft] = element.getElementsByClassName("key-a")[0]; 201 | this.buttonMap[Gokz.Button.Back] = element.getElementsByClassName("key-s")[0]; 202 | this.buttonMap[Gokz.Button.MoveRight] = element.getElementsByClassName("key-d")[0]; 203 | this.buttonMap[Gokz.Button.Walk] = element.getElementsByClassName("key-walk")[0]; 204 | this.buttonMap[Gokz.Button.Duck] = element.getElementsByClassName("key-duck")[0]; 205 | this.buttonMap[Gokz.Button.Jump] = element.getElementsByClassName("key-jump")[0]; 206 | this.syncValueElem = element.getElementsByClassName("sync-value")[0]; 207 | this.speedValueElem = element.getElementsByClassName("speed-value")[0]; 208 | viewer.showKeyDisplayChanged.addListener(function (showKeyDisplay) { 209 | if (showKeyDisplay && viewer.cameraMode === SourceUtils.CameraMode.Fixed) 210 | _this.show(); 211 | else 212 | _this.hide(); 213 | }); 214 | viewer.cameraModeChanged.addListener(function (cameraMode) { 215 | if (viewer.showKeyDisplay && cameraMode === SourceUtils.CameraMode.Fixed) 216 | _this.show(); 217 | else 218 | _this.hide(); 219 | }); 220 | viewer.playbackSkipped.addListener(function (oldTick) { 221 | _this.syncIndex = 0; 222 | _this.syncSampleCount = 0; 223 | _this.lastTick = viewer.replay.clampTick(viewer.playbackRate > 0 224 | ? viewer.tick - 32 225 | : viewer.tick + 32); 226 | }); 227 | viewer.tickChanged.addListener(function (tickData) { 228 | _this.updateButtons(tickData); 229 | _this.updateSpeed(); 230 | _this.updateSync(); 231 | }); 232 | } 233 | KeyDisplay.prototype.updateButtons = function (tickData) { 234 | for (var key in this.buttonMap) { 235 | var pressed = (tickData.buttons & parseInt(key)) !== 0; 236 | if (pressed) { 237 | this.buttonMap[key].classList.add("pressed"); 238 | } 239 | else { 240 | this.buttonMap[key].classList.remove("pressed"); 241 | } 242 | } 243 | }; 244 | KeyDisplay.prototype.updateSync = function () { 245 | if (this.lastTick === this.viewer.tick) 246 | return; 247 | var replay = this.viewer.replay; 248 | var maxSamples = Math.ceil(this.syncSampleRange * replay.tickRate); 249 | var syncBuffer = this.syncBuffer; 250 | if (syncBuffer.length < maxSamples) { 251 | syncBuffer = this.syncBuffer = new Array(maxSamples); 252 | this.syncIndex = 0; 253 | this.syncSampleCount = 0; 254 | } 255 | var min = replay.clampTick(Math.min(this.lastTick, this.viewer.tick) - 1); 256 | var max = replay.clampTick(Math.max(this.lastTick, this.viewer.tick)); 257 | var prevSpeed = this.getSpeedAtTick(min, 1); 258 | for (var i = min + 1; i <= max; ++i) { 259 | var nextSpeed = this.getSpeedAtTick(i, 1); 260 | // A bit gross 261 | if ((this.tempTickData.flags & (Gokz.EntityFlag.OnGround | Gokz.EntityFlag.PartialGround)) === 0) { 262 | syncBuffer[this.syncIndex] = nextSpeed > prevSpeed; 263 | this.syncIndex = this.syncIndex >= maxSamples - 1 ? 0 : this.syncIndex + 1; 264 | this.syncSampleCount = Math.min(this.syncSampleCount + 1, maxSamples); 265 | } 266 | prevSpeed = nextSpeed; 267 | } 268 | this.lastTick = this.viewer.tick; 269 | var syncFraction = 0.0; 270 | for (var i = 0; i < this.syncSampleCount; ++i) { 271 | if (syncBuffer[i]) 272 | ++syncFraction; 273 | } 274 | syncFraction /= Math.max(this.syncSampleCount, 1); 275 | this.syncValueElem.innerText = (syncFraction * 100).toFixed(1); 276 | }; 277 | KeyDisplay.prototype.getSpeedAtTick = function (tick, tickRange) { 278 | var replay = this.viewer.replay; 279 | var firstTick = replay.clampTick(tick - Math.ceil(tickRange / 2)); 280 | var lastTick = replay.clampTick(firstTick + tickRange); 281 | tickRange = lastTick - firstTick; 282 | var tickData = this.tempTickData; 283 | var position = this.tempPosition; 284 | replay.getTickData(lastTick, tickData); 285 | position.copy(tickData.position); 286 | replay.getTickData(firstTick, tickData); 287 | position.sub(tickData.position); 288 | // Ignore vertical speed 289 | position.z = 0; 290 | return position.length() * replay.tickRate / Math.max(1, lastTick - firstTick); 291 | }; 292 | KeyDisplay.prototype.updateSpeed = function () { 293 | // TODO: cache 294 | var replay = this.viewer.replay; 295 | var maxTickRange = Math.ceil(this.speedSampleRange * replay.tickRate); 296 | var speedString = Math.round(this.getSpeedAtTick(this.viewer.tick, maxTickRange)).toString(); 297 | for (; speedString.length < 3; speedString = "0" + speedString) 298 | ; 299 | this.speedValueElem.innerText = speedString; 300 | }; 301 | KeyDisplay.prototype.show = function () { 302 | this.element.style.display = "block"; 303 | }; 304 | KeyDisplay.prototype.hide = function () { 305 | this.element.style.display = "none"; 306 | }; 307 | return KeyDisplay; 308 | }()); 309 | Gokz.KeyDisplay = KeyDisplay; 310 | })(Gokz || (Gokz = {})); 311 | var Gokz; 312 | (function (Gokz) { 313 | var OptionsMenu = (function () { 314 | function OptionsMenu(viewer, container) { 315 | var _this = this; 316 | this.viewer = viewer; 317 | if (container === undefined) { 318 | container = this.viewer.container; 319 | } 320 | var element = this.element = document.createElement("div"); 321 | element.classList.add("options-menu"); 322 | element.innerHTML = "
"; 323 | container.appendChild(element); 324 | this.titleElem = element.getElementsByClassName("options-title")[0]; 325 | this.optionContainer = element.getElementsByClassName("options-list")[0]; 326 | viewer.showOptionsChanged.addListener(function (showOptions) { 327 | if (showOptions) 328 | _this.show(); 329 | else 330 | _this.hide(); 331 | }); 332 | } 333 | OptionsMenu.prototype.show = function () { 334 | this.element.style.display = "block"; 335 | this.showMainPage(); 336 | if (this.viewer.controls != null) { 337 | this.viewer.controls.hideSpeedControl(); 338 | } 339 | }; 340 | OptionsMenu.prototype.hide = function () { 341 | this.element.style.display = "none"; 342 | this.clear(); 343 | }; 344 | OptionsMenu.prototype.clear = function () { 345 | this.optionContainer.innerHTML = ""; 346 | }; 347 | OptionsMenu.prototype.showMainPage = function () { 348 | var viewer = this.viewer; 349 | this.clear(); 350 | this.setTitle("Options"); 351 | this.addToggleOption("Show Crosshair", function () { return viewer.showCrosshair; }, function (value) { return viewer.showCrosshair = value; }, viewer.showCrosshairChanged); 352 | this.addToggleOption("Show Framerate", function () { return viewer.showDebugPanel; }, function (value) { return viewer.showDebugPanel = value; }); 353 | this.addToggleOption("Show Key Presses", function () { return viewer.showKeyDisplay; }, function (value) { return viewer.showKeyDisplay = value; }, viewer.showKeyDisplayChanged); 354 | this.addToggleOption("Free Camera", function () { return viewer.cameraMode === SourceUtils.CameraMode.FreeCam; }, function (value) { return viewer.cameraMode = value 355 | ? SourceUtils.CameraMode.FreeCam 356 | : SourceUtils.CameraMode.Fixed; }, viewer.cameraModeChanged); 357 | }; 358 | OptionsMenu.prototype.setTitle = function (title) { 359 | this.titleElem.innerText = title; 360 | }; 361 | OptionsMenu.prototype.addToggleOption = function (label, getter, setter, changed) { 362 | var option = document.createElement("div"); 363 | option.classList.add("option"); 364 | option.innerHTML = label + "
"; 365 | this.optionContainer.appendChild(option); 366 | var toggle = option.getElementsByClassName("toggle")[0]; 367 | var updateOption = function () { 368 | if (getter()) { 369 | toggle.classList.add("on"); 370 | } 371 | else { 372 | toggle.classList.remove("on"); 373 | } 374 | }; 375 | option.addEventListener("click", function (ev) { 376 | setter(!getter()); 377 | if (changed == null) { 378 | updateOption(); 379 | } 380 | }); 381 | if (changed != null) { 382 | changed.addListener(function () { return updateOption(); }); 383 | } 384 | updateOption(); 385 | }; 386 | return OptionsMenu; 387 | }()); 388 | Gokz.OptionsMenu = OptionsMenu; 389 | })(Gokz || (Gokz = {})); 390 | var Gokz; 391 | (function (Gokz) { 392 | var ReplayControls = (function () { 393 | function ReplayControls(viewer) { 394 | var _this = this; 395 | this.playbackBarVisible = true; 396 | this.mouseOverPlaybackBar = false; 397 | this.speedControlVisible = false; 398 | this.autoHidePeriod = 2; 399 | this.viewer = viewer; 400 | this.container = viewer.container; 401 | var playbackBar = this.playbackBarElem = document.createElement("div"); 402 | playbackBar.classList.add("playback-bar"); 403 | playbackBar.innerHTML = "\n
\n \n
"; 404 | playbackBar.addEventListener("mouseover", function (ev) { 405 | _this.mouseOverPlaybackBar = true; 406 | }); 407 | playbackBar.addEventListener("mouseout", function (ev) { 408 | _this.mouseOverPlaybackBar = false; 409 | }); 410 | this.container.appendChild(playbackBar); 411 | this.scrubberElem = playbackBar.getElementsByClassName("scrubber")[0]; 412 | this.scrubberElem.addEventListener("input", function (ev) { 413 | viewer.tick = _this.scrubberElem.valueAsNumber; 414 | }); 415 | this.scrubberElem.addEventListener("mousedown", function (ev) { 416 | _this.viewer.isScrubbing = true; 417 | }); 418 | this.scrubberElem.addEventListener("mouseup", function (ev) { 419 | _this.viewer.updateTickHash(); 420 | _this.viewer.isScrubbing = false; 421 | }); 422 | this.timeElem = document.createElement("div"); 423 | this.timeElem.classList.add("time"); 424 | playbackBar.appendChild(this.timeElem); 425 | this.speedElem = document.createElement("div"); 426 | this.speedElem.classList.add("speed"); 427 | this.speedElem.addEventListener("click", function (ev) { 428 | if (_this.speedControlVisible) 429 | _this.hideSpeedControl(); 430 | else 431 | _this.showSpeedControl(); 432 | }); 433 | playbackBar.appendChild(this.speedElem); 434 | this.pauseElem = document.createElement("div"); 435 | this.pauseElem.classList.add("pause"); 436 | this.pauseElem.classList.add("control"); 437 | this.pauseElem.addEventListener("click", function (ev) { return _this.viewer.isPlaying = false; }); 438 | playbackBar.appendChild(this.pauseElem); 439 | this.resumeElem = document.createElement("div"); 440 | this.resumeElem.classList.add("play"); 441 | this.resumeElem.classList.add("control"); 442 | this.resumeElem.addEventListener("click", function (ev) { return _this.viewer.isPlaying = true; }); 443 | playbackBar.appendChild(this.resumeElem); 444 | this.settingsElem = document.createElement("div"); 445 | this.settingsElem.classList.add("settings"); 446 | this.settingsElem.classList.add("control"); 447 | this.settingsElem.addEventListener("click", function (ev) { return viewer.showOptions = !viewer.showOptions; }); 448 | playbackBar.appendChild(this.settingsElem); 449 | this.fullscreenElem = document.createElement("div"); 450 | this.fullscreenElem.classList.add("fullscreen"); 451 | this.fullscreenElem.classList.add("control"); 452 | this.fullscreenElem.addEventListener("click", function (ev) { return _this.viewer.toggleFullscreen(); }); 453 | playbackBar.appendChild(this.fullscreenElem); 454 | this.speedControlElem = document.createElement("div"); 455 | this.speedControlElem.classList.add("speed-control"); 456 | this.speedControlElem.innerHTML = ""; 457 | this.container.appendChild(this.speedControlElem); 458 | this.speedSliderElem = this.speedControlElem.getElementsByClassName("speed-slider")[0]; 459 | this.speedSliderElem.addEventListener("input", function (ev) { 460 | _this.viewer.playbackRate = ReplayControls.speedSliderValues[_this.speedSliderElem.valueAsNumber]; 461 | }); 462 | viewer.replayLoaded.addListener(function (replay) { 463 | _this.scrubberElem.max = replay.tickCount.toString(); 464 | }); 465 | viewer.isPlayingChanged.addListener(function (isPlaying) { 466 | _this.pauseElem.style.display = isPlaying ? "block" : "none"; 467 | _this.resumeElem.style.display = isPlaying ? "none" : "block"; 468 | _this.showPlaybackBar(); 469 | }); 470 | viewer.playbackRateChanged.addListener(function (playbackRate) { 471 | _this.speedElem.innerText = playbackRate.toString(); 472 | _this.speedSliderElem.valueAsNumber = ReplayControls.speedSliderValues.indexOf(playbackRate); 473 | }); 474 | viewer.tickChanged.addListener(function (tickData) { 475 | var replay = _this.viewer.replay; 476 | if (replay != null) { 477 | var totalSeconds = replay.clampTick(tickData.tick) / replay.tickRate; 478 | var minutes = Math.floor(totalSeconds / 60); 479 | var seconds = totalSeconds - minutes * 60; 480 | var secondsString = seconds.toFixed(1); 481 | _this.timeElem.innerText = minutes + ":" + (secondsString.indexOf(".") === 1 ? "0" : "") + secondsString; 482 | } 483 | _this.scrubberElem.valueAsNumber = tickData.tick; 484 | }); 485 | viewer.updated.addListener(function (dt) { 486 | if ((viewer.isPlaying && !_this.mouseOverPlaybackBar) || viewer.isPointerLocked()) { 487 | var sinceLastAction = (performance.now() - _this.lastActionTime) / 1000; 488 | var hidePeriod = viewer.isPointerLocked() ? 0 : _this.autoHidePeriod; 489 | if (sinceLastAction >= hidePeriod) { 490 | _this.hidePlaybackBar(); 491 | } 492 | } 493 | }); 494 | viewer.container.addEventListener("mousemove", function (ev) { 495 | if (!viewer.isPointerLocked()) { 496 | _this.showPlaybackBar(); 497 | } 498 | }); 499 | } 500 | ReplayControls.prototype.showPlaybackBar = function () { 501 | if (this.playbackBarVisible) { 502 | this.lastActionTime = performance.now(); 503 | return; 504 | } 505 | this.playbackBarVisible = true; 506 | this.playbackBarElem.classList.remove("hidden"); 507 | }; 508 | ReplayControls.prototype.hidePlaybackBar = function () { 509 | if (!this.playbackBarVisible) 510 | return; 511 | this.playbackBarVisible = false; 512 | this.playbackBarElem.classList.add("hidden"); 513 | this.lastActionTime = undefined; 514 | this.hideSpeedControl(); 515 | }; 516 | ReplayControls.prototype.showSpeedControl = function () { 517 | if (this.speedControlVisible) 518 | return false; 519 | this.speedControlVisible = true; 520 | this.speedControlElem.style.display = "block"; 521 | this.viewer.showOptions = false; 522 | return true; 523 | }; 524 | ReplayControls.prototype.hideSpeedControl = function () { 525 | if (!this.speedControlVisible) 526 | return false; 527 | this.speedControlVisible = false; 528 | this.speedControlElem.style.display = "none"; 529 | return true; 530 | }; 531 | ReplayControls.prototype.showSettings = function () { 532 | // TODO 533 | this.viewer.showDebugPanel = !this.viewer.showDebugPanel; 534 | }; 535 | ReplayControls.speedSliderValues = [-5, -1, 0.1, 0.25, 1, 2, 5, 10]; 536 | return ReplayControls; 537 | }()); 538 | Gokz.ReplayControls = ReplayControls; 539 | })(Gokz || (Gokz = {})); 540 | var Gokz; 541 | (function (Gokz) { 542 | (function (GlobalMode) { 543 | GlobalMode[GlobalMode["Vanilla"] = 0] = "Vanilla"; 544 | GlobalMode[GlobalMode["KzSimple"] = 1] = "KzSimple"; 545 | GlobalMode[GlobalMode["KzTimer"] = 2] = "KzTimer"; 546 | })(Gokz.GlobalMode || (Gokz.GlobalMode = {})); 547 | var GlobalMode = Gokz.GlobalMode; 548 | (function (GlobalStyle) { 549 | GlobalStyle[GlobalStyle["Normal"] = 0] = "Normal"; 550 | })(Gokz.GlobalStyle || (Gokz.GlobalStyle = {})); 551 | var GlobalStyle = Gokz.GlobalStyle; 552 | (function (Button) { 553 | Button[Button["Attack"] = 1] = "Attack"; 554 | Button[Button["Jump"] = 2] = "Jump"; 555 | Button[Button["Duck"] = 4] = "Duck"; 556 | Button[Button["Forward"] = 8] = "Forward"; 557 | Button[Button["Back"] = 16] = "Back"; 558 | Button[Button["Use"] = 32] = "Use"; 559 | Button[Button["Cancel"] = 64] = "Cancel"; 560 | Button[Button["Left"] = 128] = "Left"; 561 | Button[Button["Right"] = 256] = "Right"; 562 | Button[Button["MoveLeft"] = 512] = "MoveLeft"; 563 | Button[Button["MoveRight"] = 1024] = "MoveRight"; 564 | Button[Button["Attack2"] = 2048] = "Attack2"; 565 | Button[Button["Run"] = 4096] = "Run"; 566 | Button[Button["Reload"] = 8192] = "Reload"; 567 | Button[Button["Alt1"] = 16384] = "Alt1"; 568 | Button[Button["Alt2"] = 32768] = "Alt2"; 569 | Button[Button["Score"] = 65536] = "Score"; 570 | Button[Button["Speed"] = 131072] = "Speed"; 571 | Button[Button["Walk"] = 262144] = "Walk"; 572 | Button[Button["Zoom"] = 524288] = "Zoom"; 573 | Button[Button["Weapon1"] = 1048576] = "Weapon1"; 574 | Button[Button["Weapon2"] = 2097152] = "Weapon2"; 575 | Button[Button["BullRush"] = 4194304] = "BullRush"; 576 | Button[Button["Grenade1"] = 8388608] = "Grenade1"; 577 | Button[Button["Grenade2"] = 16777216] = "Grenade2"; 578 | })(Gokz.Button || (Gokz.Button = {})); 579 | var Button = Gokz.Button; 580 | (function (EntityFlag) { 581 | EntityFlag[EntityFlag["OnGround"] = 1] = "OnGround"; 582 | EntityFlag[EntityFlag["Ducking"] = 2] = "Ducking"; 583 | EntityFlag[EntityFlag["WaterJump"] = 4] = "WaterJump"; 584 | EntityFlag[EntityFlag["OnTrain"] = 8] = "OnTrain"; 585 | EntityFlag[EntityFlag["InRain"] = 16] = "InRain"; 586 | EntityFlag[EntityFlag["Frozen"] = 32] = "Frozen"; 587 | EntityFlag[EntityFlag["AtControls"] = 64] = "AtControls"; 588 | EntityFlag[EntityFlag["Client"] = 128] = "Client"; 589 | EntityFlag[EntityFlag["FakeClient"] = 256] = "FakeClient"; 590 | EntityFlag[EntityFlag["InWater"] = 512] = "InWater"; 591 | EntityFlag[EntityFlag["Fly"] = 1024] = "Fly"; 592 | EntityFlag[EntityFlag["Swim"] = 2048] = "Swim"; 593 | EntityFlag[EntityFlag["Conveyor"] = 4096] = "Conveyor"; 594 | EntityFlag[EntityFlag["Npc"] = 8192] = "Npc"; 595 | EntityFlag[EntityFlag["GodMode"] = 16384] = "GodMode"; 596 | EntityFlag[EntityFlag["NoTarget"] = 32768] = "NoTarget"; 597 | EntityFlag[EntityFlag["AimTarget"] = 65536] = "AimTarget"; 598 | EntityFlag[EntityFlag["PartialGround"] = 131072] = "PartialGround"; 599 | EntityFlag[EntityFlag["StaticProp"] = 262144] = "StaticProp"; 600 | EntityFlag[EntityFlag["Graphed"] = 524288] = "Graphed"; 601 | EntityFlag[EntityFlag["Grenade"] = 1048576] = "Grenade"; 602 | EntityFlag[EntityFlag["StepMovement"] = 2097152] = "StepMovement"; 603 | EntityFlag[EntityFlag["DontTouch"] = 4194304] = "DontTouch"; 604 | EntityFlag[EntityFlag["BaseVelocity"] = 8388608] = "BaseVelocity"; 605 | EntityFlag[EntityFlag["WorldBrush"] = 16777216] = "WorldBrush"; 606 | EntityFlag[EntityFlag["Object"] = 33554432] = "Object"; 607 | EntityFlag[EntityFlag["KillMe"] = 67108864] = "KillMe"; 608 | EntityFlag[EntityFlag["OnFire"] = 134217728] = "OnFire"; 609 | EntityFlag[EntityFlag["Dissolving"] = 268435456] = "Dissolving"; 610 | EntityFlag[EntityFlag["TransRagdoll"] = 536870912] = "TransRagdoll"; 611 | EntityFlag[EntityFlag["UnblockableByPlayer"] = 1073741824] = "UnblockableByPlayer"; 612 | EntityFlag[EntityFlag["Freezing"] = -2147483648] = "Freezing"; 613 | })(Gokz.EntityFlag || (Gokz.EntityFlag = {})); 614 | var EntityFlag = Gokz.EntityFlag; 615 | var TickData = (function () { 616 | function TickData() { 617 | this.position = new Facepunch.Vector3(); 618 | this.angles = new Facepunch.Vector2(); 619 | this.tick = -1; 620 | this.buttons = 0; 621 | this.flags = 0; 622 | } 623 | TickData.prototype.getEyeHeight = function () { 624 | return (this.flags & EntityFlag.Ducking) != 0 ? 46 : 64; 625 | }; 626 | return TickData; 627 | }()); 628 | Gokz.TickData = TickData; 629 | var ReplayFile = (function () { 630 | function ReplayFile(data) { 631 | var reader = this.reader = new Gokz.BinaryReader(data); 632 | var magic = reader.readInt32(); 633 | if (magic !== ReplayFile.MAGIC) { 634 | throw "Unrecognised replay file format."; 635 | } 636 | this.formatVersion = reader.readUint8(); 637 | this.pluginVersion = reader.readString(); 638 | this.mapName = reader.readString(); 639 | this.course = reader.readInt32(); 640 | this.mode = reader.readInt32(); 641 | this.style = reader.readInt32(); 642 | this.time = reader.readFloat32(); 643 | this.teleportsUsed = reader.readInt32(); 644 | this.steamId = reader.readInt32(); 645 | this.steamId2 = reader.readString(); 646 | reader.readString(); 647 | this.playerName = reader.readString(); 648 | this.tickCount = reader.readInt32(); 649 | this.tickRate = Math.round(this.tickCount / this.time); // todo 650 | this.firstTickOffset = reader.getOffset(); 651 | this.tickSize = 7 * 4; 652 | } 653 | ReplayFile.prototype.getTickData = function (tick, data) { 654 | if (data === undefined) 655 | data = new TickData(); 656 | data.tick = tick; 657 | var reader = this.reader; 658 | reader.seek(this.firstTickOffset + this.tickSize * tick, Gokz.SeekOrigin.Begin); 659 | reader.readVector3(data.position); 660 | reader.readVector2(data.angles); 661 | data.buttons = reader.readInt32(); 662 | data.flags = reader.readInt32(); 663 | return data; 664 | }; 665 | ReplayFile.prototype.clampTick = function (tick) { 666 | return tick < 0 ? 0 : tick >= this.tickCount ? this.tickCount - 1 : tick; 667 | }; 668 | ReplayFile.MAGIC = 0x676F6B7A; 669 | return ReplayFile; 670 | }()); 671 | Gokz.ReplayFile = ReplayFile; 672 | })(Gokz || (Gokz = {})); 673 | /// 674 | /// 675 | var WebGame = Facepunch.WebGame; 676 | var Gokz; 677 | (function (Gokz) { 678 | /** 679 | * Creates a GOKZ replay viewer applet. 680 | */ 681 | var ReplayViewer = (function (_super) { 682 | __extends(ReplayViewer, _super); 683 | /** 684 | * Creates a new ReplayViewer inside the given `container` element. 685 | * @param container Element that should contain the viewer. 686 | */ 687 | function ReplayViewer(container) { 688 | var _this = this; 689 | _super.call(this, container); 690 | /** 691 | * If true, the current tick will be stored in the address hash when 692 | * playback is paused or the viewer uses the playback bar to skip 693 | * around. 694 | * @default `true` 695 | */ 696 | this.saveTickInHash = true; 697 | /** 698 | * The current tick being shown during playback, starting with 0 for 699 | * the first tick. Will automatically be increased while playing, 700 | * although some ticks might be skipped depending on playback speed and 701 | * frame rate. Can be set to skip to a particular tick. 702 | */ 703 | this.tick = -1; 704 | /** 705 | * Current playback rate, measured in seconds per second. Can support 706 | * negative values for rewinding. 707 | * @default `1.0` 708 | */ 709 | this.playbackRate = 1.0; 710 | /** 711 | * If true, the replay will automatically loop back to the first tick 712 | * when it reaches the end. 713 | * @default `true` 714 | */ 715 | this.autoRepeat = true; 716 | /** 717 | * Used internally to temporarily pause playback while the user is 718 | * dragging the scrubber in the playback bar. 719 | */ 720 | this.isScrubbing = false; 721 | /** 722 | * If true, the currently displayed tick will advance based on the 723 | * value of `playbackRate`. 724 | * @default `false` 725 | */ 726 | this.isPlaying = false; 727 | /** 728 | * If true, a crosshair graphic will be displayed in the middle of the 729 | * viewer. 730 | * @default `true` 731 | */ 732 | this.showCrosshair = true; 733 | /** 734 | * If true, makes the key press display visible. 735 | * @default `true` 736 | */ 737 | this.showKeyDisplay = true; 738 | /** 739 | * If true, makes the options menu visible. 740 | * @default `false` 741 | */ 742 | this.showOptions = false; 743 | /** 744 | * Event invoked when a new replay is loaded. Will be invoked before 745 | * the map for the replay is loaded (if required). 746 | * 747 | * **Available event arguments**: 748 | * * `replay: Gokz.ReplayFile` - The newly loaded ReplayFile 749 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 750 | */ 751 | this.replayLoaded = new Gokz.Event(this); 752 | /** 753 | * Event invoked after each update. 754 | * 755 | * **Available event arguments**: 756 | * * `dt: number` - Time since the last update 757 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 758 | */ 759 | this.updated = new Gokz.Event(this); 760 | /** 761 | * Event invoked when the current tick has changed. 762 | * 763 | * **Available event arguments**: 764 | * * `tickData: Gokz.TickData` - Recorded data for the current tick 765 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 766 | */ 767 | this.tickChanged = new Gokz.ChangedEvent(this); 768 | /** 769 | * Event invoked when playback has skipped to a different tick, for 770 | * example when the user uses the scrubber. 771 | * 772 | * **Available event arguments**: 773 | * * `oldTick: number` - The previous value of `tick` before skipping 774 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 775 | */ 776 | this.playbackSkipped = new Gokz.Event(this); 777 | /** 778 | * Event invoked when `playbackRate` changes. 779 | * 780 | * **Available event arguments**: 781 | * * `playbackRate: number` - The new playback rate 782 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 783 | */ 784 | this.playbackRateChanged = new Gokz.ChangedEvent(this); 785 | /** 786 | * Event invoked when `isPlaying` changes, for example when the user 787 | * pauses or resumes playback. 788 | * 789 | * **Available event arguments**: 790 | * * `isPlaying: boolean` - True if currently playing 791 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 792 | */ 793 | this.isPlayingChanged = new Gokz.ChangedEvent(this); 794 | /** 795 | * Event invoked when `showCrosshair` changes. 796 | * 797 | * **Available event arguments**: 798 | * * `showCrosshair: boolean` - True if crosshair is now visible 799 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 800 | */ 801 | this.showCrosshairChanged = new Gokz.ChangedEvent(this); 802 | /** 803 | * Event invoked when `showKeyDisplay` changes. 804 | * 805 | * **Available event arguments**: 806 | * * `showKeyDisplay: boolean` - True if keyDisplay is now visible 807 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 808 | */ 809 | this.showKeyDisplayChanged = new Gokz.ChangedEvent(this); 810 | /** 811 | * Event invoked when `showOptions` changes. 812 | * 813 | * **Available event arguments**: 814 | * * `showOptions: boolean` - True if options menu is now visible 815 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 816 | */ 817 | this.showOptionsChanged = new Gokz.ChangedEvent(this); 818 | /** 819 | * Event invoked when `cameraMode` changes. 820 | * 821 | * **Available event arguments**: 822 | * * `cameraMode: SourceUtils.CameraMode` - Camera mode value 823 | * * `sender: Gokz.ReplayViewer` - This ReplayViewer 824 | */ 825 | this.cameraModeChanged = new Gokz.ChangedEvent(this); 826 | this.pauseTime = 1.0; 827 | this.spareTime = 0; 828 | this.prevTick = undefined; 829 | this.tickData = new Gokz.TickData(); 830 | this.tempTickData0 = new Gokz.TickData(); 831 | this.tempTickData1 = new Gokz.TickData(); 832 | this.tempTickData2 = new Gokz.TickData(); 833 | this.ignoreMouseUp = true; 834 | this.saveCameraPosInHash = false; 835 | this.controls = new Gokz.ReplayControls(this); 836 | this.keyDisplay = new Gokz.KeyDisplay(this, this.controls.playbackBarElem); 837 | this.options = new Gokz.OptionsMenu(this, this.controls.playbackBarElem); 838 | var crosshair = document.createElement("div"); 839 | crosshair.classList.add("crosshair"); 840 | container.appendChild(crosshair); 841 | this.showCrosshairChanged.addListener(function (showCrosshair) { 842 | crosshair.hidden = !showCrosshair; 843 | }); 844 | this.isPlayingChanged.addListener(function (isPlaying) { 845 | if (!isPlaying && _this.saveTickInHash) 846 | _this.updateTickHash(); 847 | if (isPlaying) { 848 | _this.wakeLock = navigator.wakeLock; 849 | if (_this.wakeLock != null) { 850 | _this.wakeLock.request("display"); 851 | } 852 | _this.cameraMode = SourceUtils.CameraMode.Fixed; 853 | } 854 | else if (_this.wakeLock != null) { 855 | _this.wakeLock.release("display"); 856 | _this.wakeLock = null; 857 | } 858 | }); 859 | this.cameraModeChanged.addListener(function (mode) { 860 | if (mode === SourceUtils.CameraMode.FreeCam) { 861 | _this.isPlaying = false; 862 | } 863 | if (_this.routeLine != null) { 864 | _this.routeLine.visible = mode === SourceUtils.CameraMode.FreeCam; 865 | } 866 | _this.canLockPointer = mode === SourceUtils.CameraMode.FreeCam; 867 | if (!_this.canLockPointer && _this.isPointerLocked()) { 868 | document.exitPointerLock(); 869 | } 870 | }); 871 | } 872 | /** 873 | * Used to display an error message in the middle of the viewer. 874 | * @param message Message to display 875 | */ 876 | ReplayViewer.prototype.showMessage = function (message) { 877 | if (this.messageElem === undefined) { 878 | this.messageElem = this.onCreateMessagePanel(); 879 | } 880 | if (this.messageElem == null) 881 | return; 882 | this.messageElem.innerText = message; 883 | }; 884 | /** 885 | * Attempt to load a GOKZ replay from the given URL. When loaded, the 886 | * replay will be stored in the `replay` property in this viewer. 887 | * @param url Url of the replay to download. 888 | */ 889 | ReplayViewer.prototype.loadReplay = function (url) { 890 | var _this = this; 891 | console.log("Downloading: " + url); 892 | var req = new XMLHttpRequest(); 893 | req.open("GET", url, true); 894 | req.responseType = "arraybuffer"; 895 | req.onload = function (ev) { 896 | if (req.status !== 200) { 897 | _this.showMessage("Unable to download replay: " + req.statusText); 898 | return; 899 | } 900 | var arrayBuffer = req.response; 901 | if (arrayBuffer) { 902 | if (_this.routeLine != null) { 903 | _this.routeLine.dispose(); 904 | _this.routeLine = null; 905 | } 906 | try { 907 | _this.replay = new Gokz.ReplayFile(arrayBuffer); 908 | } 909 | catch (e) { 910 | _this.showMessage("Unable to read replay: " + e); 911 | } 912 | } 913 | }; 914 | req.send(null); 915 | }; 916 | /** 917 | * If `saveTickInHash` is true, will set the address hash to include 918 | * the current tick number. 919 | */ 920 | ReplayViewer.prototype.updateTickHash = function () { 921 | if (this.replay == null || !this.saveTickInHash) 922 | return; 923 | this.setHash({ t: this.replay.clampTick(this.tick) + 1 }); 924 | }; 925 | ReplayViewer.prototype.onCreateMessagePanel = function () { 926 | var elem = document.createElement("div"); 927 | elem.classList.add("message"); 928 | this.container.appendChild(elem); 929 | return elem; 930 | }; 931 | ReplayViewer.prototype.onInitialize = function () { 932 | _super.prototype.onInitialize.call(this); 933 | this.canLockPointer = false; 934 | this.cameraMode = SourceUtils.CameraMode.Fixed; 935 | }; 936 | ReplayViewer.prototype.onHashChange = function (hash) { 937 | if (typeof hash === "string") 938 | return; 939 | if (!this.saveTickInHash) 940 | return; 941 | var data = hash; 942 | if (data.t !== undefined && this.tick !== data.t) { 943 | this.tick = data.t - 1; 944 | this.isPlaying = false; 945 | } 946 | }; 947 | ReplayViewer.prototype.onMouseDown = function (button, screenPos, target) { 948 | this.ignoreMouseUp = event.target !== this.canvas; 949 | if (_super.prototype.onMouseDown.call(this, button, screenPos, target)) { 950 | this.showOptions = false; 951 | return true; 952 | } 953 | return false; 954 | }; 955 | ReplayViewer.prototype.onMouseUp = function (button, screenPos, target) { 956 | var ignored = this.ignoreMouseUp || event.target !== this.canvas; 957 | this.ignoreMouseUp = true; 958 | if (ignored) 959 | return false; 960 | if (this.controls.hideSpeedControl() || this.showOptions) { 961 | this.showOptions = false; 962 | return true; 963 | } 964 | if (_super.prototype.onMouseUp.call(this, button, screenPos, target)) 965 | return true; 966 | if (button === WebGame.MouseButton.Left && this.replay != null && this.map.isReady()) { 967 | this.isPlaying = !this.isPlaying; 968 | return true; 969 | } 970 | return false; 971 | }; 972 | ReplayViewer.prototype.onKeyDown = function (key) { 973 | switch (key) { 974 | case WebGame.Key.X: 975 | this.cameraMode = this.cameraMode === SourceUtils.CameraMode.FreeCam 976 | ? SourceUtils.CameraMode.Fixed : SourceUtils.CameraMode.FreeCam; 977 | if (this.cameraMode === SourceUtils.CameraMode.FreeCam) { 978 | this.container.requestPointerLock(); 979 | } 980 | return true; 981 | case WebGame.Key.F: 982 | this.toggleFullscreen(); 983 | return true; 984 | case WebGame.Key.Space: 985 | if (this.replay != null && this.map.isReady()) { 986 | this.isPlaying = !this.isPlaying; 987 | } 988 | return true; 989 | } 990 | return _super.prototype.onKeyDown.call(this, key); 991 | }; 992 | ReplayViewer.prototype.onChangeReplay = function (replay) { 993 | this.pauseTicks = Math.round(replay.tickRate * this.pauseTime); 994 | this.tick = this.tick === -1 ? 0 : this.tick; 995 | this.spareTime = 0; 996 | this.prevTick = undefined; 997 | this.replayLoaded.dispatch(this.replay); 998 | if (this.currentMapName !== replay.mapName) { 999 | if (this.currentMapName != null) { 1000 | this.map.unload(); 1001 | } 1002 | if (this.mapBaseUrl == null) { 1003 | throw "Cannot load a map when mapBaseUrl is unspecified."; 1004 | } 1005 | var version = new Date().getTime().toString(16); 1006 | this.currentMapName = replay.mapName; 1007 | this.loadMap(this.mapBaseUrl + "/" + replay.mapName + "/index.json?v=" + version); 1008 | } 1009 | }; 1010 | ReplayViewer.prototype.onUpdateFrame = function (dt) { 1011 | _super.prototype.onUpdateFrame.call(this, dt); 1012 | if (this.replay != this.lastReplay) { 1013 | this.lastReplay = this.replay; 1014 | if (this.replay != null) { 1015 | this.onChangeReplay(this.replay); 1016 | } 1017 | } 1018 | this.showCrosshairChanged.update(this.showCrosshair); 1019 | this.showKeyDisplayChanged.update(this.showKeyDisplay); 1020 | this.showOptionsChanged.update(this.showOptions); 1021 | this.playbackRateChanged.update(this.playbackRate); 1022 | this.cameraModeChanged.update(this.cameraMode); 1023 | if (this.replay == null) { 1024 | this.updated.dispatch(dt); 1025 | return; 1026 | } 1027 | var replay = this.replay; 1028 | var tickPeriod = 1.0 / replay.tickRate; 1029 | this.isPlayingChanged.update(this.isPlaying); 1030 | if (this.prevTick !== undefined && this.tick !== this.prevTick) { 1031 | this.playbackSkipped.dispatch(this.prevTick); 1032 | } 1033 | if (this.routeLine == null && this.map.isReady()) { 1034 | this.routeLine = new Gokz.RouteLine(this.map, this.replay); 1035 | } 1036 | if (this.map.isReady() && this.isPlaying && !this.isScrubbing) { 1037 | this.spareTime += dt * this.playbackRate; 1038 | var oldTick = this.tick; 1039 | // Forward playback 1040 | while (this.spareTime > tickPeriod) { 1041 | this.spareTime -= tickPeriod; 1042 | this.tick += 1; 1043 | if (this.tick > replay.tickCount + this.pauseTicks * 2) { 1044 | this.tick = -this.pauseTicks; 1045 | } 1046 | } 1047 | // Rewinding 1048 | while (this.spareTime < 0) { 1049 | this.spareTime += tickPeriod; 1050 | this.tick -= 1; 1051 | if (this.tick < -this.pauseTicks * 2) { 1052 | this.tick = replay.tickCount + this.pauseTicks; 1053 | } 1054 | } 1055 | } 1056 | else { 1057 | this.spareTime = 0; 1058 | } 1059 | this.prevTick = this.tick; 1060 | replay.getTickData(replay.clampTick(this.tick), this.tickData); 1061 | var eyeHeight = this.tickData.getEyeHeight(); 1062 | this.tickChanged.update(this.tick, this.tickData); 1063 | if (this.spareTime >= 0 && this.spareTime <= tickPeriod) { 1064 | var t = this.spareTime / tickPeriod; 1065 | var d0 = replay.getTickData(replay.clampTick(this.tick - 1), this.tempTickData0); 1066 | var d1 = this.tickData; 1067 | var d2 = replay.getTickData(replay.clampTick(this.tick + 1), this.tempTickData1); 1068 | var d3 = replay.getTickData(replay.clampTick(this.tick + 2), this.tempTickData2); 1069 | Gokz.Utils.hermitePosition(d0.position, d1.position, d2.position, d3.position, t, this.tickData.position); 1070 | Gokz.Utils.hermiteAngles(d0.angles, d1.angles, d2.angles, d3.angles, t, this.tickData.angles); 1071 | eyeHeight = Gokz.Utils.hermiteValue(d0.getEyeHeight(), d1.getEyeHeight(), d2.getEyeHeight(), d3.getEyeHeight(), t); 1072 | } 1073 | if (this.cameraMode === SourceUtils.CameraMode.Fixed) { 1074 | this.mainCamera.setPosition(this.tickData.position.x, this.tickData.position.y, this.tickData.position.z + eyeHeight); 1075 | this.setCameraAngles((this.tickData.angles.y - 90) * Math.PI / 180, -this.tickData.angles.x * Math.PI / 180); 1076 | } 1077 | this.updated.dispatch(dt); 1078 | }; 1079 | return ReplayViewer; 1080 | }(SourceUtils.MapViewer)); 1081 | Gokz.ReplayViewer = ReplayViewer; 1082 | })(Gokz || (Gokz = {})); 1083 | var Gokz; 1084 | (function (Gokz) { 1085 | var RouteLine = (function (_super) { 1086 | __extends(RouteLine, _super); 1087 | function RouteLine(map, replay) { 1088 | _super.call(this, map, { classname: "route_line", clusters: null }); 1089 | this.isVisible = false; 1090 | this.segments = new Array(Math.ceil(replay.tickCount / RouteLine.segmentTicks)); 1091 | var tickData = new Gokz.TickData(); 1092 | var progressScale = 16 / replay.tickRate; 1093 | var lastPos = new Facepunch.Vector3(); 1094 | var currPos = new Facepunch.Vector3(); 1095 | for (var i = 0; i < this.segments.length; ++i) { 1096 | var firstTick = i * RouteLine.segmentTicks; 1097 | var lastTick = Math.min((i + 1) * RouteLine.segmentTicks, replay.tickCount - 1); 1098 | var segment = this.segments[i] = { 1099 | debugLine: new WebGame.DebugLine(map.viewer), 1100 | clusters: {} 1101 | }; 1102 | var debugLine = segment.debugLine; 1103 | var clusters = segment.clusters; 1104 | debugLine.setColor({ x: 0.125, y: 0.75, z: 0.125 }, { x: 0.0, y: 0.25, z: 0.0 }); 1105 | debugLine.frequency = 4.0; 1106 | var lineStartTick = firstTick; 1107 | for (var t = firstTick; t <= lastTick; ++t) { 1108 | replay.getTickData(t, tickData); 1109 | currPos.copy(tickData.position); 1110 | currPos.z += 16; 1111 | var leaf = map.getLeafAt(currPos); 1112 | if (leaf != null && leaf.cluster !== -1) { 1113 | clusters[leaf.cluster] = true; 1114 | } 1115 | // Start new line if first in segment or player teleported 1116 | if (t === firstTick || lastPos.sub(currPos).lengthSq() > 1024.0) { 1117 | debugLine.moveTo(currPos); 1118 | lineStartTick = t; 1119 | } 1120 | else { 1121 | debugLine.lineTo(currPos, (t - lineStartTick) * progressScale); 1122 | } 1123 | lastPos.copy(currPos); 1124 | } 1125 | debugLine.update(); 1126 | } 1127 | } 1128 | Object.defineProperty(RouteLine.prototype, "visible", { 1129 | get: function () { 1130 | return this.isVisible; 1131 | }, 1132 | set: function (value) { 1133 | if (this.isVisible === value) 1134 | return; 1135 | this.isVisible = value; 1136 | if (value) { 1137 | this.map.addPvsEntity(this); 1138 | } 1139 | else { 1140 | this.map.removePvsEntity(this); 1141 | } 1142 | this.map.viewer.forceDrawListInvalidation(true); 1143 | }, 1144 | enumerable: true, 1145 | configurable: true 1146 | }); 1147 | RouteLine.prototype.onPopulateDrawList = function (drawList, clusters) { 1148 | for (var _i = 0, _a = this.segments; _i < _a.length; _i++) { 1149 | var segment = _a[_i]; 1150 | if (clusters == null) { 1151 | drawList.addItem(segment.debugLine); 1152 | continue; 1153 | } 1154 | var segmentClusters = segment.clusters; 1155 | for (var _b = 0, clusters_1 = clusters; _b < clusters_1.length; _b++) { 1156 | var cluster = clusters_1[_b]; 1157 | if (segmentClusters[cluster]) { 1158 | drawList.addItem(segment.debugLine); 1159 | break; 1160 | } 1161 | } 1162 | } 1163 | }; 1164 | RouteLine.prototype.dispose = function () { 1165 | this.visible = false; 1166 | for (var _i = 0, _a = this.segments; _i < _a.length; _i++) { 1167 | var segment = _a[_i]; 1168 | segment.debugLine.dispose(); 1169 | } 1170 | this.segments.splice(0, this.segments.length); 1171 | }; 1172 | RouteLine.segmentTicks = 60 * 128; 1173 | return RouteLine; 1174 | }(SourceUtils.Entities.PvsEntity)); 1175 | Gokz.RouteLine = RouteLine; 1176 | })(Gokz || (Gokz = {})); 1177 | var Gokz; 1178 | (function (Gokz) { 1179 | var Utils = (function () { 1180 | function Utils() { 1181 | } 1182 | Utils.deltaAngle = function (a, b) { 1183 | return (b - a) - Math.floor((b - a + 180) / 360) * 360; 1184 | }; 1185 | Utils.hermiteValue = function (p0, p1, p2, p3, t) { 1186 | var m0 = (p2 - p0) * 0.5; 1187 | var m1 = (p3 - p1) * 0.5; 1188 | var t2 = t * t; 1189 | var t3 = t * t * t; 1190 | return (2 * t3 - 3 * t2 + 1) * p1 + (t3 - 2 * t2 + t) * m0 1191 | + (-2 * t3 + 3 * t2) * p2 + (t3 - t2) * m1; 1192 | }; 1193 | Utils.hermitePosition = function (p0, p1, p2, p3, t, out) { 1194 | out.x = Utils.hermiteValue(p0.x, p1.x, p2.x, p3.x, t); 1195 | out.y = Utils.hermiteValue(p0.y, p1.y, p2.y, p3.y, t); 1196 | out.z = Utils.hermiteValue(p0.z, p1.z, p2.z, p3.z, t); 1197 | }; 1198 | Utils.hermiteAngles = function (a0, a1, a2, a3, t, out) { 1199 | out.x = Utils.hermiteValue(a1.x + Utils.deltaAngle(a1.x, a0.x), a1.x, a1.x + Utils.deltaAngle(a1.x, a2.x), a1.x + Utils.deltaAngle(a1.x, a3.x), t); 1200 | out.y = Utils.hermiteValue(a1.y + Utils.deltaAngle(a1.y, a0.y), a1.y, a1.y + Utils.deltaAngle(a1.y, a2.y), a1.y + Utils.deltaAngle(a1.y, a3.y), t); 1201 | }; 1202 | return Utils; 1203 | }()); 1204 | Gokz.Utils = Utils; 1205 | })(Gokz || (Gokz = {})); 1206 | --------------------------------------------------------------------------------