@quenty/fakeskybox 11.15.0 → 11.16.0

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 CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [11.16.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/fakeskybox@11.15.0...@quenty/fakeskybox@11.16.0) (2026-04-23)
7
+
8
+
9
+ ### Features
10
+
11
+ * Add fake skybox support ([2c6a1ed](https://github.com/Quenty/NevermoreEngine/commit/2c6a1ed6ed6a4897aab6f9e04e4a3b55b246603d))
12
+ * Additional improvments ([44896ef](https://github.com/Quenty/NevermoreEngine/commit/44896efe8dd9506ad6002bc41f816b9b2b482ebc))
13
+
14
+
15
+
16
+
17
+
6
18
  # [11.15.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/fakeskybox@11.14.0...@quenty/fakeskybox@11.15.0) (2026-02-20)
7
19
 
8
20
  **Note:** Version bump only for package @quenty/fakeskybox
@@ -0,0 +1,10 @@
1
+ {
2
+ "targets": {
3
+ "test": {
4
+ "universeId": 9716264427,
5
+ "placeId": 96018294584015,
6
+ "project": "test/default.project.json",
7
+ "scriptTemplate": "test/scripts/Server/ServerMain.server.lua"
8
+ }
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/fakeskybox",
3
- "version": "11.15.0",
3
+ "version": "11.16.0",
4
4
  "description": "Allow transitions between skyboxes",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -29,12 +29,25 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@quenty/acceltween": "2.7.0",
32
- "@quenty/loader": "10.10.0",
33
- "@quenty/maid": "3.8.0",
34
- "@quenty/signal": "7.12.0"
32
+ "@quenty/baseobject": "10.13.0",
33
+ "@quenty/basicpane": "13.30.0",
34
+ "@quenty/blend": "12.34.0",
35
+ "@quenty/colorsequenceutils": "7.11.0",
36
+ "@quenty/enums": "1.3.0",
37
+ "@quenty/instanceutils": "13.29.0",
38
+ "@quenty/loader": "10.11.0",
39
+ "@quenty/maid": "3.9.0",
40
+ "@quenty/nevermore-test-runner": "1.4.0",
41
+ "@quenty/rx": "13.28.0",
42
+ "@quenty/servicebag": "11.17.0",
43
+ "@quenty/signal": "7.13.0",
44
+ "@quenty/statestack": "14.31.0",
45
+ "@quenty/sunpositionutils": "2.5.0",
46
+ "@quenty/valueobject": "13.30.0",
47
+ "@quentystudios/jest-lua": "3.10.0-quenty.2"
35
48
  },
36
49
  "publishConfig": {
37
50
  "access": "public"
38
51
  },
39
- "gitHead": "305e70aba3395b2f94f500c731327760984aeb4f"
52
+ "gitHead": "9413391da8b6f9026762285b601fd1d37d385a54"
40
53
  }
@@ -0,0 +1,17 @@
1
+ --[=[
2
+ @class FakeSkyboxServiceClient
3
+ ]=]
4
+
5
+ local require = require(script.Parent.loader).load(script)
6
+
7
+ local ServiceBag = require("ServiceBag")
8
+
9
+ local FakeSkyboxServiceClient = {}
10
+ FakeSkyboxServiceClient.ServiceName = "FakeSkyboxServiceClient"
11
+
12
+ function FakeSkyboxServiceClient:Init(serviceBag: ServiceBag.ServiceBag)
13
+ assert(not self._serviceBag, "Already initialized")
14
+ self._serviceBag = assert(serviceBag, "No serviceBag")
15
+ end
16
+
17
+ return FakeSkyboxServiceClient
@@ -0,0 +1,548 @@
1
+ --!strict
2
+ --[=[
3
+ Allow transitions between skyboxes
4
+
5
+ @class FakeSkybox
6
+ ]=]
7
+
8
+ local require = require(script.Parent.loader).load(script)
9
+
10
+ local Lighting = game:GetService("Lighting")
11
+ local Workspace = game:GetService("Workspace")
12
+
13
+ local BasicPane = require("BasicPane")
14
+ local BasicPaneUtils = require("BasicPaneUtils")
15
+ local Blend = require("Blend")
16
+ local FakeSkyboxRenderMethod = require("FakeSkyboxRenderMethod")
17
+ local Observable = require("Observable")
18
+ local Rx = require("Rx")
19
+ local RxInstanceUtils = require("RxInstanceUtils")
20
+ local RxStateStackUtils = require("RxStateStackUtils")
21
+ local SkyboxRenderPart = require("SkyboxRenderPart")
22
+ local SkyboxSide = require("SkyboxSide")
23
+ local SpringObject = require("SpringObject")
24
+ local SunPositionUtils = require("SunPositionUtils")
25
+ local ValueObject = require("ValueObject")
26
+
27
+ local SKYBOX_PROPERTY_IMAGE_MAP = table.freeze({
28
+ [Enum.NormalId.Top] = "SkyboxUp",
29
+ [Enum.NormalId.Bottom] = "SkyboxDn",
30
+
31
+ -- Bind backwards
32
+ [Enum.NormalId.Right] = "SkyboxLf",
33
+ [Enum.NormalId.Left] = "SkyboxRt",
34
+
35
+ [Enum.NormalId.Front] = "SkyboxFt",
36
+ [Enum.NormalId.Back] = "SkyboxBk",
37
+ })
38
+
39
+ local DEFAULT_SKY_DATA = table.freeze({
40
+ SkyboxUp = "rbxasset://textures/sky/sky512_up.tex",
41
+ SkyboxDn = "rbxasset://textures/sky/sky512_dn.tex",
42
+ SkyboxLf = "rbxasset://textures/sky/sky512_lf.tex",
43
+ SkyboxRt = "rbxasset://textures/sky/sky512_rt.tex",
44
+ SkyboxFt = "rbxasset://textures/sky/sky512_ft.tex",
45
+ SkyboxBk = "rbxasset://textures/sky/sky512_bk.tex",
46
+
47
+ -- Defaults
48
+ SunTextureId = "rbxasset://sky/sun.jpg",
49
+ MoonTextureId = "rbxasset://sky/moon.jpg",
50
+ })
51
+
52
+ -- Can't really render the black image propery
53
+ local DECAL_REPLACEMENTS = {
54
+ ["rbxasset://sky/sun.jpg"] = "rbxassetid://6196665106",
55
+ ["rbxasset://sky/moon.jpg"] = "rbxassetid://6444320592",
56
+ }
57
+
58
+ local CELESTRIAL_BODY_OFFSET_STUDS = 10
59
+ local CELESTRIAL_BODY_MAX_DISTANCE_RENDER_HACK = 1000
60
+ local CELESTRIAL_BODY_PART_DEPTH = 1
61
+
62
+ local FakeSkybox = setmetatable({}, BasicPane)
63
+ FakeSkybox.__index = FakeSkybox
64
+ FakeSkybox.ClassName = "FakeSkybox"
65
+
66
+ export type FakeSkybox =
67
+ typeof(setmetatable(
68
+ {} :: {
69
+ Gui: Folder,
70
+ _skyboxWidth: ValueObject.ValueObject<number>,
71
+ _skyboxZOffset: ValueObject.ValueObject<number>,
72
+ _percentVisible: SpringObject.SpringObject<number>,
73
+ _skyValue: ValueObject.ValueObject<Sky?>,
74
+ _atmosphereValue: ValueObject.ValueObject<Atmosphere?>,
75
+ _cameraValue: ValueObject.ValueObject<Camera?>,
76
+ _viewportLightingArgs: ValueObject.ValueObject<SunPositionUtils.ViewportLightingArgs>,
77
+ _renderMethod: ValueObject.ValueObject<FakeSkyboxRenderMethod.FakeSkyboxRenderMethod>,
78
+
79
+ -- Cache
80
+ _observeSkyboxCFrameCache: Observable.Observable<CFrame>?,
81
+ _observeSunPositionDataCache: Observable.Observable<SunPositionUtils.SunPositionData>?,
82
+ _observeSkyboxGradientCache: Observable.Observable<ColorSequence>?,
83
+ },
84
+ {} :: typeof({ __index = FakeSkybox })
85
+ ))
86
+ & BasicPane.BasicPane
87
+
88
+ --[=[
89
+ Constructs a new skybox defaulting to the current lighting + camera.
90
+ ]=]
91
+ function FakeSkybox.new(): FakeSkybox
92
+ local self: FakeSkybox = setmetatable(BasicPane.new() :: any, FakeSkybox)
93
+
94
+ -- State
95
+ self._skyValue = self._maid:Add(ValueObject.new(nil))
96
+ self._atmosphereValue = self._maid:Add(ValueObject.new(nil))
97
+ self._percentVisible = self._maid:Add(SpringObject.new(0, 30))
98
+ self._skyboxWidth = self._maid:Add(ValueObject.new(2000, "number")) -- We want 2,700 but we are limited by max size
99
+ self._skyboxZOffset = self._maid:Add(ValueObject.new(-700, "number")) -- This adds the extra 700
100
+ self._cameraValue = self._maid:Add(ValueObject.new(nil))
101
+ self._viewportLightingArgs =
102
+ self._maid:Add(ValueObject.fromObservable(SunPositionUtils.observeViewportLightingArgs()))
103
+ self._renderMethod =
104
+ self._maid:Add(ValueObject.new(FakeSkyboxRenderMethod.SURFACEGUI :: any, FakeSkyboxRenderMethod:GetInterface()))
105
+
106
+ self._maid:GiveTask(self:ObserveVisible():Subscribe(function(isVisible, doNotAnimate)
107
+ self._percentVisible:SetTarget(isVisible and 1 or 0, doNotAnimate)
108
+ end))
109
+
110
+ self._maid:GiveTask(self:_render():Subscribe(function(gui)
111
+ self.Gui = gui
112
+ end))
113
+
114
+ self:_renderSkyboxSides()
115
+ self:_renderSun()
116
+ self:_renderMoon()
117
+
118
+ -- Set defaults
119
+ self:SetSky(nil)
120
+ self:SetAtmosphere(nil)
121
+ self:SetCamera(nil)
122
+
123
+ return self
124
+ end
125
+
126
+ --[=[
127
+ Set the part size
128
+ ]=]
129
+ function FakeSkybox.SetPartSize(self: FakeSkybox, skyboxWidth: ValueObject.Mountable<number>): ()
130
+ return self._skyboxWidth:Mount(skyboxWidth)
131
+ end
132
+
133
+ --[=[
134
+ Sets the transition speed of the fake skybox
135
+ ]=]
136
+ function FakeSkybox.SetSpeed(self: FakeSkybox, speed: number): ()
137
+ self._percentVisible:SetSpeed(speed)
138
+ end
139
+
140
+ --[=[
141
+ Sets the render method for the skybox parts
142
+ ]=]
143
+ function FakeSkybox.SetRenderMethod(
144
+ self: FakeSkybox,
145
+ renderMethod: ValueObject.Mountable<FakeSkyboxRenderMethod.FakeSkyboxRenderMethod>
146
+ ): ()
147
+ return self._renderMethod:Mount(renderMethod)
148
+ end
149
+
150
+ --[=[
151
+ Sets the skybox
152
+ ]=]
153
+ function FakeSkybox.SetSky(self: FakeSkybox, sky: ValueObject.Mountable<Sky?>?): () -> ()
154
+ return self._skyValue:Mount(sky or self:_observeSkyFromLighting())
155
+ end
156
+
157
+ --[=[
158
+ Sets the atmosphere
159
+ ]=]
160
+ function FakeSkybox.SetAtmosphere(self: FakeSkybox, atmosphere: ValueObject.Mountable<Atmosphere?>?): () -> ()
161
+ return self._atmosphereValue:Mount(atmosphere or self:_observeAtmosphereFromLighting())
162
+ end
163
+
164
+ function FakeSkybox._observeSkyFromLighting(_self: FakeSkybox): Observable.Observable<Sky?>
165
+ return RxInstanceUtils.observeChildrenOfClassBrio(Lighting, "Sky"):Pipe({
166
+ RxStateStackUtils.topOfStack(nil) :: any,
167
+ }) :: any
168
+ end
169
+
170
+ function FakeSkybox._observeAtmosphereFromLighting(_self: FakeSkybox): Observable.Observable<Atmosphere?>
171
+ return RxInstanceUtils.observeChildrenOfClassBrio(Lighting, "Atmosphere"):Pipe({
172
+ RxStateStackUtils.topOfStack(nil) :: any,
173
+ }) :: any
174
+ end
175
+
176
+ function FakeSkybox._observeCurrentCamera(_self: FakeSkybox): Observable.Observable<Camera?>
177
+ return RxInstanceUtils.observeProperty(Workspace, "CurrentCamera") :: any
178
+ end
179
+
180
+ --[=[
181
+ Sets the camera to track to
182
+ ]=]
183
+ function FakeSkybox.SetCamera(self: FakeSkybox, camera: ValueObject.Mountable<Camera?>?): () -> ()
184
+ return self._cameraValue:Mount(camera or self:_observeCurrentCamera())
185
+ end
186
+
187
+ function FakeSkybox.SetViewportLightingArgs(
188
+ self: FakeSkybox,
189
+ viewportLightingArgs: ValueObject.Mountable<SunPositionUtils.ViewportLightingArgs>
190
+ ): () -> ()
191
+ return self._viewportLightingArgs:Mount(viewportLightingArgs)
192
+ end
193
+
194
+ function FakeSkybox._renderSkyboxSides(self: FakeSkybox): ()
195
+ for normalId, propertyName in SKYBOX_PROPERTY_IMAGE_MAP do
196
+ local skyboxSide = self._maid:Add(SkyboxSide.new(normalId))
197
+ self._maid:Add(skyboxSide:SetRenderMethod(self._renderMethod:Observe()))
198
+ self._maid:Add(skyboxSide:SetTransparency(self:_observeTransparency()))
199
+ self._maid:Add(skyboxSide:SetPartSize(self._skyboxWidth:Observe()))
200
+ self._maid:Add(skyboxSide:SetImage(self:_observeImage(propertyName)))
201
+ self._maid:Add(skyboxSide:SetSkyboxCFrame(self:_observeSkyboxCFrame()))
202
+ self._maid:Add(skyboxSide:SetZOffset(self._skyboxZOffset:Observe()))
203
+ -- self._maid:Add(skyboxSide:SetBrightness(self:_observeSkyImageBrightness()))
204
+ self._maid:Add(skyboxSide:SetSkyboxGradient(self:_observeSkyboxGradient()))
205
+
206
+ skyboxSide.Gui.Parent = self.Gui
207
+ end
208
+ end
209
+
210
+ function FakeSkybox._observeTransparency(self: FakeSkybox): Observable.Observable<number>
211
+ return BasicPaneUtils.toTransparency(self._percentVisible:Observe()) :: any
212
+ end
213
+
214
+ function FakeSkybox._renderSun(self: FakeSkybox): ()
215
+ local observeSunState = self:_observeCelestrialBodyState("SunAngularSize")
216
+
217
+ local sunRender = self._maid:Add(SkyboxRenderPart.new())
218
+ sunRender:SetRenderMethod(self._renderMethod:Observe())
219
+ sunRender:SetCanvasSize(Vector2.new(256, 256)) -- TODO: Be smart about this
220
+ self._maid:Add(sunRender:SetBrightness(self:_observeBodyBrightness(observeSunState, function(state)
221
+ return SunPositionUtils.getSunImageBrightness(state.sunPositionData) * 10
222
+ end)))
223
+ self._maid:Add(sunRender:SetTransparency(self:_observeTransparency()))
224
+ self._maid:Add(sunRender:SetImage(self:_observeImage("SunTextureId")))
225
+ self._maid:Add(sunRender:SetSize(self:_observeCelestialBodyPartSize(
226
+ observeSunState,
227
+ Rx.combineLatest({
228
+ atmosphereDensity = self._atmosphereValue:Observe():Pipe({
229
+ Rx.map(function(atmosphere): number?
230
+ if atmosphere then
231
+ return atmosphere.Density
232
+ else
233
+ return nil
234
+ end
235
+ end) :: any,
236
+ }),
237
+ viewportLightingArgs = self._viewportLightingArgs:Observe(),
238
+ }):Pipe({
239
+ Rx.map(function(state: any)
240
+ if not state.atmosphereDensity then
241
+ -- No atmosphere, no size adjustment
242
+ return 1
243
+ end
244
+
245
+ return 1 + (state.atmosphereDensity * state.viewportLightingArgs.environmentDiffuseScale) / 2
246
+ end) :: any,
247
+ }) :: any
248
+ )))
249
+ self._maid:Add(sunRender:SetCFrame(self:_observeCelestrialBodyCFrame(observeSunState, "sunPosition")))
250
+
251
+ sunRender.Gui.Name = "SunPart"
252
+ sunRender.Gui.Parent = self.Gui
253
+ end
254
+
255
+ function FakeSkybox._renderMoon(self: FakeSkybox): ()
256
+ local observeMoonState = self:_observeCelestrialBodyState("MoonAngularSize")
257
+ local observeMoonBrightness = self:_observeBodyBrightness(observeMoonState, function(_state)
258
+ -- local brightness = SunPositionUtils.getMoonImageBrightness(state.sunPositionData, state.environmentDiffuseScale)
259
+ -- return brightness
260
+
261
+ return 1
262
+ end)
263
+
264
+ local moonRender = self._maid:Add(SkyboxRenderPart.new())
265
+ moonRender:SetRenderMethod(self._renderMethod:Observe())
266
+ moonRender:SetCanvasSize(Vector2.new(256, 256)) -- TODO: Be smart about this
267
+ self._maid:Add(moonRender:SetBrightness(observeMoonBrightness))
268
+ self._maid:Add(moonRender:SetTransparency(self:_observeTransparency()))
269
+ self._maid:Add(moonRender:SetImage(self:_observeImage("MoonTextureId")))
270
+ self._maid:Add(moonRender:SetSize(self:_observeCelestialBodyPartSize(observeMoonState)))
271
+ self._maid:Add(moonRender:SetCFrame(self:_observeCelestrialBodyCFrame(observeMoonState, "moonPosition")))
272
+
273
+ moonRender.Gui.Name = "MoonPart"
274
+ moonRender.Gui.Parent = self.Gui
275
+ end
276
+
277
+ function FakeSkybox._observeSkyImageBrightness(self: FakeSkybox): Observable.Observable<number>
278
+ return self:_observeSunPositionData():Pipe({
279
+ Rx.map(function(sunPositionData)
280
+ return math.clamp(SunPositionUtils.getLightSourceBrightness(sunPositionData), 0.3, 1)
281
+ end) :: any,
282
+ }) :: any
283
+ end
284
+
285
+ function FakeSkybox._observeSkyboxGradient(self: FakeSkybox): Observable.Observable<ColorSequence>
286
+ if self._observeSkyboxGradientCache then
287
+ return self._observeSkyboxGradientCache
288
+ end
289
+
290
+ self._observeSkyboxGradientCache = self._viewportLightingArgs:Observe():Pipe({
291
+ Rx.map(function(state: SunPositionUtils.ViewportLightingArgs)
292
+ return SunPositionUtils.getSkyboxGradient(state.clockTime, state.environmentDiffuseScale)
293
+ end) :: any,
294
+ }) :: any
295
+ assert(self._observeSkyboxGradientCache, "Typechecking assertion")
296
+
297
+ return self._observeSkyboxGradientCache
298
+ end
299
+
300
+ function FakeSkybox._observeBodyBrightness(
301
+ _self: FakeSkybox,
302
+ observeCelestrialBodyState: Observable.Observable<CelestrialBodyState>,
303
+ compute: (state: CelestrialBodyState) -> number
304
+ ): Observable.Observable<number>
305
+ return observeCelestrialBodyState:Pipe({
306
+ Rx.map(function(state: any): number
307
+ if not state.bodyAngularSizeDegrees or not state.celestrialBodiesShown then
308
+ return 0
309
+ end
310
+
311
+ return compute(state)
312
+ end) :: any,
313
+ }) :: any
314
+ end
315
+
316
+ type CelestrialBodyState = {
317
+ sunPositionData: SunPositionUtils.SunPositionData,
318
+ viewportLightingArgs: SunPositionUtils.ViewportLightingArgs,
319
+ bodyAngularSizeDegrees: number?,
320
+ skyboxWidth: number,
321
+ celestrialBodiesShown: boolean,
322
+ }
323
+
324
+ function FakeSkybox._observeCelestrialBodyState(
325
+ self: FakeSkybox,
326
+ angularSizeProperty: string
327
+ ): Observable.Observable<CelestrialBodyState>
328
+ return Rx.combineLatest({
329
+ bodyAngularSizeDegrees = self._skyValue:Observe():Pipe({
330
+ Rx.switchMap(function(sky): any
331
+ if sky then
332
+ return RxInstanceUtils.observeProperty(sky, angularSizeProperty)
333
+ else
334
+ return Rx.of(nil)
335
+ end
336
+ end) :: any,
337
+ }),
338
+ viewportLightingArgs = self._viewportLightingArgs:Observe(),
339
+ brightness = RxInstanceUtils.observeProperty(Lighting, "Brightness"),
340
+ skyboxWidth = self._skyboxWidth:Observe(),
341
+ celestrialBodiesShown = self._skyValue:Observe():Pipe({
342
+ Rx.switchMap(function(sky): any
343
+ if sky then
344
+ return RxInstanceUtils.observeProperty(sky, "CelestialBodiesShown")
345
+ else
346
+ return Rx.of(true)
347
+ end
348
+ end) :: any,
349
+ }),
350
+ environmentDiffuseScale = RxInstanceUtils.observeProperty(Lighting, "EnvironmentDiffuseScale"),
351
+ sunPositionData = self:_observeSunPositionData(),
352
+ }):Pipe({
353
+ Rx.cache() :: any,
354
+ }) :: any
355
+ end
356
+
357
+ function FakeSkybox._observeCelestialBodyPartSize(
358
+ self: FakeSkybox,
359
+ observeCelestrialBodyState: Observable.Observable<CelestrialBodyState>,
360
+ observeSizeModifier: Observable.Observable<number>?
361
+ ): Observable.Observable<Vector3>
362
+ return Rx.combineLatest({
363
+ bodyState = observeCelestrialBodyState,
364
+ sizeModifier = observeSizeModifier or 1,
365
+ renderMethod = self._renderMethod:Observe(),
366
+ }):Pipe({
367
+ Rx.map(function(state: any): Vector3?
368
+ if not state.bodyState.bodyAngularSizeDegrees or not state.bodyState.celestrialBodiesShown then
369
+ return Vector3.zero
370
+ end
371
+
372
+ local celestrialBodiesRadius =
373
+ self:_getCelestrialBodiesRenderDistance(state.renderMethod, state.bodyState.skyboxWidth)
374
+ local celestrialBodyRadius = math.tan(math.rad(state.bodyState.bodyAngularSizeDegrees) / 2)
375
+ * celestrialBodiesRadius
376
+ local diameter = celestrialBodyRadius * 2
377
+
378
+ diameter *= state.sizeModifier
379
+
380
+ return Vector3.new(diameter, diameter, CELESTRIAL_BODY_PART_DEPTH)
381
+ end) :: any,
382
+ }) :: any
383
+ end
384
+
385
+ function FakeSkybox._getCelestrialBodiesRenderDistance(
386
+ _self: FakeSkybox,
387
+ renderMethod: FakeSkyboxRenderMethod.FakeSkyboxRenderMethod,
388
+ skyboxWidth: number
389
+ ): number
390
+ assert(FakeSkyboxRenderMethod:IsValue(renderMethod))
391
+
392
+ local radius = ((skyboxWidth / 2) - CELESTRIAL_BODY_OFFSET_STUDS)
393
+ if renderMethod == FakeSkyboxRenderMethod.DECAL then
394
+ -- Decals are rendered at a max distance, so we need to clamp the radius to avoid them disappearing
395
+ radius = radius / 4
396
+ end
397
+
398
+ return math.clamp(radius, 0, CELESTRIAL_BODY_MAX_DISTANCE_RENDER_HACK)
399
+ end
400
+
401
+ function FakeSkybox.ObserveBrightness(self: FakeSkybox): Observable.Observable<number>
402
+ return self:_observeSkyImageBrightness()
403
+ end
404
+
405
+ function FakeSkybox._observeCelestrialBodyCFrame(
406
+ self: FakeSkybox,
407
+ observeCelestrialBodyState: Observable.Observable<CelestrialBodyState>,
408
+ positionType: "sunPosition" | "moonPosition"
409
+ ): Observable.Observable<CFrame>
410
+ return Rx.combineLatest({
411
+ bodyState = observeCelestrialBodyState,
412
+ renderMethod = self._renderMethod:Observe(),
413
+ celestrialSkyboxCFrame = self:_observeCelestrialSkyCFrame(),
414
+ }):Pipe({
415
+ Rx.map(function(state: any): CFrame
416
+ if not state.bodyState.bodyAngularSizeDegrees or not state.bodyState.celestrialBodiesShown then
417
+ return CFrame.identity
418
+ end
419
+
420
+ local celestrialBodiesRadius =
421
+ self:_getCelestrialBodiesRenderDistance(state.renderMethod, state.bodyState.skyboxWidth)
422
+ local direction = state.bodyState.sunPositionData[positionType].Unit
423
+
424
+ local cframe: CFrame = state.celestrialSkyboxCFrame
425
+ * CFrame.lookAt(Vector3.zero, direction, Vector3.yAxis)
426
+ * CFrame.new(0, 0, -celestrialBodiesRadius - CELESTRIAL_BODY_PART_DEPTH / 2)
427
+ * CFrame.Angles(0, math.pi, 0)
428
+
429
+ return cframe
430
+ end) :: any,
431
+ }) :: any
432
+ end
433
+
434
+ function FakeSkybox._observeSunPositionData(self: FakeSkybox): Observable.Observable<SunPositionUtils.SunPositionData>
435
+ if self._observeSunPositionDataCache then
436
+ return self._observeSunPositionDataCache
437
+ end
438
+
439
+ self._observeSunPositionDataCache = Rx.combineLatest({
440
+ clockTime = RxInstanceUtils.observeProperty(Lighting, "ClockTime"),
441
+ geoLatitude = RxInstanceUtils.observeProperty(Lighting, "GeographicLatitude"),
442
+ }):Pipe({
443
+ Rx.map(function(state: any): SunPositionUtils.SunPositionData
444
+ return SunPositionUtils.getSunPositionData(state.clockTime, state.geoLatitude)
445
+ end) :: any,
446
+ Rx.cache() :: any,
447
+ }) :: any
448
+ assert(self._observeSunPositionDataCache, "Typechecking assertion")
449
+
450
+ return self._observeSunPositionDataCache
451
+ end
452
+
453
+ function FakeSkybox._observeImage(self: FakeSkybox, propertyName: string): Observable.Observable<string>
454
+ local default = DEFAULT_SKY_DATA[propertyName]
455
+ if not default then
456
+ error("[FakeSkybox] - No default for property: " .. propertyName)
457
+ end
458
+
459
+ return Rx.combineLatest({
460
+ renderMethod = self._renderMethod:Observe(),
461
+ imageValue = self._skyValue:Observe():Pipe({
462
+ Rx.switchMap(function(sky): any
463
+ if sky then
464
+ return RxInstanceUtils.observeProperty(sky, propertyName):Pipe({
465
+ Rx.map(function(value)
466
+ return value or default
467
+ end) :: any,
468
+ })
469
+ else
470
+ return Rx.of(default)
471
+ end
472
+ end) :: any,
473
+ }),
474
+ }):Pipe({
475
+ Rx.map(function(state)
476
+ if state.renderMethod == FakeSkyboxRenderMethod.DECAL and state.imageValue then
477
+ -- Decal mode doesn't support the sun/moon textures, so we replace them with a solid color texture
478
+ return DECAL_REPLACEMENTS[state.imageValue] or state.imageValue
479
+ else
480
+ return state.imageValue
481
+ end
482
+ end) :: any,
483
+ }) :: any
484
+ end
485
+
486
+ function FakeSkybox._observeCelestrialSkyCFrame(self: FakeSkybox): Observable.Observable<CFrame>
487
+ return self._cameraValue:Observe():Pipe({
488
+ Rx.switchMap(function(camera): any
489
+ if camera then
490
+ return RxInstanceUtils.observeProperty(camera, "CFrame")
491
+ else
492
+ return Rx.EMPTY
493
+ end
494
+ end) :: any,
495
+ Rx.map(function(cameraCFrame)
496
+ if cameraCFrame then
497
+ return CFrame.new(cameraCFrame.Position)
498
+ else
499
+ return CFrame.identity
500
+ end
501
+ end) :: any,
502
+ }) :: any
503
+ end
504
+
505
+ function FakeSkybox._observeSkyboxCFrame(self: FakeSkybox): Observable.Observable<CFrame>
506
+ if self._observeSkyboxCFrameCache then
507
+ return self._observeSkyboxCFrameCache
508
+ end
509
+
510
+ self._observeSkyboxCFrameCache = Rx.combineLatest({
511
+ cameraCFrame = self._cameraValue:Observe():Pipe({
512
+ Rx.switchMap(function(camera): any
513
+ if camera then
514
+ return RxInstanceUtils.observeProperty(camera, "CFrame")
515
+ else
516
+ return Rx.EMPTY
517
+ end
518
+ end) :: any,
519
+ }),
520
+ skyboxOrientation = self._skyValue:Observe():Pipe({
521
+ Rx.switchMap(function(sky): any
522
+ if sky then
523
+ return RxInstanceUtils.observeProperty(sky, "SkyboxOrientation")
524
+ else
525
+ return Rx.of(Vector3.zero)
526
+ end
527
+ end) :: any,
528
+ }),
529
+ }):Pipe({
530
+ Rx.map(function(state)
531
+ local orientation =
532
+ CFrame.Angles(state.skyboxOrientation.X, state.skyboxOrientation.Y, state.skyboxOrientation.Z)
533
+ return CFrame.new(state.cameraCFrame.Position) * orientation
534
+ end) :: any,
535
+ Rx.cache() :: any,
536
+ }) :: any
537
+ assert(self._observeSkyboxCFrameCache, "Typechecking assertion")
538
+
539
+ return self._observeSkyboxCFrameCache
540
+ end
541
+
542
+ function FakeSkybox:_render(): any
543
+ return Blend.New "Folder" {
544
+ Name = "Skybox",
545
+ }
546
+ end
547
+
548
+ return FakeSkybox
@@ -0,0 +1,27 @@
1
+ --!nonstrict
2
+ --[[
3
+ @class FakeSkybox.spec.lua
4
+ ]]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local FakeSkybox = require("FakeSkybox")
9
+ local Jest = require("Jest")
10
+
11
+ local describe = Jest.Globals.describe
12
+ local expect = Jest.Globals.expect
13
+ local it = Jest.Globals.it
14
+
15
+ describe("FakeSkybox", function()
16
+ it("should be requireable", function()
17
+ expect(FakeSkybox).never.toBeNil()
18
+ end)
19
+
20
+ it("should have ClassName set to FakeSkybox", function()
21
+ expect(FakeSkybox.ClassName).toEqual("FakeSkybox")
22
+ end)
23
+
24
+ it("should have a new constructor", function()
25
+ expect(typeof(FakeSkybox.new)).toEqual("function")
26
+ end)
27
+ end)
@@ -0,0 +1,15 @@
1
+ --!strict
2
+ --[=[
3
+ @class FakeSkyboxRenderMethod
4
+ ]=]
5
+
6
+ local require = require(script.Parent.loader).load(script)
7
+
8
+ local SimpleEnum = require("SimpleEnum")
9
+
10
+ export type FakeSkyboxRenderMethod = "surfacegui" | "decal"
11
+
12
+ return SimpleEnum.new({
13
+ SURFACEGUI = "surfacegui" :: "surfacegui",
14
+ DECAL = "decal" :: "decal",
15
+ })