@rpgjs/client 5.0.0-beta.20 → 5.0.0-beta.22
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.
- package/CHANGELOG.md +7 -0
- package/dist/Gui/Gui.js +37 -4
- package/dist/Gui/Gui.js.map +1 -1
- package/dist/RpgClientEngine.d.ts +16 -9
- package/dist/RpgClientEngine.js +25 -7
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/character.ce.js +150 -9
- package/dist/components/character.ce.js.map +1 -1
- package/dist/components/gui/mobile/index.d.ts +51 -2
- package/dist/components/gui/mobile/index.js +12 -4
- package/dist/components/gui/mobile/index.js.map +1 -1
- package/dist/components/gui/mobile/index.spec.d.ts +1 -0
- package/dist/components/gui/mobile/mobile.ce.js +303 -55
- package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/services/cameraFollow.d.ts +51 -0
- package/dist/services/cameraFollow.js +134 -0
- package/dist/services/cameraFollow.js.map +1 -0
- package/dist/services/cameraFollow.spec.d.ts +1 -0
- package/package.json +5 -5
- package/src/Gui/Gui.spec.ts +19 -0
- package/src/Gui/Gui.ts +50 -11
- package/src/RpgClientEngine.ts +37 -18
- package/src/components/character.ce +167 -9
- package/src/components/gui/mobile/index.spec.ts +94 -0
- package/src/components/gui/mobile/index.ts +74 -6
- package/src/components/gui/mobile/mobile.ce +347 -65
- package/src/index.ts +12 -0
- package/src/services/cameraFollow.spec.ts +220 -0
- package/src/services/cameraFollow.ts +222 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
applyCameraFollow,
|
|
4
|
+
cameraFollowAnimationOptions,
|
|
5
|
+
cameraFollowOptions,
|
|
6
|
+
ownsCameraFollowRevision,
|
|
7
|
+
} from "./cameraFollow";
|
|
8
|
+
|
|
9
|
+
const createViewport = () => ({
|
|
10
|
+
animate: vi.fn(),
|
|
11
|
+
follow: vi.fn(),
|
|
12
|
+
plugins: {
|
|
13
|
+
remove: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("camera follow", () => {
|
|
18
|
+
it("normalizes transition options from smoothMove", () => {
|
|
19
|
+
expect(cameraFollowAnimationOptions(true)).toEqual({
|
|
20
|
+
time: 1000,
|
|
21
|
+
ease: "easeInOutSine",
|
|
22
|
+
});
|
|
23
|
+
expect(cameraFollowAnimationOptions({ time: 450, ease: "easeInOutQuad" })).toEqual({
|
|
24
|
+
time: 450,
|
|
25
|
+
ease: "easeInOutQuad",
|
|
26
|
+
});
|
|
27
|
+
expect(cameraFollowAnimationOptions({ time: 450, ease: "easeInOutQuadd" } as any)).toEqual({
|
|
28
|
+
time: 450,
|
|
29
|
+
ease: "easeInOutSine",
|
|
30
|
+
});
|
|
31
|
+
expect(cameraFollowAnimationOptions(false)).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("normalizes continuous follow options", () => {
|
|
35
|
+
expect(cameraFollowOptions({ speed: 12, acceleration: 0.2, radius: 80 })).toEqual({
|
|
36
|
+
speed: 12,
|
|
37
|
+
acceleration: 0.2,
|
|
38
|
+
radius: 80,
|
|
39
|
+
});
|
|
40
|
+
expect(cameraFollowOptions({ speed: -4, acceleration: null, radius: null })).toEqual({
|
|
41
|
+
speed: 0,
|
|
42
|
+
acceleration: null,
|
|
43
|
+
radius: null,
|
|
44
|
+
});
|
|
45
|
+
expect(cameraFollowOptions(true)).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("animates to the target then follows it", () => {
|
|
49
|
+
const viewport = createViewport();
|
|
50
|
+
const target = { x: 120, y: 240 };
|
|
51
|
+
|
|
52
|
+
applyCameraFollow({
|
|
53
|
+
viewport,
|
|
54
|
+
target,
|
|
55
|
+
smoothMove: { time: 800, ease: "easeInOutQuad", speed: 10, radius: 32 },
|
|
56
|
+
followRevision: 2,
|
|
57
|
+
isCurrentRevision: (revision) => revision === 2,
|
|
58
|
+
shouldFollowCamera: () => true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(viewport.plugins.remove).toHaveBeenCalledWith("animate");
|
|
62
|
+
expect(viewport.plugins.remove).toHaveBeenCalledWith("follow");
|
|
63
|
+
expect(viewport.animate).toHaveBeenCalledWith(
|
|
64
|
+
expect.objectContaining({
|
|
65
|
+
position: { x: 120, y: 240 },
|
|
66
|
+
time: 800,
|
|
67
|
+
ease: "easeInOutQuad",
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
expect(viewport.follow).not.toHaveBeenCalled();
|
|
71
|
+
|
|
72
|
+
const animateOptions = viewport.animate.mock.calls[0][0];
|
|
73
|
+
animateOptions.callbackOnComplete();
|
|
74
|
+
|
|
75
|
+
const [followTarget, followOptions] = viewport.follow.mock.calls[0];
|
|
76
|
+
expect(followTarget).not.toBe(target);
|
|
77
|
+
expect(followTarget.x).toBe(120);
|
|
78
|
+
expect(followTarget.y).toBe(240);
|
|
79
|
+
expect(followOptions).toEqual({
|
|
80
|
+
speed: 10,
|
|
81
|
+
radius: 32,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("follows instantly when smoothMove is disabled", () => {
|
|
86
|
+
const viewport = createViewport();
|
|
87
|
+
const target = { x: 120, y: 240 };
|
|
88
|
+
|
|
89
|
+
applyCameraFollow({
|
|
90
|
+
viewport,
|
|
91
|
+
target,
|
|
92
|
+
smoothMove: false,
|
|
93
|
+
followRevision: 1,
|
|
94
|
+
isCurrentRevision: () => true,
|
|
95
|
+
shouldFollowCamera: () => true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(viewport.animate).not.toHaveBeenCalled();
|
|
99
|
+
const [followTarget] = viewport.follow.mock.calls[0];
|
|
100
|
+
expect(followTarget).not.toBe(target);
|
|
101
|
+
expect(followTarget.x).toBe(120);
|
|
102
|
+
expect(followTarget.y).toBe(240);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("does not follow after animation if another camera command superseded it", () => {
|
|
106
|
+
const viewport = createViewport();
|
|
107
|
+
|
|
108
|
+
applyCameraFollow({
|
|
109
|
+
viewport,
|
|
110
|
+
target: { x: 120, y: 240 },
|
|
111
|
+
smoothMove: { time: 800, ease: "easeInOutQuad" },
|
|
112
|
+
followRevision: 2,
|
|
113
|
+
isCurrentRevision: (revision) => revision === 3,
|
|
114
|
+
shouldFollowCamera: () => true,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const animateOptions = viewport.animate.mock.calls[0][0];
|
|
118
|
+
animateOptions.callbackOnComplete();
|
|
119
|
+
|
|
120
|
+
expect(viewport.follow).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not follow a target whose position cannot be read", () => {
|
|
124
|
+
const viewport = createViewport();
|
|
125
|
+
const target = {
|
|
126
|
+
get x(): number {
|
|
127
|
+
throw new Error("target destroyed");
|
|
128
|
+
},
|
|
129
|
+
get y() {
|
|
130
|
+
return 240;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
applyCameraFollow({
|
|
135
|
+
viewport,
|
|
136
|
+
target,
|
|
137
|
+
smoothMove: false,
|
|
138
|
+
followRevision: 1,
|
|
139
|
+
isCurrentRevision: () => true,
|
|
140
|
+
shouldFollowCamera: () => true,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(viewport.plugins.remove).toHaveBeenCalledWith("animate");
|
|
144
|
+
expect(viewport.plugins.remove).toHaveBeenCalledWith("follow");
|
|
145
|
+
expect(viewport.animate).not.toHaveBeenCalled();
|
|
146
|
+
expect(viewport.follow).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("keeps the follow target readable when the source target is destroyed", () => {
|
|
150
|
+
const viewport = createViewport();
|
|
151
|
+
let position: { x: number; y: number } | null = { x: 120, y: 240 };
|
|
152
|
+
const target = {
|
|
153
|
+
get destroyed() {
|
|
154
|
+
return position === null;
|
|
155
|
+
},
|
|
156
|
+
get x() {
|
|
157
|
+
if (!position) throw new Error("target destroyed");
|
|
158
|
+
return position.x;
|
|
159
|
+
},
|
|
160
|
+
get y() {
|
|
161
|
+
if (!position) throw new Error("target destroyed");
|
|
162
|
+
return position.y;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
applyCameraFollow({
|
|
167
|
+
viewport,
|
|
168
|
+
target,
|
|
169
|
+
smoothMove: false,
|
|
170
|
+
followRevision: 1,
|
|
171
|
+
isCurrentRevision: () => true,
|
|
172
|
+
shouldFollowCamera: () => true,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const [followTarget] = viewport.follow.mock.calls[0];
|
|
176
|
+
expect(followTarget.x).toBe(120);
|
|
177
|
+
expect(followTarget.y).toBe(240);
|
|
178
|
+
|
|
179
|
+
position = { x: 160, y: 280 };
|
|
180
|
+
expect(followTarget.x).toBe(160);
|
|
181
|
+
expect(followTarget.y).toBe(280);
|
|
182
|
+
|
|
183
|
+
position = null;
|
|
184
|
+
expect(followTarget.x).toBe(160);
|
|
185
|
+
expect(followTarget.y).toBe(280);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("does not follow after animation if the target was destroyed", () => {
|
|
189
|
+
const viewport = createViewport();
|
|
190
|
+
let destroyed = false;
|
|
191
|
+
const target = {
|
|
192
|
+
get destroyed() {
|
|
193
|
+
return destroyed;
|
|
194
|
+
},
|
|
195
|
+
x: 120,
|
|
196
|
+
y: 240,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
applyCameraFollow({
|
|
200
|
+
viewport,
|
|
201
|
+
target,
|
|
202
|
+
smoothMove: { time: 800, ease: "easeInOutQuad" },
|
|
203
|
+
followRevision: 2,
|
|
204
|
+
isCurrentRevision: (revision) => revision === 2,
|
|
205
|
+
shouldFollowCamera: () => true,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
destroyed = true;
|
|
209
|
+
const animateOptions = viewport.animate.mock.calls[0][0];
|
|
210
|
+
animateOptions.callbackOnComplete();
|
|
211
|
+
|
|
212
|
+
expect(viewport.follow).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("only lets the sprite that owns the active follow revision clear plugins", () => {
|
|
216
|
+
expect(ownsCameraFollowRevision(2, 2)).toBe(true);
|
|
217
|
+
expect(ownsCameraFollowRevision(2, 3)).toBe(false);
|
|
218
|
+
expect(ownsCameraFollowRevision(null, 2)).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
export const DEFAULT_CAMERA_FOLLOW_TIME = 1000;
|
|
2
|
+
export const DEFAULT_CAMERA_FOLLOW_EASE = "easeInOutSine";
|
|
3
|
+
|
|
4
|
+
export const CAMERA_FOLLOW_EASES = [
|
|
5
|
+
"linear",
|
|
6
|
+
"easeInQuad",
|
|
7
|
+
"easeOutQuad",
|
|
8
|
+
"easeInOutQuad",
|
|
9
|
+
"easeInCubic",
|
|
10
|
+
"easeOutCubic",
|
|
11
|
+
"easeInOutCubic",
|
|
12
|
+
"easeInQuart",
|
|
13
|
+
"easeOutQuart",
|
|
14
|
+
"easeInOutQuart",
|
|
15
|
+
"easeInQuint",
|
|
16
|
+
"easeOutQuint",
|
|
17
|
+
"easeInOutQuint",
|
|
18
|
+
"easeInSine",
|
|
19
|
+
"easeOutSine",
|
|
20
|
+
"easeInOutSine",
|
|
21
|
+
"easeInExpo",
|
|
22
|
+
"easeOutExpo",
|
|
23
|
+
"easeInOutExpo",
|
|
24
|
+
"easeInCirc",
|
|
25
|
+
"easeOutCirc",
|
|
26
|
+
"easeInOutCirc",
|
|
27
|
+
"easeInElastic",
|
|
28
|
+
"easeOutElastic",
|
|
29
|
+
"easeInOutElastic",
|
|
30
|
+
"easeInBack",
|
|
31
|
+
"easeOutBack",
|
|
32
|
+
"easeInOutBack",
|
|
33
|
+
"easeInBounce",
|
|
34
|
+
"easeOutBounce",
|
|
35
|
+
"easeInOutBounce",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
export type CameraFollowEase = typeof CAMERA_FOLLOW_EASES[number];
|
|
39
|
+
|
|
40
|
+
export type CameraFollowSmoothMoveOptions = {
|
|
41
|
+
/** Enable or disable the smooth transition when options are sent as an object. */
|
|
42
|
+
enabled?: boolean;
|
|
43
|
+
/** Duration of the transition to the new camera target, in milliseconds. */
|
|
44
|
+
time?: number;
|
|
45
|
+
/** pixi-viewport easing name, for example "easeInOutQuad". */
|
|
46
|
+
ease?: CameraFollowEase;
|
|
47
|
+
/** Continuous follow speed after the transition. 0 keeps the target centered instantly. */
|
|
48
|
+
speed?: number;
|
|
49
|
+
/** Continuous follow acceleration after the transition. */
|
|
50
|
+
acceleration?: number | null;
|
|
51
|
+
/** Center radius where the followed target can move without moving the viewport. */
|
|
52
|
+
radius?: number | null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type CameraFollowSmoothMove = boolean | CameraFollowSmoothMoveOptions;
|
|
56
|
+
|
|
57
|
+
export type CameraFollowTarget = {
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
destroyed?: boolean;
|
|
61
|
+
} | null | undefined;
|
|
62
|
+
|
|
63
|
+
export type CameraFollowPosition = {
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface CameraFollowApplyContext {
|
|
69
|
+
viewport: any;
|
|
70
|
+
target: CameraFollowTarget;
|
|
71
|
+
smoothMove: CameraFollowSmoothMove;
|
|
72
|
+
followRevision: number;
|
|
73
|
+
isCurrentRevision: (revision: number) => boolean;
|
|
74
|
+
shouldFollowCamera: () => boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const finiteNumber = (value: unknown, fallback: number) => {
|
|
78
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const isCameraFollowEase = (value: unknown): value is CameraFollowEase => {
|
|
82
|
+
return typeof value === "string" && (CAMERA_FOLLOW_EASES as readonly string[]).includes(value);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const smoothMoveEnabled = (smoothMove: CameraFollowSmoothMove) => {
|
|
86
|
+
if (smoothMove === false) return false;
|
|
87
|
+
if (typeof smoothMove === "object" && smoothMove !== null && smoothMove.enabled === false) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const cameraFollowAnimationOptions = (
|
|
94
|
+
smoothMove: CameraFollowSmoothMove
|
|
95
|
+
) => {
|
|
96
|
+
if (!smoothMoveEnabled(smoothMove)) return null;
|
|
97
|
+
const options = typeof smoothMove === "object" && smoothMove !== null ? smoothMove : {};
|
|
98
|
+
return {
|
|
99
|
+
time: Math.max(0, finiteNumber(options.time, DEFAULT_CAMERA_FOLLOW_TIME)),
|
|
100
|
+
ease: isCameraFollowEase(options.ease) ? options.ease : DEFAULT_CAMERA_FOLLOW_EASE,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const cameraFollowOptions = (smoothMove: CameraFollowSmoothMove) => {
|
|
105
|
+
if (typeof smoothMove !== "object" || smoothMove === null) return undefined;
|
|
106
|
+
const options: { speed?: number; acceleration?: number | null; radius?: number | null } = {};
|
|
107
|
+
if (typeof smoothMove.speed === "number" && Number.isFinite(smoothMove.speed)) {
|
|
108
|
+
options.speed = Math.max(0, smoothMove.speed);
|
|
109
|
+
}
|
|
110
|
+
if (typeof smoothMove.acceleration === "number" && Number.isFinite(smoothMove.acceleration)) {
|
|
111
|
+
options.acceleration = Math.max(0, smoothMove.acceleration);
|
|
112
|
+
} else if (smoothMove.acceleration === null) {
|
|
113
|
+
options.acceleration = null;
|
|
114
|
+
}
|
|
115
|
+
if (typeof smoothMove.radius === "number" && Number.isFinite(smoothMove.radius)) {
|
|
116
|
+
options.radius = Math.max(0, smoothMove.radius);
|
|
117
|
+
} else if (smoothMove.radius === null) {
|
|
118
|
+
options.radius = null;
|
|
119
|
+
}
|
|
120
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const clearCameraFollowPlugins = (viewport: any) => {
|
|
124
|
+
viewport?.plugins?.remove?.("animate");
|
|
125
|
+
viewport?.plugins?.remove?.("follow");
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const ownsCameraFollowRevision = (
|
|
129
|
+
appliedRevision: number | null,
|
|
130
|
+
currentRevision: number
|
|
131
|
+
) => {
|
|
132
|
+
return appliedRevision !== null && appliedRevision === currentRevision;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const readCameraFollowPosition = (
|
|
136
|
+
target: CameraFollowTarget
|
|
137
|
+
): CameraFollowPosition | null => {
|
|
138
|
+
if (!target) return null;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
if (target.destroyed) return null;
|
|
142
|
+
const x = target.x;
|
|
143
|
+
const y = target.y;
|
|
144
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
|
|
145
|
+
return { x, y };
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const createCameraFollowTarget = (
|
|
152
|
+
target: CameraFollowTarget,
|
|
153
|
+
initialPosition: CameraFollowPosition
|
|
154
|
+
) => {
|
|
155
|
+
let lastPosition = initialPosition;
|
|
156
|
+
|
|
157
|
+
const readPosition = () => {
|
|
158
|
+
const nextPosition = readCameraFollowPosition(target);
|
|
159
|
+
if (nextPosition) {
|
|
160
|
+
lastPosition = nextPosition;
|
|
161
|
+
}
|
|
162
|
+
return lastPosition;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
get x() {
|
|
167
|
+
return readPosition().x;
|
|
168
|
+
},
|
|
169
|
+
get y() {
|
|
170
|
+
return readPosition().y;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const followCameraInstantly = (
|
|
176
|
+
viewport: any,
|
|
177
|
+
target: CameraFollowTarget,
|
|
178
|
+
smoothMove: CameraFollowSmoothMove
|
|
179
|
+
) => {
|
|
180
|
+
const position = readCameraFollowPosition(target);
|
|
181
|
+
if (!position) return false;
|
|
182
|
+
|
|
183
|
+
const followTarget = createCameraFollowTarget(target, position);
|
|
184
|
+
const followOptions = cameraFollowOptions(smoothMove);
|
|
185
|
+
if (followOptions) {
|
|
186
|
+
viewport.follow(followTarget, followOptions);
|
|
187
|
+
} else {
|
|
188
|
+
viewport.follow(followTarget);
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const applyCameraFollow = ({
|
|
194
|
+
viewport,
|
|
195
|
+
target,
|
|
196
|
+
smoothMove,
|
|
197
|
+
followRevision,
|
|
198
|
+
isCurrentRevision,
|
|
199
|
+
shouldFollowCamera,
|
|
200
|
+
}: CameraFollowApplyContext) => {
|
|
201
|
+
clearCameraFollowPlugins(viewport);
|
|
202
|
+
|
|
203
|
+
const position = readCameraFollowPosition(target);
|
|
204
|
+
if (!position) return false;
|
|
205
|
+
|
|
206
|
+
const animationOptions = cameraFollowAnimationOptions(smoothMove);
|
|
207
|
+
if (!animationOptions || animationOptions.time <= 0) {
|
|
208
|
+
return followCameraInstantly(viewport, target, smoothMove);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
viewport.animate({
|
|
212
|
+
position,
|
|
213
|
+
time: animationOptions.time,
|
|
214
|
+
ease: animationOptions.ease,
|
|
215
|
+
callbackOnComplete: () => {
|
|
216
|
+
if (!isCurrentRevision(followRevision) || !shouldFollowCamera()) return;
|
|
217
|
+
if (!readCameraFollowPosition(target)) return;
|
|
218
|
+
followCameraInstantly(viewport, target, smoothMove);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
return true;
|
|
222
|
+
};
|