evotars 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.editorconfig +13 -0
  2. package/.prettierrc.json +3 -0
  3. package/LICENSE +21 -0
  4. package/README.md +40 -0
  5. package/index.html +13 -0
  6. package/package.json +20 -0
  7. package/public/fonts/Rubik.ttf +0 -0
  8. package/public/sounds/jump.mp3 +0 -0
  9. package/public/sprites/agent/agent.json +166 -0
  10. package/public/sprites/agent/agent.png +0 -0
  11. package/public/sprites/cat/cat.json +231 -0
  12. package/public/sprites/cat/cat.png +0 -0
  13. package/public/sprites/dude/dude.json +231 -0
  14. package/public/sprites/dude/dude.png +0 -0
  15. package/public/sprites/girl/girl.json +231 -0
  16. package/public/sprites/girl/girl.png +0 -0
  17. package/public/sprites/nerd/nerd.json +231 -0
  18. package/public/sprites/nerd/nerd.png +0 -0
  19. package/public/sprites/senior/senior.json +231 -0
  20. package/public/sprites/senior/senior.png +0 -0
  21. package/public/sprites/sith/sith.json +166 -0
  22. package/public/sprites/sith/sith.png +0 -0
  23. package/src/app.ts +63 -0
  24. package/src/config/config.ts +133 -0
  25. package/src/config/constants.ts +1 -0
  26. package/src/dudes.ts +71 -0
  27. package/src/entities/Dude.ts +453 -0
  28. package/src/entities/DudeEmoteSpitter.ts +60 -0
  29. package/src/entities/DudeMessage.ts +163 -0
  30. package/src/entities/DudeName.ts +39 -0
  31. package/src/entities/DudeSpriteContainer.ts +68 -0
  32. package/src/entities/DudeTrailEffect.ts +72 -0
  33. package/src/helpers/timer.ts +43 -0
  34. package/src/helpers/types.ts +11 -0
  35. package/src/index.ts +75 -0
  36. package/src/services/assetsLoader.ts +27 -0
  37. package/src/services/dudesManager.ts +305 -0
  38. package/src/services/soundService.ts +28 -0
  39. package/src/services/spriteProvider.ts +93 -0
  40. package/src/styles.css +16 -0
  41. package/src/types/dto/action.ts +68 -0
  42. package/src/types/dto/chatter.ts +4 -0
  43. package/src/types/dto/event.ts +9 -0
  44. package/src/types/dto/index.ts +6 -0
  45. package/src/types/dto/message.ts +8 -0
  46. package/src/types/dto/settings.ts +5 -0
  47. package/src/types/dto/user.ts +13 -0
  48. package/src/types/index.ts +1 -0
  49. package/tsconfig.json +23 -0
@@ -0,0 +1,305 @@
1
+ import {
2
+ MessageEntity,
3
+ RaidData,
4
+ TwitchChatterEntity,
5
+ UserActionEntity,
6
+ isColorUserActionEntity,
7
+ isDashUserActionEntity,
8
+ isGrowUserActionEntity,
9
+ isJumpUserActionEntity,
10
+ } from "../types";
11
+ import * as PIXI from "pixi.js";
12
+ import tinycolor from "tinycolor2";
13
+ import { app } from "../app";
14
+ import { config } from "../config/config";
15
+ import { Dude, DudeProps } from "../entities/Dude";
16
+ import { timers } from "../helpers/timer";
17
+
18
+ type DudesManagerSubscription = {
19
+ onAdd: (dude: Dude) => void;
20
+ onDelete: (dude: Dude) => void;
21
+ };
22
+
23
+ const DUDE_DESPAWN_TIMER = 1000 * 60 * 5;
24
+
25
+ class DudesManager {
26
+ private viewers: Record<string, Dude | undefined> = {};
27
+
28
+ private raiders: Dude[] = [];
29
+
30
+ private lastDudeActivity: Record<string, number | undefined> = {};
31
+
32
+ private subscriptions: DudesManagerSubscription[] = [];
33
+
34
+ public subscribe(subscription: DudesManagerSubscription) {
35
+ this.subscriptions.push(subscription);
36
+ }
37
+
38
+ public update() {
39
+ for (const id in this.viewers) {
40
+ const viewer = this.viewers[id];
41
+
42
+ if (viewer) {
43
+ viewer.update();
44
+ }
45
+ }
46
+
47
+ for (const dude of this.raiders) {
48
+ dude.update();
49
+ }
50
+ }
51
+
52
+ public processRaid(data: RaidData) {
53
+ if (!app.settings.fallingRaiders) {
54
+ return;
55
+ }
56
+
57
+ const time = (1 / data.viewers) * 5000;
58
+
59
+ const spriteConfig = config.sprites["agent"];
60
+
61
+ for (let i = 0; i < data.viewers; i++) {
62
+ const dude = new Dude({
63
+ isAnonymous: true,
64
+ zIndex: -1,
65
+ sprite: spriteConfig,
66
+ color: new PIXI.Color(data.broadcaster.info.color),
67
+ });
68
+
69
+ timers.add(time * i, () => {
70
+ this.addRaider(dude);
71
+ dude.spawn({ isFalling: true });
72
+ });
73
+
74
+ timers.add(i * time + 30000, () => {
75
+ dude.despawn({
76
+ onComplete: () => {
77
+ this.deleteRaider(dude);
78
+ },
79
+ });
80
+ });
81
+ }
82
+
83
+ const dude = this.viewers[data.broadcaster.id];
84
+
85
+ const props: DudeProps = {
86
+ ...this.prepareDudeProps(
87
+ data.broadcaster.info.displayName,
88
+ data.broadcaster.info.color,
89
+ ),
90
+ scale: 8,
91
+ };
92
+
93
+ const spawnBroadcaster = () => {
94
+ const dude = new Dude(props);
95
+ this.addViewer(data.broadcaster.id, dude);
96
+ dude.spawn({ isFalling: true, positionX: 0.5 });
97
+ };
98
+
99
+ this.lastDudeActivity[data.broadcaster.id] = performance.now();
100
+
101
+ if (!dude) {
102
+ spawnBroadcaster();
103
+ } else {
104
+ dude.despawn({
105
+ onComplete: () => {
106
+ spawnBroadcaster();
107
+ },
108
+ });
109
+ }
110
+ }
111
+
112
+ public processChatters(data: TwitchChatterEntity[]) {
113
+ for (const id in this.viewers) {
114
+ const lastMessageTime = this.lastDudeActivity[id];
115
+ const spawnedRecently =
116
+ !!lastMessageTime &&
117
+ performance.now() - lastMessageTime < DUDE_DESPAWN_TIMER;
118
+
119
+ if (data.every((chatter) => chatter.userId != id) && !spawnedRecently) {
120
+ const dude = this.viewers[id];
121
+
122
+ if (dude) {
123
+ dude.despawn({
124
+ onComplete: () => {
125
+ this.deleteViewer(id, dude);
126
+ },
127
+ });
128
+ }
129
+ }
130
+ }
131
+
132
+ if (app.settings.showAnonymousDudes) {
133
+ for (const chatter of data) {
134
+ const dude = this.viewers[chatter.userId];
135
+
136
+ if (!dude) {
137
+ const props = {
138
+ name: chatter.name,
139
+ isAnonymous: !this.hasActivity(chatter.userId),
140
+ };
141
+
142
+ const dude = new Dude(props);
143
+ dude.spawn();
144
+
145
+ this.addViewer(chatter.userId, dude);
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ private hasActivity = (userId: string): boolean =>
152
+ !!this.lastDudeActivity[userId];
153
+
154
+ public doAction(action: UserActionEntity, dude: Dude) {
155
+ if (isJumpUserActionEntity(action)) {
156
+ dude.jump({
157
+ velocityX: action.data.velocityX,
158
+ velocityY: action.data.velocityY,
159
+ cooldown: action.cooldown,
160
+ });
161
+ }
162
+
163
+ if (isDashUserActionEntity(action)) {
164
+ dude.dash({ force: action.data.force, cooldown: action.cooldown });
165
+ }
166
+
167
+ if (isColorUserActionEntity(action)) {
168
+ const color = tinycolor(action.data.color);
169
+
170
+ if (color && color.isValid()) {
171
+ dude.setUserProps({ color: new PIXI.Color(action.data.color) });
172
+ }
173
+ }
174
+
175
+ if (isGrowUserActionEntity(action)) {
176
+ dude.scale({
177
+ value: action.data.scale,
178
+ duration: action.data.duration,
179
+ cooldown: action.cooldown,
180
+ });
181
+ }
182
+ }
183
+
184
+ private prepareDudeProps(
185
+ name: string,
186
+ color?: string,
187
+ sprite?: string,
188
+ ): DudeProps {
189
+ const props: DudeProps = {
190
+ name,
191
+ isAnonymous: false,
192
+ };
193
+
194
+ const spriteConfig = sprite ? config.sprites[sprite] : null;
195
+
196
+ if (spriteConfig) {
197
+ props.sprite = spriteConfig;
198
+ }
199
+
200
+ if (color) {
201
+ props.color = new PIXI.Color(color);
202
+ }
203
+
204
+ return props;
205
+ }
206
+
207
+ public processAction(action: UserActionEntity): void {
208
+ const dude = this.viewers[action.userId];
209
+
210
+ const props: DudeProps = this.prepareDudeProps(
211
+ action.info.displayName,
212
+ action.info.color,
213
+ action.info.sprite,
214
+ );
215
+
216
+ if (!dude) {
217
+ const dude = new Dude(props);
218
+
219
+ dude.spawn({
220
+ onComplete: () => {
221
+ const dude = this.viewers[action.userId];
222
+ if (dude) {
223
+ this.doAction(action, dude);
224
+ }
225
+ },
226
+ });
227
+
228
+ this.addViewer(action.userId, dude);
229
+ } else {
230
+ this.doAction(action, dude);
231
+ dude.setProps(props);
232
+ }
233
+ }
234
+
235
+ public processMessage(data: MessageEntity): void {
236
+ const props: DudeProps = this.prepareDudeProps(
237
+ data.info.displayName,
238
+ data.info.color,
239
+ data.info.sprite,
240
+ );
241
+
242
+ let dude = this.viewers[data.userId];
243
+
244
+ if (!dude) {
245
+ const isFalling = app.settings.fallingDudes
246
+ ? !this.hasActivity(data.userId)
247
+ : false;
248
+
249
+ dude = new Dude(props);
250
+ dude.spawn({ isFalling });
251
+
252
+ this.addViewer(data.userId, dude);
253
+ } else {
254
+ dude.setProps(props);
255
+ }
256
+
257
+ this.lastDudeActivity[data.userId] = performance.now();
258
+
259
+ if (data.message) {
260
+ dude.addMessage(data.message);
261
+ }
262
+
263
+ if (data.emotes.length > 0) {
264
+ dude.spitEmotes(data.emotes);
265
+ }
266
+ }
267
+
268
+ public addViewer(id: string, dude: Dude): void {
269
+ this.subscriptions.forEach((s) => s.onAdd(dude));
270
+ this.viewers[id] = dude;
271
+ }
272
+
273
+ public deleteViewer(id: string, dude: Dude): void {
274
+ this.subscriptions.forEach((s) => s.onDelete(dude));
275
+ delete this.viewers[id];
276
+ }
277
+
278
+ public addRaider(dude: Dude): void {
279
+ this.subscriptions.forEach((s) => s.onAdd(dude));
280
+ this.raiders.push(dude);
281
+ }
282
+
283
+ public deleteRaider(dude: Dude): void {
284
+ this.subscriptions.forEach((s) => s.onDelete(dude));
285
+ this.raiders = this.raiders.filter((raider) => raider != dude);
286
+ }
287
+
288
+ public zIndexDudeMax(from: number) {
289
+ return Object.values(this.viewers).reduce((zIndex, dude) => {
290
+ return dude && zIndex <= dude.container.zIndex
291
+ ? dude.container.zIndex + 1
292
+ : zIndex;
293
+ }, from);
294
+ }
295
+
296
+ public zIndexDudeMin(from: number) {
297
+ return Object.values(this.viewers).reduce((zIndex, dude) => {
298
+ return dude && zIndex >= dude.container.zIndex
299
+ ? dude.container.zIndex - 1
300
+ : zIndex;
301
+ }, from);
302
+ }
303
+ }
304
+
305
+ export const dudesManager = new DudesManager();
@@ -0,0 +1,28 @@
1
+ export type SoundManifest = {
2
+ [key: string]: string;
3
+ };
4
+
5
+ class SoundService {
6
+ assets: { [key: string]: HTMLAudioElement } = {};
7
+
8
+ async init(manifest: SoundManifest) {
9
+ this.assets = Object.fromEntries(
10
+ Array.from(Object.entries(manifest)).map(([key, value]) => {
11
+ return [key, new Audio(value)];
12
+ })
13
+ );
14
+ }
15
+
16
+ play(name: string) {
17
+ const audio = this.assets[name];
18
+
19
+ if (audio) {
20
+ audio.volume = 0.2;
21
+ audio.pause();
22
+ audio.currentTime = 0;
23
+ audio.play();
24
+ }
25
+ }
26
+ }
27
+
28
+ export const soundService = new SoundService();
@@ -0,0 +1,93 @@
1
+ import { AsepriteData, assetsLoader } from './assetsLoader';
2
+ import * as PIXI from 'pixi.js';
3
+
4
+ export type DudeLayerAnimatedSprite = {
5
+ layer: DudeSpriteLayers;
6
+ sprite: PIXI.AnimatedSprite;
7
+ };
8
+
9
+ export type DudeTagAnimatedSprites = {
10
+ [tag in DudeSpriteTags]: DudeLayerAnimatedSprite[];
11
+ };
12
+
13
+ export enum DudeSpriteLayers {
14
+ Body = 'Body',
15
+ Eyes = 'Eyes',
16
+ }
17
+
18
+ export enum DudeSpriteTags {
19
+ Idle = 'Idle',
20
+ Jump = 'Jump',
21
+ Fall = 'Fall',
22
+ Land = 'Land',
23
+ Run = 'Run',
24
+ Die = 'Die',
25
+ }
26
+
27
+ export type FrameTag = {
28
+ from: number;
29
+ name: string;
30
+ to: number;
31
+ direction: string;
32
+ };
33
+
34
+ export class SpriteProvider {
35
+ public createLayerAnimatedSprites(
36
+ name: string,
37
+ sheet: PIXI.Spritesheet<AsepriteData>,
38
+ frameTag: FrameTag
39
+ ): DudeLayerAnimatedSprite[] {
40
+ const layers = sheet.data.meta.layers;
41
+
42
+ if (layers) {
43
+ const textures = Object.fromEntries<PIXI.FrameObject[]>(
44
+ layers.map((layer) => [layer.name, []])
45
+ );
46
+
47
+ for (let i = frameTag.from; i <= frameTag.to; i++) {
48
+ const framekey = i.toString();
49
+
50
+ for (const layer in textures) {
51
+ const key = name + '_' + layer + '_' + framekey;
52
+ const texture = sheet.textures[key];
53
+ const time = sheet.data.frames[key].duration;
54
+
55
+ textures[layer].push({ texture: texture, time: time });
56
+ }
57
+ }
58
+
59
+ return Object.entries(textures).map((entry) => {
60
+ const sprite = new PIXI.AnimatedSprite(entry[1]);
61
+ sprite.texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST;
62
+ return { layer: entry[0] as DudeSpriteLayers, sprite };
63
+ });
64
+ }
65
+
66
+ throw Error("Sprite sheet doesn't have layers");
67
+ }
68
+
69
+ public createTagAnimatedSprites(sheetName: string): DudeTagAnimatedSprites {
70
+ if (!sheetName) {
71
+ throw Error('Sheet is not defined');
72
+ }
73
+
74
+ const sheet = assetsLoader.sheets[sheetName];
75
+
76
+ return (sheet.data.meta.frameTags ?? []).reduce<DudeTagAnimatedSprites>(
77
+ (sprites, tag) => {
78
+ if (tag && tag.name) {
79
+ sprites[tag.name as DudeSpriteTags] = this.createLayerAnimatedSprites(
80
+ sheetName,
81
+ sheet,
82
+ tag
83
+ );
84
+ }
85
+
86
+ return sprites;
87
+ },
88
+ {} as DudeTagAnimatedSprites
89
+ );
90
+ }
91
+ }
92
+
93
+ export const spriteProvider = new SpriteProvider();
package/src/styles.css ADDED
@@ -0,0 +1,16 @@
1
+ :root {
2
+ margin: 0;
3
+ padding: 0;
4
+ }
5
+
6
+ body {
7
+ margin: 0;
8
+ padding: 0;
9
+ width: 100%;
10
+ height: 100%;
11
+ overflow: hidden;
12
+ }
13
+
14
+ #root {
15
+ height: 400px;
16
+ }
@@ -0,0 +1,68 @@
1
+ import { UserInfo } from "./user";
2
+
3
+ export type ActionData = Record<string, string | number | undefined>;
4
+
5
+ export type ActionableData = {
6
+ arguments: string[];
7
+ action: ActionData;
8
+ };
9
+
10
+ export type ActionEntity = {
11
+ id: number;
12
+ name: string;
13
+ title: string;
14
+ description: string;
15
+ data: ActionData;
16
+ };
17
+
18
+ export type UserActionEntity = {
19
+ userId: string;
20
+ cooldown: number;
21
+ info: UserInfo;
22
+ } & ActionEntity;
23
+
24
+ export type JumpUserActionEntity = {
25
+ name: "jump";
26
+ data: {
27
+ velocityX: number;
28
+ velocityY: number;
29
+ };
30
+ } & UserActionEntity;
31
+
32
+ export type ColorUserActionEntity = {
33
+ name: "color";
34
+ data: {
35
+ color: string;
36
+ };
37
+ } & UserActionEntity;
38
+
39
+ export type GrowUserActionEntity = {
40
+ name: "grow";
41
+ data: {
42
+ duration: number;
43
+ scale: number;
44
+ };
45
+ } & UserActionEntity;
46
+
47
+ export type DashUserActionEntity = {
48
+ name: "dash";
49
+ data: {
50
+ force: number;
51
+ };
52
+ } & UserActionEntity;
53
+
54
+ export const isJumpUserActionEntity = (
55
+ entity: ActionEntity,
56
+ ): entity is JumpUserActionEntity => entity.name == "jump";
57
+
58
+ export const isColorUserActionEntity = (
59
+ entity: ActionEntity,
60
+ ): entity is ColorUserActionEntity => entity.name == "color";
61
+
62
+ export const isGrowUserActionEntity = (
63
+ entity: ActionEntity,
64
+ ): entity is GrowUserActionEntity => entity.name == "grow";
65
+
66
+ export const isDashUserActionEntity = (
67
+ entity: ActionEntity,
68
+ ): entity is DashUserActionEntity => entity.name == "dash";
@@ -0,0 +1,4 @@
1
+ export type TwitchChatterEntity = {
2
+ userId: string;
3
+ name: string;
4
+ };
@@ -0,0 +1,9 @@
1
+ import { UserInfo } from './user';
2
+
3
+ export type RaidData = {
4
+ broadcaster: {
5
+ id: string;
6
+ info: UserInfo;
7
+ };
8
+ viewers: number;
9
+ };
@@ -0,0 +1,6 @@
1
+ export * from './message';
2
+ export * from './chatter';
3
+ export * from './settings';
4
+ export * from './user';
5
+ export * from './action';
6
+ export * from './event';
@@ -0,0 +1,8 @@
1
+ import { UserInfo } from './user';
2
+
3
+ export type MessageEntity = {
4
+ userId: string;
5
+ message: string;
6
+ emotes: string[];
7
+ info: UserInfo;
8
+ };
@@ -0,0 +1,5 @@
1
+ export type SettingsEntity = {
2
+ showAnonymousDudes?: boolean;
3
+ fallingDudes?: boolean;
4
+ fallingRaiders?: boolean;
5
+ };
@@ -0,0 +1,13 @@
1
+ export type UserEntity = {
2
+ userId: number;
3
+ guid: string;
4
+ displayName: string;
5
+ profileImageUrl: string;
6
+ previewUrl: string;
7
+ };
8
+
9
+ export type UserInfo = {
10
+ displayName: string;
11
+ color?: string;
12
+ sprite?: string;
13
+ }
@@ -0,0 +1 @@
1
+ export * from './dto';
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable", "ES2021.String"],
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noFallthroughCasesInSwitch": true
21
+ },
22
+ "include": ["src"]
23
+ }