Avatar and 3d thumbnail changes.

- Fixed 3d view causing a rerender on resize.
- Avatar editor page.
- Made avatar editor page mobile-friendly.
- Fixed bug on shop where thumbnails would persist across pages.
- Avatar editor CSS changes. Added a class for the shop item card.
- Avatar editor api.
- Updated 2d/3d thumbnail JS to support the 2d asset/user thumbnail api.
- Fixed some rate limiters to not be global.
- Tightened up the rate limit on the maintenance bypass api to 20 requests with a decay of 30 minutes.
- APIs for avatar editor. This is for wearing/removing/listing items.
- Ability to delete files from the CDN.
- BodyColors/CharacterAppearance client endpoints.
- Wearable assets.
- BodyColors XML in grid storage folder for use by the BodyColors endpoint.
- Owned/worn asset on sign up configurable.
- Models for outfits. They are not finished at this moment in time.
This commit is contained in:
Graphictoria 2023-01-16 22:07:11 -05:00
parent d9ca6b4785
commit 131ff27343
32 changed files with 3031 additions and 112 deletions

View File

@ -41,6 +41,19 @@ class AssetHelper
if(!$marketplaceResult->ok() || !$assetResult->ok())
return false;
$assetTypeId = $marketplaceResult['AssetTypeId'];
if(
$assetTypeId == 41 || // Hair Accessory
$assetTypeId == 42 || // Face Accessory
$assetTypeId == 43 || // Neck Accessory
$assetTypeId == 44 || // Shoulder Accessory
$assetTypeId == 45 || // Front Accessory
$assetTypeId == 46 || // Back Accessory
$assetTypeId == 47 // Waist Accessory
) {
$assetTypeId = 8;
}
$assetContent = Http::get($assetResult['locations'][0]['location']);
$hash = CdnHelper::SaveContent($assetContent->body(), $assetContent->header('Content-Type'));
$asset = Asset::create([
@ -50,7 +63,7 @@ class AssetHelper
'approved' => true,
'priceInTokens' => $marketplaceResult['PriceInRobux'] ?: 0,
'onSale' => $marketplaceResult['IsForSale'],
'assetTypeId' => $marketplaceResult['AssetTypeId'],
'assetTypeId' => $assetTypeId,
'assetVersionId' => 0
]);
$assetVersion = AssetVersion::create([
@ -82,6 +95,19 @@ class AssetHelper
if(!$marketplaceResult->ok())
return false;
$assetTypeId = $marketplaceResult['AssetTypeId'];
if(
$assetTypeId == 41 || // Hair Accessory
$assetTypeId == 42 || // Face Accessory
$assetTypeId == 43 || // Neck Accessory
$assetTypeId == 44 || // Shoulder Accessory
$assetTypeId == 45 || // Front Accessory
$assetTypeId == 46 || // Back Accessory
$assetTypeId == 47 // Waist Accessory
) {
$assetTypeId = 8;
}
$hash = CdnHelper::SaveContentB64($b64Content, 'application/octet-stream');
$asset = Asset::create([
'creatorId' => ($uploadToHolder ? 1 : Auth::user()->id),
@ -90,7 +116,7 @@ class AssetHelper
'approved' => true,
'priceInTokens' => $marketplaceResult['PriceInRobux'] ?: 0,
'onSale' => $marketplaceResult['IsForSale'],
'assetTypeId' => $marketplaceResult['AssetTypeId'],
'assetTypeId' => $assetTypeId,
'assetVersionId' => 0
]);
$assetVersion = AssetVersion::create([

View File

@ -0,0 +1,851 @@
<?php
/*
XlXi 2023
Brick color helper.
*/
namespace App\Helpers;
class BrickColorHelper
{
private static $brickColors = [
1 => [
'Color' => [242, 243, 243],
'Name' => 'White'
],
2 => [
'Color' => [161, 165, 162],
'Name' => 'Grey'
],
3 => [
'Color' => [249, 233, 153],
'Name' => 'Light yellow'
],
5 => [
'Color' => [215, 197, 154],
'Name' => 'Brick yellow'
],
6 => [
'Color' => [194, 218, 184],
'Name' => 'Light green (Mint)'
],
9 => [
'Color' => [232, 186, 200],
'Name' => 'Light reddish violet'
],
11 => [
'Color' => [0x80, 0xbb, 0xdb],
'Name' => 'Pastel Blue'
],
12 => [
'Color' => [203, 132, 66],
'Name' => 'Light orange brown'
],
18 => [
'Color' => [204, 142, 105],
'Name' => 'Nougat'
],
21 => [
'Color' => [196, 40, 28],
'Name' => 'Bright red'
],
22 => [
'Color' => [196, 112, 160],
'Name' => 'Med. reddish violet'
],
23 => [
'Color' => [13, 105, 172],
'Name' => 'Bright blue'
],
24 => [
'Color' => [245, 205, 48],
'Name' => 'Bright yellow'
],
25 => [
'Color' => [98, 71, 50],
'Name' => 'Earth orange'
],
26 => [
'Color' => [27, 42, 53],
'Name' => 'Black'
],
27 => [
'Color' => [109, 110, 108],
'Name' => 'Dark grey'
],
28 => [
'Color' => [40, 127, 71],
'Name' => 'Dark green'
],
29 => [
'Color' => [161, 196, 140],
'Name' => 'Medium green'
],
36 => [
'Color' => [243, 207, 155],
'Name' => 'Lig. Yellowich orange'
],
37 => [
'Color' => [75, 151, 75],
'Name' => 'Bright green'
],
38 => [
'Color' => [160, 95, 53],
'Name' => 'Dark orange'
],
39 => [
'Color' => [193, 202, 222],
'Name' => 'Light bluish violet'
],
40 => [
'Color' => [236, 236, 236],
'Name' => 'Transparent'
],
41 => [
'Color' => [205, 84, 75],
'Name' => 'Tr. Red'
],
42 => [
'Color' => [193, 223, 240],
'Name' => 'Tr. Lg blue'
],
43 => [
'Color' => [123, 182, 232],
'Name' => 'Tr. Blue'
],
44 => [
'Color' => [247, 241, 141],
'Name' => 'Tr. Yellow'
],
45 => [
'Color' => [180, 210, 228],
'Name' => 'Light blue'
],
47 => [
'Color' => [217, 133, 108],
'Name' => 'Tr. Flu. Reddish orange'
],
48 => [
'Color' => [132, 182, 141],
'Name' => 'Tr. Green'
],
49 => [
'Color' => [248, 241, 132],
'Name' => 'Tr. Flu. Green'
],
50 => [
'Color' => [236, 232, 222],
'Name' => 'Phosph. White'
],
100 => [
'Color' => [238, 196, 182],
'Name' => 'Light red'
],
101 => [
'Color' => [218, 134, 122],
'Name' => 'Medium red'
],
102 => [
'Color' => [110, 153, 202],
'Name' => 'Medium blue'
],
103 => [
'Color' => [199, 193, 183],
'Name' => 'Light grey'
],
104 => [
'Color' => [107, 50, 124],
'Name' => 'Bright violet'
],
105 => [
'Color' => [226, 155, 64],
'Name' => 'Br. yellowish orange'
],
106 => [
'Color' => [218, 133, 65],
'Name' => 'Bright orange'
],
107 => [
'Color' => [0, 143, 156],
'Name' => 'Bright bluish green'
],
108 => [
'Color' => [104, 92, 67],
'Name' => 'Earth yellow'
],
110 => [
'Color' => [67, 84, 147],
'Name' => 'Bright bluish violet'
],
111 => [
'Color' => [191, 183, 177],
'Name' => 'Tr. Brown'
],
112 => [
'Color' => [104, 116, 172],
'Name' => 'Medium bluish violet'
],
113 => [
'Color' => [228, 173, 200],
'Name' => 'Tr. Medi. reddish violet'
],
115 => [
'Color' => [199, 210, 60],
'Name' => 'Med. yellowish green'
],
116 => [
'Color' => [85, 165, 175],
'Name' => 'Med. bluish green'
],
118 => [
'Color' => [183, 215, 213],
'Name' => 'Light bluish green'
],
119 => [
'Color' => [164, 189, 71],
'Name' => 'Br. yellowish green'
],
120 => [
'Color' => [217, 228, 167],
'Name' => 'Lig. yellowish green'
],
121 => [
'Color' => [231, 172, 88],
'Name' => 'Med. yellowish orange'
],
123 => [
'Color' => [211, 111, 76],
'Name' => 'Br. reddish orange'
],
124 => [
'Color' => [146, 57, 120],
'Name' => 'Bright reddish violet'
],
125 => [
'Color' => [234, 184, 146],
'Name' => 'Light orange'
],
126 => [
'Color' => [165, 165, 203],
'Name' => 'Tr. Bright bluish violet'
],
127 => [
'Color' => [220, 188, 129],
'Name' => 'Gold'
],
128 => [
'Color' => [174, 122, 89],
'Name' => 'Dark nougat'
],
131 => [
'Color' => [156, 163, 168],
'Name' => 'Silver'
],
133 => [
'Color' => [213, 115, 61],
'Name' => 'Neon orange'
],
134 => [
'Color' => [216, 221, 86],
'Name' => 'Neon green'
],
135 => [
'Color' => [116, 134, 157],
'Name' => 'Sand blue'
],
136 => [
'Color' => [135, 124, 144],
'Name' => 'Sand violet'
],
137 => [
'Color' => [224, 152, 100],
'Name' => 'Medium orange'
],
138 => [
'Color' => [149, 138, 115],
'Name' => 'Sand yellow'
],
140 => [
'Color' => [32, 58, 86],
'Name' => 'Earth blue'
],
141 => [
'Color' => [39, 70, 45],
'Name' => 'Earth green'
],
143 => [
'Color' => [207, 226, 247],
'Name' => 'Tr. Flu. Blue'
],
145 => [
'Color' => [121, 136, 161],
'Name' => 'Sand blue metallic'
],
146 => [
'Color' => [149, 142, 163],
'Name' => 'Sand violet metallic'
],
147 => [
'Color' => [147, 135, 103],
'Name' => 'Sand yellow metallic'
],
148 => [
'Color' => [87, 88, 87],
'Name' => 'Dark grey metallic'
],
149 => [
'Color' => [22, 29, 50],
'Name' => 'Black metallic'
],
150 => [
'Color' => [171, 173, 172],
'Name' => 'Light grey metallic'
],
151 => [
'Color' => [120, 144, 130],
'Name' => 'Sand green'
],
153 => [
'Color' => [149, 121, 119],
'Name' => 'Sand red'
],
154 => [
'Color' => [123, 46, 47],
'Name' => 'Dark red'
],
157 => [
'Color' => [255, 246, 123],
'Name' => 'Tr. Flu. Yellow'
],
158 => [
'Color' => [225, 164, 194],
'Name' => 'Tr. Flu. Red'
],
168 => [
'Color' => [117, 108, 98],
'Name' => 'Gun metallic'
],
176 => [
'Color' => [151, 105, 91],
'Name' => 'Red flip/flop'
],
178 => [
'Color' => [180, 132, 85],
'Name' => 'Yellow flip/flop'
],
179 => [
'Color' => [137, 135, 136],
'Name' => 'Silver flip/flop'
],
180 => [
'Color' => [215, 169, 75],
'Name' => 'Curry'
],
190 => [
'Color' => [249, 214, 46],
'Name' => 'Fire Yellow'
],
191 => [
'Color' => [232, 171, 45],
'Name' => 'Flame yellowish orange'
],
192 => [
'Color' => [105, 64, 40],
'Name' => 'Reddish brown'
],
193 => [
'Color' => [207, 96, 36],
'Name' => 'Flame reddish orange'
],
194 => [
'Color' => [163, 162, 165],
'Name' => 'Medium stone grey'
],
195 => [
'Color' => [70, 103, 164],
'Name' => 'Royal blue'
],
196 => [
'Color' => [35, 71, 139],
'Name' => 'Dark Royal blue'
],
198 => [
'Color' => [142, 66, 133],
'Name' => 'Bright reddish lilac'
],
199 => [
'Color' => [99, 95, 98],
'Name' => 'Dark stone grey'
],
200 => [
'Color' => [130, 138, 93],
'Name' => 'Lemon metalic'
],
208 => [
'Color' => [229, 228, 223],
'Name' => 'Light stone grey'
],
209 => [
'Color' => [176, 142, 68],
'Name' => 'Dark Curry'
],
210 => [
'Color' => [112, 149, 120],
'Name' => 'Faded green'
],
211 => [
'Color' => [121, 181, 181],
'Name' => 'Turquoise'
],
212 => [
'Color' => [159, 195, 233],
'Name' => 'Light Royal blue'
],
213 => [
'Color' => [108, 129, 183],
'Name' => 'Medium Royal blue'
],
216 => [
'Color' => [143, 76, 42],
'Name' => 'Rust'
],
217 => [
'Color' => [124, 92, 70],
'Name' => 'Brown'
],
218 => [
'Color' => [150, 112, 159],
'Name' => 'Reddish lilac'
],
219 => [
'Color' => [107, 98, 155],
'Name' => 'Lilac'
],
220 => [
'Color' => [167, 169, 206],
'Name' => 'Light lilac'
],
221 => [
'Color' => [205, 98, 152],
'Name' => 'Bright purple'
],
222 => [
'Color' => [228, 173, 200],
'Name' => 'Light purple'
],
223 => [
'Color' => [220, 144, 149],
'Name' => 'Light pink'
],
224 => [
'Color' => [240, 213, 160],
'Name' => 'Light brick yellow'
],
225 => [
'Color' => [235, 184, 127],
'Name' => 'Warm yellowish orange'
],
226 => [
'Color' => [253, 234, 141],
'Name' => 'Cool yellow'
],
232 => [
'Color' => [125, 187, 221],
'Name' => 'Dove blue'
],
268 => [
'Color' => [52, 43, 117],
'Name' => 'Medium lilac'
],
301 => [
'Color' => [80, 109, 84],
'Name' => 'Slime green'
],
302 => [
'Color' => [91, 93, 105],
'Name' => 'Smoky grey'
],
303 => [
'Color' => [0, 16, 176],
'Name' => 'Dark blue'
],
304 => [
'Color' => [44, 101, 29],
'Name' => 'Parsley green'
],
305 => [
'Color' => [82, 124, 174],
'Name' => 'Steel blue'
],
306 => [
'Color' => [51, 88, 130],
'Name' => 'Storm blue'
],
307 => [
'Color' => [16, 42, 220],
'Name' => 'Lapis'
],
308 => [
'Color' => [61, 21, 133],
'Name' => 'Dark indigo'
],
309 => [
'Color' => [52, 142, 64],
'Name' => 'Sea green'
],
310 => [
'Color' => [91, 154, 76],
'Name' => 'Shamrock'
],
311 => [
'Color' => [159, 161, 172],
'Name' => 'Fossil'
],
312 => [
'Color' => [89, 34, 89],
'Name' => 'Mulberry'
],
313 => [
'Color' => [31, 128, 29],
'Name' => 'Forest green'
],
314 => [
'Color' => [159, 173, 192],
'Name' => 'Cadet blue'
],
315 => [
'Color' => [9, 137, 207],
'Name' => 'Electric blue'
],
316 => [
'Color' => [123, 0, 123],
'Name' => 'Eggplant'
],
317 => [
'Color' => [124, 156, 107],
'Name' => 'Moss'
],
318 => [
'Color' => [138, 171, 133],
'Name' => 'Artichoke'
],
319 => [
'Color' => [185, 196, 177],
'Name' => 'Sage green'
],
320 => [
'Color' => [202, 203, 209],
'Name' => 'Ghost grey'
],
321 => [
'Color' => [167, 94, 155],
'Name' => 'Lilac'
],
322 => [
'Color' => [123, 47, 123],
'Name' => 'Plum'
],
323 => [
'Color' => [148, 190, 129],
'Name' => 'Olivine'
],
324 => [
'Color' => [168, 189, 153],
'Name' => 'Laurel green'
],
325 => [
'Color' => [223, 223, 222],
'Name' => 'Quill grey'
],
327 => [
'Color' => [151, 0, 0],
'Name' => 'Crimson'
],
328 => [
'Color' => [177, 229, 166],
'Name' => 'Mint'
],
329 => [
'Color' => [152, 194, 219],
'Name' => 'Baby blue'
],
330 => [
'Color' => [255, 152, 220],
'Name' => 'Carnation pink'
],
331 => [
'Color' => [255, 89, 89],
'Name' => 'Persimmon'
],
332 => [
'Color' => [117, 0, 0],
'Name' => 'Maroon'
],
333 => [
'Color' => [239, 184, 56],
'Name' => 'Gold'
],
334 => [
'Color' => [248, 217, 109],
'Name' => 'Daisy orange'
],
335 => [
'Color' => [231, 231, 236],
'Name' => 'Pearl'
],
336 => [
'Color' => [199, 212, 228],
'Name' => 'Fog'
],
337 => [
'Color' => [255, 148, 148],
'Name' => 'Salmon'
],
338 => [
'Color' => [190, 104, 98],
'Name' => 'Terra Cotta'
],
339 => [
'Color' => [86, 36, 36],
'Name' => 'Cocoa'
],
340 => [
'Color' => [241, 231, 199],
'Name' => 'Wheat'
],
341 => [
'Color' => [254, 243, 187],
'Name' => 'Buttermilk'
],
342 => [
'Color' => [224, 178, 208],
'Name' => 'Mauve'
],
343 => [
'Color' => [212, 144, 189],
'Name' => 'Sunrise'
],
344 => [
'Color' => [150, 85, 85],
'Name' => 'Tawny'
],
345 => [
'Color' => [143, 76, 42],
'Name' => 'Rust'
],
346 => [
'Color' => [211, 190, 150],
'Name' => 'Cashmere'
],
347 => [
'Color' => [226, 220, 188],
'Name' => 'Khaki'
],
348 => [
'Color' => [237, 234, 234],
'Name' => 'Lily white'
],
349 => [
'Color' => [233, 218, 218],
'Name' => 'Seashell'
],
350 => [
'Color' => [136, 62, 62],
'Name' => 'Burgundy'
],
351 => [
'Color' => [188, 155, 93],
'Name' => 'Cork'
],
352 => [
'Color' => [199, 172, 120],
'Name' => 'Burlap'
],
353 => [
'Color' => [202, 191, 163],
'Name' => 'Beige'
],
354 => [
'Color' => [187, 179, 178],
'Name' => 'Oyster'
],
355 => [
'Color' => [108, 88, 75],
'Name' => 'Pine Cone'
],
356 => [
'Color' => [160, 132, 79],
'Name' => 'Fawn brown'
],
357 => [
'Color' => [149, 137, 136],
'Name' => 'Hurricane grey'
],
358 => [
'Color' => [171, 168, 158],
'Name' => 'Cloudy grey'
],
359 => [
'Color' => [175, 148, 131],
'Name' => 'Linen'
],
360 => [
'Color' => [150, 103, 102],
'Name' => 'Copper'
],
361 => [
'Color' => [86, 66, 54],
'Name' => 'Dirt brown'
],
362 => [
'Color' => [126, 104, 63],
'Name' => 'Bronze'
],
363 => [
'Color' => [105, 102, 92],
'Name' => 'Flint'
],
364 => [
'Color' => [90, 76, 66],
'Name' => 'Dark taupe'
],
365 => [
'Color' => [106, 57, 9],
'Name' => 'Burnt Sienna'
],
1001 => [
'Color' => [248, 248, 248],
'Name' => 'Institutional white'
],
1002 => [
'Color' => [205, 205, 205],
'Name' => 'Mid gray'
],
1003 => [
'Color' => [17, 17, 17],
'Name' => 'Really black'
],
1004 => [
'Color' => [255, 0, 0],
'Name' => 'Really red'
],
1005 => [
'Color' => [255, 175, 0],
'Name' => 'Deep orange'
],
1006 => [
'Color' => [180, 128, 255],
'Name' => 'Alder'
],
1007 => [
'Color' => [163, 75, 75],
'Name' => 'Dusty Rose'
],
1008 => [
'Color' => [193, 190, 66],
'Name' => 'Olive'
],
1009 => [
'Color' => [255, 255, 0],
'Name' => 'New Yeller'
],
1010 => [
'Color' => [0, 0, 255],
'Name' => 'Really blue'
],
1011 => [
'Color' => [0, 32, 96],
'Name' => 'Navy blue'
],
1012 => [
'Color' => [33, 84, 185],
'Name' => 'Deep blue'
],
1013 => [
'Color' => [4, 175, 236],
'Name' => 'Cyan'
],
1014 => [
'Color' => [170, 85, 0],
'Name' => 'CGA brown'
],
1015 => [
'Color' => [170, 0, 170],
'Name' => 'Magenta'
],
1016 => [
'Color' => [255, 102, 204],
'Name' => 'Pink'
],
1017 => [
'Color' => [255, 175, 0],
'Name' => 'Deep orange'
],
1018 => [
'Color' => [18, 238, 212],
'Name' => 'Teal'
],
1019 => [
'Color' => [0, 255, 255],
'Name' => 'Toothpaste'
],
1020 => [
'Color' => [0, 255, 0],
'Name' => 'Lime green'
],
1021 => [
'Color' => [58, 125, 21],
'Name' => 'Camo'
],
1022 => [
'Color' => [127, 142, 100],
'Name' => 'Grime'
],
1023 => [
'Color' => [140, 91, 159],
'Name' => 'Lavender'
],
1024 => [
'Color' => [175, 221, 255],
'Name' => 'Pastel light blue'
],
1025 => [
'Color' => [255, 201, 201],
'Name' => 'Pastel orange'
],
1026 => [
'Color' => [177, 167, 255],
'Name' => 'Pastel violet'
],
1027 => [
'Color' => [159, 243, 233],
'Name' => 'Pastel blue-green'
],
1028 => [
'Color' => [204, 255, 204],
'Name' => 'Pastel green'
],
1029 => [
'Color' => [255, 255, 204],
'Name' => 'Pastel yellow'
],
1030 => [
'Color' => [255, 204, 153],
'Name' => 'Pastel brown'
],
1031 => [
'Color' => [98, 37, 209],
'Name' => 'Royal purple'
],
1032 => [
'Color' => [255, 0, 191],
'Name' => 'Hot pink'
]
];
public static function IsValidColor($colorId)
{
return array_key_exists($colorId, self::$brickColors);
}
}

View File

@ -44,6 +44,21 @@ class CdnHelper
return $hash;
}
public static function Delete($hash)
{
$disk = self::GetDisk();
$cdnHash = CdnHash::where('hash', $hash);
if($disk->exists($hash) && $cdnHash->exists()) {
$cdnHash->delete();
$disk->delete($hash);
return true;
}
return false;
}
public static function SaveContentB64($contentB64, $mime)
{
return self::SaveContent(base64_decode($contentB64), $mime);

View File

@ -123,7 +123,7 @@ class GridHelper
public static function getDefaultThumbnail($fileName)
{
$disk = $self::getThumbDisk();
$disk = self::getThumbDisk();
if(!$disk->exists($fileName))
throw new Exception('Unable to locate template file.');
@ -133,7 +133,26 @@ class GridHelper
public static function getUnknownThumbnail()
{
return $self::getDefaultThumbnail('UnknownThumbnail.png');
return self::getDefaultThumbnail('UnknownThumbnail.png');
}
private static function getGameDisk()
{
return Storage::build([
'driver' => 'local',
'root' => storage_path('app/grid/game'),
]);
}
public static function getBodyColorsXML()
{
$disk = self::getGameDisk();
$fileName = 'BodyColors.xml';
if(!$disk->exists($fileName))
throw new Exception('Unable to locate template file.');
return $disk->get($fileName);
}
public static function getArbiter($name)

View File

@ -0,0 +1,263 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Support\Str;
use App\Helpers\BrickColorHelper;
use App\Helpers\ValidationHelper;
use App\Http\Controllers\Controller;
use App\Models\AvatarAsset;
use App\Models\AvatarColor;
use App\Models\UserAsset;
class AvatarController extends Controller
{
protected $validAssetTypeIds = [
'17', // Heads
'18', // Faces
'8', // Hats
'2', // T-Shirts
'11', // Shirts
'12', // Pants
'19', // Gear
'27', // Torsos
'29', // Left Arms
'28', // Right Arms
'30', // Left Legs
'31', // Right Legs
'32' // Packages
];
public function redrawUser()
{
Auth::user()->redraw();
return response(['success' => true]);
}
public static function GetUserAssets($userId)
{
return UserAsset::where('owner_id', $userId)
->whereRelation('asset', 'moderated', false)
->orderByDesc('id');
}
public function listAssets(Request $request)
{
$validator = Validator::make($request->all(), [
'assetTypeId' => ['required', 'int']
]);
if($validator->fails()) {
return ValidationHelper::generateValidatorError($validator);
}
$valid = $validator->valid();
if(!in_array($valid['assetTypeId'], $this->validAssetTypeIds)) {
$validator->errors()->add('assetTypeId', 'Invalid assetTypeId supplied.');
return ValidationHelper::generateValidatorError($validator);
}
$userAssets = self::GetUserAssets(Auth::user()->id)
->whereRelation('asset', 'assetTypeId', $valid['assetTypeId'])
->groupBy('asset_id')
->paginate(12);
$data = [];
foreach($userAssets as $userAsset)
{
$asset = $userAsset->asset;
array_push($data, [
'id' => $asset->id,
'Url' => route('shop.asset', ['asset' => $asset, 'assetName' => Str::slug($asset->name, '-')]),
'Thumbnail' => $asset->getThumbnail(),
'Name' => $asset->name,
'Wearing' => Auth::user()->isWearing($asset->id)
]);
}
return response([
'data' => $data,
'pages' => ($userAssets->hasPages() ? $userAssets->lastPage() : 1)
]);
}
public function listWearing(Request $request)
{
$avatarAssets = AvatarAsset::where('owner_id', Auth::user()->id)->get();
$data = [];
foreach($avatarAssets as $avatarAsset)
{
$asset = $avatarAsset->asset;
array_push($data, [
'id' => $asset->id,
'Url' => route('shop.asset', ['asset' => $asset, 'assetName' => Str::slug($asset->name, '-')]),
'Thumbnail' => $asset->getThumbnail(),
'Name' => $asset->name,
'Wearing' => true
]);
}
return response([
'data' => $data
]);
}
public function wearAsset(Request $request)
{
$validator = Validator::make($request->all(), [
'id' => [
'required',
Rule::exists('App\Models\Asset', 'id')->where(function($query) {
return $query->where('moderated', false);
})
]
]);
if($validator->fails()) {
return ValidationHelper::generateValidatorError($validator);
}
$valid = $validator->valid();
$userAsset = self::GetUserAssets(Auth::user()->id)
->where('asset_id', $valid['id'])
->first();
if(!$userAsset) {
$validator->errors()->add('id', 'User does not own asset.');
return ValidationHelper::generateValidatorError($validator);
}
if(Auth::user()->isWearing($valid['id']) && $userAsset->asset->assetTypeId == 8) { // 8 = hat
$validator->errors()->add('id', 'User is already wearing asset.');
return ValidationHelper::generateValidatorError($validator);
}
if(!in_array($userAsset->asset->assetTypeId, $this->validAssetTypeIds)) {
$validator->errors()->add('id', 'This asset cannot be worn.');
return ValidationHelper::generateValidatorError($validator);
}
$wornItems = AvatarAsset::where('owner_id', Auth::user()->id)
->whereRelation('asset', 'assetTypeId', $userAsset->asset->assetTypeId);
if($userAsset->asset->assetTypeId != 8 && $wornItems->exists()) // 8 = hat
{
$wornItems->delete();
}
elseif($userAsset->asset->assetTypeId == 8 && $wornItems->count() >= 10)
{
$validator->errors()->add('id', 'User has hit the wearing limit on this asset type.');
return ValidationHelper::generateValidatorError($validator);
}
AvatarAsset::Create([
'owner_id' => Auth::user()->id,
'asset_id' => $valid['id']
]);
Auth::user()->redraw();
return response(['success' => true]);
}
public function removeAsset(Request $request)
{
$validator = Validator::make($request->all(), [
'id' => [
'required',
Rule::exists('App\Models\Asset', 'id')->where(function($query) {
return $query->where('moderated', false);
})
]
]);
if($validator->fails()) {
return ValidationHelper::generateValidatorError($validator);
}
$valid = $validator->valid();
if(!Auth::user()->isWearing($valid['id'])) {
$validator->errors()->add('id', 'User is not wearing asset.');
return ValidationHelper::generateValidatorError($validator);
}
AvatarAsset::where('owner_id', Auth::user()->id)
->where('asset_id', $valid['id'])
->delete();
Auth::user()->redraw();
return response(['success' => true]);
}
public function setBodyColor(Request $request)
{
$validator = Validator::make($request->all(), [
'part' => ['required', 'regex:/(Head|Torso|LeftArm|RightArm|LeftLeg|RightLeg)/i'],
'color' => ['required', 'int']
]);
if($validator->fails()) {
return ValidationHelper::generateValidatorError($validator);
}
$valid = $validator->valid();
if(!BrickColorHelper::isValidColor($valid['color'])) {
$validator->errors()->add('color', 'Invalid color id.');
return ValidationHelper::generateValidatorError($validator);
}
$part = strtolower($valid['part']);
switch($part)
{
case 'leftarm':
$part = 'leftArm';
break;
case 'rightarm':
$part = 'rightArm';
break;
case 'leftleg':
$part = 'leftLeg';
break;
case 'rightleg':
$part = 'rightLeg';
break;
}
$bodyColors = Auth::user()->getBodyColors();
$bodyColors->{$part} = $valid['color'];
$bodyColors->save();
Auth::user()->redraw();
return response(['success' => true]);
}
public function getBodyColors(Request $request)
{
$bodyColors = Auth::user()->getBodyColors();
return response([
'data' => [
'Head' => $bodyColors->head,
'Torso' => $bodyColors->torso,
'RightArm' => $bodyColors->rightArm,
'LeftArm' => $bodyColors->leftArm,
'RightLeg' => $bodyColors->rightLeg,
'LeftLeg' => $bodyColors->leftLeg
]
]);
}
}

View File

@ -2,15 +2,19 @@
namespace App\Http\Controllers\Web\Auth;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules;
use App\Http\Controllers\Controller;
use App\Models\AvatarAsset;
use App\Models\DefaultUserAsset;
use App\Models\UserAsset;
use App\Models\User;
use App\Providers\RouteServiceProvider;
class RegisteredUserController extends Controller
{
@ -53,7 +57,22 @@ class RegisteredUserController extends Controller
'email' => $request->email,
'password' => Hash::make($request->password),
]);
foreach(DefaultUserAsset::all() as $defaultAsset)
{
UserAsset::createSerialed($user->id, $defaultAsset->asset_id);
if($defaultAsset->wearing)
{
AvatarAsset::create([
'owner_id' => $user->id,
'asset_id' => $defaultAsset->asset_id
]);
}
}
$user->redraw();
event(new Registered($user));
Auth::login($user);

View File

@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers\Web;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use App\Helpers\GridHelper;
use App\Helpers\ValidationHelper;
use App\Http\Controllers\Controller;
use App\Models\AvatarAsset;
use App\Models\User;
class ClientAvatarController extends Controller
{
public function bodyColors(Request $request)
{
$validator = Validator::make($request->all(), [
'userId' => [
'required',
Rule::exists('App\Models\User', 'id')
]
]);
if($validator->fails()) {
return ValidationHelper::generateValidatorError($validator);
}
$valid = $validator->valid();
$user = User::where('id', $valid['userId'])->first();
if($user->hasActivePunishment() && $user->getPunishment()->isDeletion()) {
$validator->errors()->add('id', 'User is moderated.');
return ValidationHelper::generateValidatorError($validator);
}
$document = simplexml_load_string(GridHelper::getBodyColorsXML());
$bodyColors = $user->getBodyColors();
$document->xpath('//int[@name="HeadColor"]')[0][0] = $bodyColors->head;
$document->xpath('//int[@name="TorsoColor"]')[0][0] = $bodyColors->torso;
$document->xpath('//int[@name="LeftArmColor"]')[0][0] = $bodyColors->leftArm;
$document->xpath('//int[@name="LeftLegColor"]')[0][0] = $bodyColors->leftLeg;
$document->xpath('//int[@name="RightArmColor"]')[0][0] = $bodyColors->rightArm;
$document->xpath('//int[@name="RightLegColor"]')[0][0] = $bodyColors->rightLeg;
return response($document->asXML())
->header('Content-Type', 'application/xml');
}
public function characterFetch(Request $request)
{
$validator = Validator::make($request->all(), [
'userId' => [
'required',
Rule::exists('App\Models\User', 'id')
]
]);
if($validator->fails()) {
return ValidationHelper::generateValidatorError($validator);
}
$valid = $validator->valid();
$user = User::where('id', $valid['userId'])->first();
if($user->hasActivePunishment() && $user->getPunishment()->isDeletion()) {
$validator->errors()->add('id', 'User is moderated.');
return ValidationHelper::generateValidatorError($validator);
}
$charApp = '';
$charApp .= route('client.bodyColors', ['userId' => $user->id]);
foreach($user->getWearing()->get() as $avatarAsset)
{
$charApp .= ';' . route('client.asset', ['id' => $avatarAsset->asset->id]);
if($avatarAsset->asset->assetTypeId == 19) // Gear
$charApp .= '&equipped=1';
}
return response($charApp)
->header('Content-Type', 'text/plain');
}
}

View File

@ -100,12 +100,17 @@ class ArbiterRender implements ShouldQueue
array_push($arguments, 0); // Camera Offset X
array_push($arguments, 0); // Camera Offset Y
case 'Avatar':
$arguments[0] = url('test', ['id' => $this->assetId]); // TODO: this
$arguments[0] = route('client.characterFetch', ['userId' => $this->assetId]);
break;
case 'Torso':
case 'Right Arm':
case 'Left Arm':
case 'Left Leg':
case 'Right Leg':
$this->type = 'BodyPart';
case 'Head':
case 'Shirt':
case 'Pants':
case 'BodyPart':
// TODO: XlXi: Move this to config, as it could be different from prod in a testing environment. Also move this to it's own asset (not loading from roblox).
array_push($arguments, 'https://www.roblox.com/asset/?id=1785197'); // Rig
break;

View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AvatarAsset extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'owner_id',
'asset_id'
];
public function asset()
{
return $this->belongsTo(Asset::class, 'asset_id');
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AvatarColor extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'owner_id',
'head',
'torso',
'leftArm',
'rightArm',
'leftLeg',
'rightLeg'
];
public static function GetRandomPrimaryColor()
{
$colors = [1, 208, 194, 199, 26, 21, 24, 23, 102, 141, 37, 29];
return $colors[array_rand($colors)];
}
public static function GetRandomHeadColor()
{
$colors = [1, 208, 194, 226];
return $colors[array_rand($colors)];
}
public static function user($userId)
{
return self::where('owner_id', $userId);
}
public static function newForUser($userId)
{
$headColor = self::GetRandomHeadColor();
$torsoColor = self::GetRandomPrimaryColor();
return self::create([
'owner_id' => $userId,
'head' => $headColor,
'torso' => $torsoColor,
'leftArm' => $headColor,
'rightArm' => $headColor,
'leftLeg' => 102,
'rightLeg' => 102
]);
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AvatarOutfit extends Model
{
use HasFactory;
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AvatarOutfitAsset extends Model
{
use HasFactory;
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class DefaultUserAsset extends Model
{
use HasFactory;
}

View File

@ -3,7 +3,6 @@
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -11,12 +10,10 @@ use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Laravel\Sanctum\HasApiTokens;
use App\Helpers\CdnHelper;
use App\Notifications\ResetPasswordNotification;
use App\Models\UserRoleset;
use App\Models\Roleset;
use App\Models\Friend;
class User extends Authenticatable implements MustVerifyEmail
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
@ -210,6 +207,49 @@ class User extends Authenticatable implements MustVerifyEmail
$this->save();
}
public function redraw()
{
$oldHashes = [
$this->thumbnailBustHash,
$this->thumbnail2DHash,
$this->thumbnail3DHash
];
$this->thumbnailBustHash = null;
$this->thumbnail2DHash = null;
$this->thumbnail3DHash = null;
$this->timestamps = false;
$this->save();
foreach($oldHashes as $hash)
{
if(!User::where('thumbnailBustHash', $hash)->orWhere('thumbnail2DHash', $hash)->orWhere('thumbnail3DHash', $hash)->exists())
CdnHelper::Delete($hash);
}
}
public function isWearing($assetId)
{
return AvatarAsset::where('owner_id', $this->id)
->where('asset_id', $assetId)
->exists();
}
public function getWearing()
{
return AvatarAsset::where('owner_id', $this->id)
->whereRelation('asset', 'moderated', 0);
}
public function getBodyColors()
{
$colors = AvatarColor::user($this->id);
if($colors->exists())
return $colors->first();
return AvatarColor::newForUser($this->id);
}
public function userToJson()
{
return [

View File

@ -87,6 +87,15 @@ return [
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'modes' => [
//'ONLY_FULL_GROUP_BY',
'STRICT_TRANS_TABLES',
'NO_ZERO_IN_DATE',
'NO_ZERO_DATE',
'ERROR_FOR_DIVISION_BY_ZERO',
'NO_AUTO_CREATE_USER',
'NO_ENGINE_SUBSTITUTION'
],
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('avatar_assets', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('owner_id');
$table->unsignedBigInteger('asset_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('avatar_assets');
}
};

View File

@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('avatar_colors', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('owner_id')->unique();
$table->unsignedBigInteger('head');
$table->unsignedBigInteger('torso');
$table->unsignedBigInteger('leftArm');
$table->unsignedBigInteger('rightArm');
$table->unsignedBigInteger('leftLeg');
$table->unsignedBigInteger('rightLeg');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('avatar_colors');
}
};

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('default_user_assets', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('asset_id');
$table->boolean('wearing');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('default_user_assets');
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('avatar_outfits', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('owner_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('avatar_outfits');
}
};

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('avatar_outfit_assets', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('outfit_id');
$table->unsignedBigInteger('asset_id');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('avatar_outfit_assets');
}
};

View File

@ -0,0 +1,882 @@
/*
Copyright © XlXi 2022
*/
import { Component, createRef, createElement } from 'react';
import axios from 'axios';
import classNames from 'classnames/bind';
import { buildGenericApiUrl } from '../util/HTTP.js';
import ProgressiveImage from './ProgressiveImage';
import Loader from './Loader';
import ThumbnailTool from './ThumbnailTool';
axios.defaults.withCredentials = true;
const brickColors = [
{id: 1, name: 'White'},
{id: 2, name: 'Grey'},
{id: 3, name: 'Light yellow'},
{id: 5, name: 'Brick yellow'},
{id: 6, name: 'Light green (Mint)'},
{id: 9, name: 'Light reddish violet'},
{id: 11, name: 'Pastel Blue'},
{id: 12, name: 'Light orange brown'},
{id: 18, name: 'Nougat'},
{id: 21, name: 'Bright red'},
{id: 22, name: 'Med. reddish violet'},
{id: 23, name: 'Bright blue'},
{id: 24, name: 'Bright yellow'},
{id: 25, name: 'Earth orange'},
{id: 26, name: 'Black'},
{id: 27, name: 'Dark grey'},
{id: 28, name: 'Dark green'},
{id: 29, name: 'Medium green'},
{id: 36, name: 'Lig. Yellowich orange'},
{id: 37, name: 'Bright green'},
{id: 38, name: 'Dark orange'},
{id: 39, name: 'Light bluish violet'},
{id: 40, name: 'Transparent'},
{id: 41, name: 'Tr. Red'},
{id: 42, name: 'Tr. Lg blue'},
{id: 43, name: 'Tr. Blue'},
{id: 44, name: 'Tr. Yellow'},
{id: 45, name: 'Light blue'},
{id: 47, name: 'Tr. Flu. Reddish orange'},
{id: 48, name: 'Tr. Green'},
{id: 49, name: 'Tr. Flu. Green'},
{id: 50, name: 'Phosph. White'},
{id: 100, name: 'Light red'},
{id: 101, name: 'Medium red'},
{id: 102, name: 'Medium blue'},
{id: 103, name: 'Light grey'},
{id: 104, name: 'Bright violet'},
{id: 105, name: 'Br. yellowish orange'},
{id: 106, name: 'Bright orange'},
{id: 107, name: 'Bright bluish green'},
{id: 108, name: 'Earth yellow'},
{id: 110, name: 'Bright bluish violet'},
{id: 111, name: 'Tr. Brown'},
{id: 112, name: 'Medium bluish violet'},
{id: 113, name: 'Tr. Medi. reddish violet'},
{id: 115, name: 'Med. yellowish green'},
{id: 116, name: 'Med. bluish green'},
{id: 118, name: 'Light bluish green'},
{id: 119, name: 'Br. yellowish green'},
{id: 120, name: 'Lig. yellowish green'},
{id: 121, name: 'Med. yellowish orange'},
{id: 123, name: 'Br. reddish orange'},
{id: 124, name: 'Bright reddish violet'},
{id: 125, name: 'Light orange'},
{id: 126, name: 'Tr. Bright bluish violet'},
{id: 127, name: 'Gold'},
{id: 128, name: 'Dark nougat'},
{id: 131, name: 'Silver'},
{id: 133, name: 'Neon orange'},
{id: 134, name: 'Neon green'},
{id: 135, name: 'Sand blue'},
{id: 136, name: 'Sand violet'},
{id: 137, name: 'Medium orange'},
{id: 138, name: 'Sand yellow'},
{id: 140, name: 'Earth blue'},
{id: 141, name: 'Earth green'},
{id: 143, name: 'Tr. Flu. Blue'},
{id: 145, name: 'Sand blue metallic'},
{id: 146, name: 'Sand violet metallic'},
{id: 147, name: 'Sand yellow metallic'},
{id: 148, name: 'Dark grey metallic'},
{id: 149, name: 'Black metallic'},
{id: 150, name: 'Light grey metallic'},
{id: 151, name: 'Sand green'},
{id: 153, name: 'Sand red'},
{id: 154, name: 'Dark red'},
{id: 157, name: 'Tr. Flu. Yellow'},
{id: 158, name: 'Tr. Flu. Red'},
{id: 168, name: 'Gun metallic'},
{id: 176, name: 'Red flip/flop'},
{id: 178, name: 'Yellow flip/flop'},
{id: 179, name: 'Silver flip/flop'},
{id: 180, name: 'Curry'},
{id: 190, name: 'Fire Yellow'},
{id: 191, name: 'Flame yellowish orange'},
{id: 192, name: 'Reddish brown'},
{id: 193, name: 'Flame reddish orange'},
{id: 194, name: 'Medium stone grey'},
{id: 195, name: 'Royal blue'},
{id: 196, name: 'Dark Royal blue'},
{id: 198, name: 'Bright reddish lilac'},
{id: 199, name: 'Dark stone grey'},
{id: 200, name: 'Lemon metalic'},
{id: 208, name: 'Light stone grey'},
{id: 209, name: 'Dark Curry'},
{id: 210, name: 'Faded green'},
{id: 211, name: 'Turquoise'},
{id: 212, name: 'Light Royal blue'},
{id: 213, name: 'Medium Royal blue'},
{id: 216, name: 'Rust'},
{id: 217, name: 'Brown'},
{id: 218, name: 'Reddish lilac'},
{id: 219, name: 'Lilac'},
{id: 220, name: 'Light lilac'},
{id: 221, name: 'Bright purple'},
{id: 222, name: 'Light purple'},
{id: 223, name: 'Light pink'},
{id: 224, name: 'Light brick yellow'},
{id: 225, name: 'Warm yellowish orange'},
{id: 226, name: 'Cool yellow'},
{id: 232, name: 'Dove blue'},
{id: 268, name: 'Medium lilac'},
{id: 301, name: 'Slime green'},
{id: 302, name: 'Smoky grey'},
{id: 303, name: 'Dark blue'},
{id: 304, name: 'Parsley green'},
{id: 305, name: 'Steel blue'},
{id: 306, name: 'Storm blue'},
{id: 307, name: 'Lapis'},
{id: 308, name: 'Dark indigo'},
{id: 309, name: 'Sea green'},
{id: 310, name: 'Shamrock'},
{id: 311, name: 'Fossil'},
{id: 312, name: 'Mulberry'},
{id: 313, name: 'Forest green'},
{id: 314, name: 'Cadet blue'},
{id: 315, name: 'Electric blue'},
{id: 316, name: 'Eggplant'},
{id: 317, name: 'Moss'},
{id: 318, name: 'Artichoke'},
{id: 319, name: 'Sage green'},
{id: 320, name: 'Ghost grey'},
{id: 321, name: 'Lilac'},
{id: 322, name: 'Plum'},
{id: 323, name: 'Olivine'},
{id: 324, name: 'Laurel green'},
{id: 325, name: 'Quill grey'},
{id: 327, name: 'Crimson'},
{id: 328, name: 'Mint'},
{id: 329, name: 'Baby blue'},
{id: 330, name: 'Carnation pink'},
{id: 331, name: 'Persimmon'},
{id: 332, name: 'Maroon'},
{id: 333, name: 'Gold'},
{id: 334, name: 'Daisy orange'},
{id: 335, name: 'Pearl'},
{id: 336, name: 'Fog'},
{id: 337, name: 'Salmon'},
{id: 338, name: 'Terra Cotta'},
{id: 339, name: 'Cocoa'},
{id: 340, name: 'Wheat'},
{id: 341, name: 'Buttermilk'},
{id: 342, name: 'Mauve'},
{id: 343, name: 'Sunrise'},
{id: 344, name: 'Tawny'},
{id: 345, name: 'Rust'},
{id: 346, name: 'Cashmere'},
{id: 347, name: 'Khaki'},
{id: 348, name: 'Lily white'},
{id: 349, name: 'Seashell'},
{id: 350, name: 'Burgundy'},
{id: 351, name: 'Cork'},
{id: 352, name: 'Burlap'},
{id: 353, name: 'Beige'},
{id: 354, name: 'Oyster'},
{id: 355, name: 'Pine Cone'},
{id: 356, name: 'Fawn brown'},
{id: 357, name: 'Hurricane grey'},
{id: 358, name: 'Cloudy grey'},
{id: 359, name: 'Linen'},
{id: 360, name: 'Copper'},
{id: 361, name: 'Dirt brown'},
{id: 362, name: 'Bronze'},
{id: 363, name: 'Flint'},
{id: 364, name: 'Dark taupe'},
{id: 365, name: 'Burnt Sienna'},
{id: 1001, name: 'Institutional white'},
{id: 1002, name: 'Mid gray'},
{id: 1003, name: 'Really black'},
{id: 1004, name: 'Really red'},
{id: 1005, name: 'Deep orange'},
{id: 1006, name: 'Alder'},
{id: 1007, name: 'Dusty Rose'},
{id: 1008, name: 'Olive'},
{id: 1009, name: 'New Yeller'},
{id: 1010, name: 'Really blue'},
{id: 1011, name: 'Navy blue'},
{id: 1012, name: 'Deep blue'},
{id: 1013, name: 'Cyan'},
{id: 1014, name: 'CGA brown'},
{id: 1015, name: 'Magenta'},
{id: 1016, name: 'Pink'},
{id: 1017, name: 'Deep orange'},
{id: 1018, name: 'Teal'},
{id: 1019, name: 'Toothpaste'},
{id: 1020, name: 'Lime green'},
{id: 1021, name: 'Camo'},
{id: 1022, name: 'Grime'},
{id: 1023, name: 'Lavender'},
{id: 1024, name: 'Pastel light blue'},
{id: 1025, name: 'Pastel orange'},
{id: 1026, name: 'Pastel violet'},
{id: 1027, name: 'Pastel blue-green'},
{id: 1028, name: 'Pastel green'},
{id: 1029, name: 'Pastel yellow'},
{id: 1030, name: 'Pastel brown'},
{id: 1031, name: 'Royal purple'},
{id: 1032, name: 'Hot pink'}
];
class EditorItemCard extends Component {
constructor(props) {
super(props);
this.state = {
worn: false
};
this.wearAssetId = this.wearAssetId.bind(this);
}
componentDidMount() {
if(this.props.item.Wearing)
this.setState({ worn: true });
}
wearAssetId(assetId) {
this.setState({ worn: !this.state.worn });
if(this.state.worn)
this.props.unwearAssetId(assetId);
else
this.props.wearAssetId(assetId);
}
render() {
var item = this.props.item;
return (
<div className="virtubrick-item-card virtubrick-avatar-card">
<span className="card m-2">
<a className="text-decoration-none text-reset" href={ item.Url }>
<ProgressiveImage
src={ item.Thumbnail }
placeholderImg={ buildGenericApiUrl('www', 'images/busy/asset.png') }
alt={ item.Name }
className='img-fluid'
/>
<div className="p-2 pb-0">
<p className="text-truncate">{ item.Name }</p>
</div>
</a>
<button className={classNames({'btn': true, 'btn-sm': true, 'btn-primary': !this.state.worn, 'btn-danger': this.state.worn, 'm-2': true,})} onClick={ () => this.wearAssetId(item.id) }>{ this.state.worn ? 'Take off' : 'Wear' }</button>
</span>
</div>
);
}
}
class WardrobeTab extends Component {
constructor(props) {
super(props);
this.state = {
loaded: false,
assetTabs: [
{label: 'Heads', typeId: 17},
{label: 'Faces', typeId: 18},
{label: 'Hats', typeId: 8},
{label: 'T-Shirts', typeId: 2},
{label: 'Shirts', typeId: 11},
{label: 'Pants', typeId: 12},
{label: 'Gear', typeId: 19},
{label: 'Torsos', typeId: 27},
{label: 'Left Arms', typeId: 29},
{label: 'Right Arms', typeId: 28},
{label: 'Left Legs', typeId: 30},
{label: 'Right Legs', typeId: 31},
{label: 'Packages', typeId: 32}
],
pageKey: 0,
pageNumber: 1,
pages: 0,
items: []
};
this.setTypeId = this.setTypeId.bind(this);
this.incrementPage = this.incrementPage.bind(this);
this.loadPage = this.loadPage.bind(this);
this.refresh = this.refresh.bind(this);
}
componentDidMount() {
this.setTypeId(8, true); // XlXi: Bypass needed, else the initial page load isn't going to happen.
}
setTypeId(assetTypeId, forceLoad = false) {
this.setState({ selectedTypeId: assetTypeId}, function() {
this.loadPage(1, !forceLoad);
});
}
refresh() {
this.loadPage(this.state.pageNumber);
}
incrementPage(amount) {
this.loadPage(this.state.pageNumber + amount);
}
loadPage(pageNum, noBypass = true) {
if(!this.state.loaded && noBypass)
return;
this.setState({ loaded: false });
let oldPageNum = this.state.pageNumber;
this.setState({ pageNumber: pageNum });
axios.get(buildGenericApiUrl('api', `avatar/v1/list?assetTypeId=${this.state.selectedTypeId}${ pageNum > 1 ? '&page=' + pageNum : '' }`))
.then(res => {
const items = res.data;
let newKey = this.state.pageKey + 1;
this.setState({ pageKey: newKey, items: items.data, pages: items.pages, loaded: true });
})
.catch(() => {
this.props.setError('Error loading wardrobe page.');
this.setState({ pageNumber: oldPageNum, loaded: true });
});
}
render() {
return (
<>
<ul className="nav nav-pills my-2 mx-auto justify-content-center vb-wardrobe-nav">
{
this.state.assetTabs.map(({ label, typeId, ref }) =>
<li className="nav-item">
<button className={classNames({'nav-link': true, 'active': this.state.selectedTypeId == typeId, 'disabled': !this.state.loaded})} disabled={ !this.state.loaded } onClick={ () => this.setTypeId(typeId) }>{ label }</button>
</li>
)
}
</ul>
{
!this.state.loaded
?
<div className="virtubrick-shop-overlay">
<Loader />
</div>
:
null
}
{
this.state.items.length == 0
?
<p className="text-muted text-center">Nothing found.</p>
:
<div key={ this.state.pageKey }>
{
this.state.items.map((item, index) =>
<EditorItemCard unwearAssetId={ this.props.unwearAssetId } wearAssetId={ this.props.wearAssetId } item={ item } key={ index } />
)
}
</div>
}
{
this.state.pages > 1 ?
<ul className="list-inline mx-auto mt-3">
<li className="list-inline-item">
<button className="btn btn-secondary" disabled={(this.state.pageNumber <= 1) ? true : null} onClick={ () => this.incrementPage(-1) }><i className="fa-solid fa-angle-left"></i></button>
</li>
<li className="list-inline-item virtubrick-paginator">
<span>Page&nbsp;</span>
<input type="text" value={ this.state.pageNumber || '' } className="form-control" disabled={this.state.loaded ? null : true} />
<span>&nbsp;of { this.state.pages || '???' }</span>
</li>
<li className="list-inline-item">
<button className="btn btn-secondary" disabled={(this.state.pageNumber >= this.state.pages) ? true : null} onClick={ () => this.incrementPage(1) }><i className="fa-solid fa-angle-right"></i></button>
</li>
</ul>
:
null
}
</>
);
}
}
class OutfitsTab extends Component {
constructor(props) {
super(props);
}
render() {
return (<><p>outfits</p></>);
}
}
// TODO: XlXi: Move this out of this component. I was too lazy to do it initially but it can be done.
class BodyColorPaneBodyPart extends Component {
constructor(props) {
super(props);
this.state = {
color: 'vb-bc-194'
}
this.setColor = this.setColor.bind(this);
}
setColor(color) {
this.setState({ color: color });
}
render() {
return (
<button
className={ `vb-character vb-character-${ this.props.className } ${ this.state.color }` }
onClick={ this.props.onClick }
></button>
)
}
}
class BodyColorSelectionModal extends Component {
constructor(props) {
super(props);
this.state = {
};
this.ModalRef = createRef();
this.Modal = null;
}
componentDidMount() {
this.Modal = new Bootstrap.Modal(this.ModalRef.current);
this.Modal.show();
this.ModalRef.current.addEventListener('hidden.bs.modal', (event) => {
this.props.setModal(null);
});
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="bc-tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new Bootstrap.Tooltip(tooltipTriggerEl)
});
}
componentWillUnmount() {
this.Modal.dispose();
}
render() {
return (
<div ref={this.ModalRef} className="modal fade">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content text-center">
<div className="modal-header">
<h5 className="modal-title">Set Color</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<div className="vb-hex pb-5">
<div className="vb-hex-container">
{
brickColors.map(({ id, name }) =>
<div className={ `vb-bc-${ id }` } onClick={ () => this.props.setBodyColor(id) } data-bs-dismiss="modal" data-bs-toggle="bc-tooltip" data-bs-placement="top" title="" data-bs-original-title={ name }></div>
)
}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
}
class BodyColorPane extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
showModal: false
};
this.bodyParts = [
{name: 'Head', className: 'head', ref: createRef()},
{name: 'Torso', className: 'torso', ref: createRef()},
{name: 'RightArm', className: 'limb', ref: createRef()},
{name: 'LeftArm', className: 'limb', ref: createRef()},
{name: 'RightLeg', className: 'limb', ref: createRef()},
{name: 'LeftLeg', className: 'limb', ref: createRef()}
];
this.findPart = this.findPart.bind(this);
this.setModal = this.setModal.bind(this);
this.load = this.load.bind(this);
this.openColorModal = this.openColorModal.bind(this);
this.setBodyColor = this.setBodyColor.bind(this);
this.getBodyPartElem = this.getBodyPartElem.bind(this);
}
findPart(name) {
return this.bodyParts.find(obj => {
return obj.name === name
});
}
setModal(modal = null) {
this.visibleModal = modal;
if(modal) {
this.setState({'showModal': true});
} else {
this.setState({'showModal': false});
}
}
load() {
this.setState({ loading: true });
axios.get(buildGenericApiUrl('api', 'avatar/v1/body-color'))
.then(res => {
Object.keys(res.data.data).map(key => {
let part = this.findPart(key);
part.ref.current.setColor(`vb-bc-${ res.data.data[key] }`);
});
this.setState({ loading: false });
})
.catch(() => {
this.props.setError('Error loading body colors pane.');
this.setState({ loading: false });
});
}
componentDidMount() {
this.load();
}
openColorModal(name) {
this.setModal(<BodyColorSelectionModal setModal={ this.setModal } setBodyColor={ (color) => this.setBodyColor(name, color) } />);
}
setBodyColor(name, color) {
let part = this.findPart(name);
part.ref.current.setColor(`vb-bc-${ color }`);
this.props.setBodyColor(name, color);
}
getBodyPartElem(name) {
let part = this.findPart(name);
if(!part.elem)
{
let elem = createElement(
BodyColorPaneBodyPart,
{
className: part.className,
onClick: (() => this.openColorModal(part.name)),
ref: part.ref,
key: part.name
}
);
part.elem = elem;
return elem
}
return part.elem;
}
render() {
return (
<>
{
this.state.loading
?
<div className="virtubrick-shop-overlay">
<Loader />
</div>
:
null
}
{ this.getBodyPartElem('Head') }
<div className="d-flex mx-auto">
{ this.getBodyPartElem('RightArm') }
{ this.getBodyPartElem('Torso') }
{ this.getBodyPartElem('LeftArm') }
</div>
<div className="d-flex mx-auto">
{ this.getBodyPartElem('RightLeg') }
{ this.getBodyPartElem('LeftLeg') }
</div>
{ this.state.showModal ? this.visibleModal : null }
</>
);
}
}
class WearingPane extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
items: [],
pageKey: 0
};
this.load = this.load.bind(this);
}
componentDidMount() {
this.load();
}
load() {
this.setState({ loading: true });
axios.get(buildGenericApiUrl('api', 'avatar/v1/wearing'))
.then(res => {
const items = res.data;
let newKey = this.state.pageKey + 1;
this.setState({ pageKey: newKey, items: items.data, loading: false });
})
.catch(() => {
this.props.setError('Error loading wearing pane.');
this.setState({ pageNumber: oldPageNum, loading: false });
});
}
render() {
return (
<>
{
(this.state.loading)
?
<div className="virtubrick-shop-overlay">
<Loader />
</div>
:
null
}
{
this.state.items.length == 0
?
<p className="text-muted text-center">Nothing found.</p>
:
<div key={ this.state.pageKey }>
{
this.state.items.map((item, index) =>
<EditorItemCard unwearAssetId={ this.props.unwearAssetId } item={ item } key={ index } />
)
}
</div>
}
</>
);
}
}
class AvatarEditor extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
thumbnailLoading: false,
tabLoaded: false,
tabs: [
{label: 'Wardrobe', name: 'wardrobe', ref: createRef()},
{label: 'Outfits', name: 'outfits', ref: createRef()}
],
error: null,
thumbKey: 0
};
this.tabIndex = 0;
this.wearingPane = createRef();
this.bodyColorPane = createRef();
this.tabPane = createRef();
this.setCurrentTab = this.setCurrentTab.bind(this);
this.setTab = this.setTab.bind(this);
this.redrawCharacter = this.redrawCharacter.bind(this);
this.reloadCharacter = this.reloadCharacter.bind(this);
this.setError = this.setError.bind(this);
this.thumbCallback = this.thumbCallback.bind(this);
this.wearAssetId = this.wearAssetId.bind(this);
this.unwearAssetId = this.unwearAssetId.bind(this);
this.setBodyColor = this.setBodyColor.bind(this);
}
componentDidMount() {
this.setTab('wardrobe');
}
setCurrentTab(instance)
{
this.currentTab = instance;
this.setState({tabLoaded: true});
}
setTab(tabType) {
this.setState({tabLoaded: false});
this.tabIndex += 1;
let component = {
wardrobe: WardrobeTab,
outfits: OutfitsTab
}[tabType];
this.setCurrentTab(createElement(
component,
{
setError: this.setError,
reloadCharacter: this.reloadCharacter,
wearAssetId: this.wearAssetId,
unwearAssetId: this.unwearAssetId,
ref: this.tabPane,
key: this.tabIndex
}
));
this.state.tabs.map(({ name, ref }) => {
if(name == tabType)
ref.current.classList.add('active');
else
ref.current.classList.remove('active');
});
}
redrawCharacter() {
if(this.state.loading) return;
this.setState({ loading: true });
axios.post(buildGenericApiUrl('api', 'avatar/v1/redraw'))
.then(res => {
this.reloadCharacter();
this.setState({ loading: false });
})
.catch(() => {
this.setError('An error occurred while redrawing.');
this.setState({ loading: false });
});
}
setError(state) {
this.setState({ error: state });
setTimeout(function(){
this.setState({ error: null });
}.bind(this), 2000);
}
reloadCharacter() {
let thumbKey = this.state.thumbKey + 1;
this.wearingPane.current.load();
this.bodyColorPane.current.load();
this.tabPane.current.refresh();
this.setState({ thumbKey: thumbKey });
}
thumbCallback(loading) {
this.setState({ thumbnailLoading: loading });
}
wearAssetId(assetId, callback) {
if(this.state.thumbnailLoading) return;
axios.post(buildGenericApiUrl('api', `avatar/v1/wear?id=${ assetId }`))
.then(res => {
this.reloadCharacter();
if(callback) callback();
})
.catch(() => {
this.setError('An error occurred while trying to wear this item.');
});
}
unwearAssetId(assetId, callback) {
if(this.state.thumbnailLoading) return;
axios.post(buildGenericApiUrl('api', `avatar/v1/unwear?id=${ assetId }`))
.then(res => {
this.reloadCharacter();
if(callback) callback();
})
.catch(() => {
this.setError('An error occurred while trying to take off this item.');
});
}
setBodyColor(name, color) {
if(this.state.thumbnailLoading) return;
axios.post(buildGenericApiUrl('api', `avatar/v1/set-body-color?part=${ name }&color=${ color }`))
.then(res => {
this.reloadCharacter();
})
.catch(() => {
this.setError('An error occurred while trying to change body color.');
});
}
render() {
return (
<>
{
this.state.error
?
<div className="alert alert-danger virtubrick-alert virtubrick-error-popup">{ this.state.error }</div>
:
null
}
<div className="col-lg-3">
<div className="card text-center vb-avatar-editor-card">
{
this.state.loading
?
<div className='position-absolute top-50 start-50 translate-middle'>
<Loader />
</div>
:
<ThumbnailTool element={ this.props.element } key={ this.state.thumbKey } placeholder="images/busy/user.png" thumbLoadCallback={ this.thumbCallback } />
}
</div>
<p className="fst-italic">Is something wrong with your avatar?</p>
<a className="text-decoration-none" href="#" onClick={ this.redrawCharacter }>Click here to re-draw it!</a>
<h4 className="mt-3">Colors</h4>
<div className="card p-4">
<BodyColorPane setError={ this.setError } setBodyColor={ this.setBodyColor } ref={ this.bodyColorPane } />
</div>
</div>
<div className="mt-lg-0 mt-4 col-lg-9">
<ul className="nav nav-tabs">
{
this.state.tabs.map(({ label, name, ref }) =>
<li className="nav-item">
<button className="nav-link" onClick={ () => this.setTab(name) } ref={ ref }>{ label }</button>
</li>
)
}
</ul>
<div className="card p-2">
{ this.state.tabLoaded ? this.currentTab : <Loader /> }
</div>
<h4 className="mt-3">Currently Wearing</h4>
<div className="card p-2">
<WearingPane setError={ this.setError } reloadCharacter={ this.reloadCharacter } unwearAssetId={ this.unwearAssetId } ref={ this.wearingPane } />
</div>
</div>
</>
);
}
}
export default AvatarEditor;

View File

@ -270,6 +270,7 @@ class Shop extends Component {
super(props);
this.state = {
selectedCategoryId: -1,
pageKey: 0,
pageItems: [],
pageLoaded: true,
pageNumber: null,
@ -332,11 +333,12 @@ class Shop extends Component {
url += ((paramIterator++ == 0 ? '?' : '&') + `${key}=${data[key]}`);
});
let newKey = this.state.pageKey + 1;
axios.get(url)
.then(res => {
const items = res.data;
this.setState({ pageItems: items.data, pageCount: items.pages, pageLoaded: true, error: false });
this.setState({ pageKey: newKey, pageItems: items.data, pageCount: items.pages, pageLoaded: true, error: false });
}).catch(err => {
const data = err.response.data;
@ -344,7 +346,7 @@ class Shop extends Component {
if(data.errors)
errorMessage = data.errors[0].message;
this.setState({ pageItems: [], pageCount: 1, pageNumber: 1, pageLoaded: true, error: errorMessage });
this.setState({ pageKey: newKey, pageItems: [], pageCount: 1, pageNumber: 1, pageLoaded: true, error: errorMessage });
this.inputBox.current.focus();
});
}
@ -404,7 +406,7 @@ class Shop extends Component {
(this.state.pageItems.length == 0 && !this.state.error) ?
<p className="text-muted text-center">Nothing found.</p>
:
<div>
<div key={ this.state.pageKey }>
{
this.state.pageItems.map((item, index) =>
<ShopItemCard item={ item } key={ index } />

View File

@ -20,7 +20,7 @@ import Loader from './Loader';
axios.defaults.withCredentials = true;
const Scene = ({json}) => {
function Scene({json}) {
const mtl = useLoader(MTLLoader, json.mtl);
const obj = useLoader(OBJLoader, json.obj, (loader) => {
mtl.preload();
@ -29,7 +29,8 @@ const Scene = ({json}) => {
let controls = useRef();
let midPoint;
useThree(({camera, scene}) => {
const {camera, scene} = useThree();
useEffect(() => {
let aabbMax = json.AABB.max;
let aabbMin = json.AABB.min;
aabbMax = new THREE.Vector3(aabbMax.x, aabbMax.y, aabbMax.z);
@ -53,27 +54,24 @@ const Scene = ({json}) => {
camera.translateZ(0.5);
// lighting
// FIXME: XlXi: if you toggle 3d on and off it'll create these twice
let ambient = new THREE.AmbientLight(0x878780);
const ambient = new THREE.AmbientLight(0x7F7F7F);
scene.add(ambient);
let sunLight = new THREE.DirectionalLight(0xacacac);
sunLight.position.set(0.671597898, 0.671597898, -0.312909544).normalize();
const sunLight = new THREE.DirectionalLight(0xFFFFFF);
sunLight.position.set(-0.17786, 0.2563, -0.2787).normalize();
scene.add(sunLight);
let backLight = new THREE.DirectionalLight(0x444444);
let backLightPos = new THREE.Vector3()
const backLight = new THREE.DirectionalLight(0xB3B3B8);
const backLightPos = new THREE.Vector3()
.copy(sunLight.position)
.negate()
.normalize(); // inverse of sun direction
backLight.position.set(backLightPos);
scene.add(backLight);
});
useEffect(() => {
controls.current.target = midPoint;
controls.current.update();
});
}, []);
return (
<>
@ -98,8 +96,9 @@ class ThumbnailTool extends Component {
super(props);
this.state = {
initialLoading: true,
loading: false,
loading: true,
is3d: false,
image2d: null,
seed3d: 0
};
@ -112,6 +111,7 @@ class ThumbnailTool extends Component {
let thumbnailElement = this.props.element;
if (thumbnailElement) {
this.thumbnail2d = thumbnailElement.getAttribute('data-asset-thumbnail-2d');
this.thumbnail3d = thumbnailElement.getAttribute('data-asset-thumbnail-3d');
this.assetId = thumbnailElement.getAttribute('data-asset-id');
this.assetName = thumbnailElement.getAttribute('data-asset-name');
this.wearable = Boolean(thumbnailElement.getAttribute('data-wearable'));
@ -119,13 +119,29 @@ class ThumbnailTool extends Component {
this.setState({ initialLoading: false });
if(this.props.thumbLoadCallback)
this.props.thumbLoadCallback(true);
if(this.renderable3d && localStorage.getItem('vb-use-3d-thumbnails') === 'true')
this.toggle3D();
else
{
if(this.thumbnail2d && this.thumbnail2d.match(/^https?:\/\/api\./gi))
this.loadThumbnail(this.thumbnail2d, false);
else
this.setState({ loading: false, image2d: this.thumbnail2d });
}
}
}
componentDidUpdate(oldProps, oldState)
{
if(oldState.loading != this.state.loading && this.props.thumbLoadCallback)
this.props.thumbLoadCallback(this.state.loading);
}
loadThumbnail(url, is3d) {
axios.get(buildGenericApiUrl('api', url))
axios.get(url)
.then(res => {
let data = res.data;
@ -149,7 +165,7 @@ class ThumbnailTool extends Component {
this.setState({ loading: false, json3d: res.data });
});
} else {
this.setState({ loading: false });
this.setState({ loading: false, image2d: data.data });
}
} else {
let lt = this.loadThumbnail;
@ -162,7 +178,7 @@ class ThumbnailTool extends Component {
let is3d = !this.state.is3d;
this.setState({ loading: true, is3d: is3d });
this.loadThumbnail(`thumbnails/v1/try-asset?id=${this.assetId}&type=${this.state.is3d ? '3D' : '2D'}`, is3d);
this.loadThumbnail(buildGenericApiUrl('api', `thumbnails/v1/try-asset?id=${this.assetId}&type=${this.state.is3d ? '3D' : '2D'}`), is3d);
}
toggle3D() {
@ -171,70 +187,74 @@ class ThumbnailTool extends Component {
this.setState({ loading: true, is3d: is3d, seed3d: Math.random() });
localStorage.setItem('vb-use-3d-thumbnails', is3d);
if(is3d) {
this.loadThumbnail(`thumbnails/v1/asset?id=${this.assetId}&type=3D`, true);
} else {
this.setState({ loading: false });
if(is3d)
this.loadThumbnail(this.thumbnail3d, true);
else
{
if(this.thumbnail2d && this.thumbnail2d.match(/^https?:\/\/api\./gi))
this.loadThumbnail(this.thumbnail2d, false);
else
this.setState({ loading: false });
}
}
render() {
return (
<>
{
this.state.initialLoading
?
<div className='position-absolute top-50 start-50 translate-middle'>
<Loader />
</div>
:
<>
{
this.state.loading
?
<div className='position-absolute top-50 start-50 translate-middle'>
<Loader />
</div>
:
(
this.state.is3d
?
<Canvas key={ this.state.seed3d }>
<Suspense fallback={null}>
<Scene json={ this.state.json3d } />
</Suspense>
</Canvas>
:
<ProgressiveImage
src={ this.thumbnail2d }
placeholderImg={ buildGenericApiUrl('www', (this.props.placeholder == null ? 'images/busy/asset.png' : this.props.placeholder)) }
alt={ this.assetName }
className='img-fluid'
width={ this.props.width }
height={ this.props.height }
/>
)
}
{ this.wearable || this.renderable3d ?
<div className='d-flex position-absolute bottom-0 end-0 pb-2 pe-2'>
{
this.wearable ?
<button className='btn btn-secondary me-2' onClick={ this.tryAsset } disabled={ this.state.loading }>Try On</button>
:
null
}
{
this.renderable3d ?
<button className='btn btn-secondary' onClick={ this.toggle3D } disabled={ this.state.loading }>{ this.state.is3d ? '2D' : '3D' }</button>
:
null
}
{
this.state.initialLoading
?
<div className='position-absolute top-50 start-50 translate-middle'>
<Loader />
</div>
:
null
}
</>
}
<>
{
this.state.loading
?
<div className='position-absolute top-50 start-50 translate-middle'>
<Loader />
</div>
:
(
this.state.is3d
?
<Canvas key={ this.state.seed3d }>
<Suspense fallback={null}>
<Scene json={ this.state.json3d } />
</Suspense>
</Canvas>
:
<ProgressiveImage
src={ this.state.image2d }
placeholderImg={ buildGenericApiUrl('www', (this.props.placeholder == null ? 'images/busy/asset.png' : this.props.placeholder)) }
alt={ this.assetName }
className='img-fluid'
width={ this.props.width }
height={ this.props.height }
/>
)
}
{ this.wearable || this.renderable3d ?
<div className='d-flex position-absolute bottom-0 end-0 pb-2 pe-2'>
{
this.wearable ?
<button className='btn btn-secondary me-2' onClick={ this.tryAsset } disabled={ this.state.loading }>Try On</button>
:
null
}
{
this.renderable3d ?
<button className='btn btn-secondary' onClick={ this.toggle3D } disabled={ this.state.loading }>{ this.state.is3d ? '2D' : '3D' }</button>
:
null
}
</div>
:
null
}
</>
}
</>
);
}

View File

@ -0,0 +1,19 @@
/*
Copyright © XlXi 2022
*/
import $ from 'jquery';
import React from 'react';
import { render } from 'react-dom';
import AvatarEditor from '../components/AvatarEditor';
const editorId = 'vb-avatar-editor';
$(document).ready(function() {
if (document.getElementById(editorId)) {
let eElem = document.getElementById(editorId);
render(<AvatarEditor element={ eElem }/>, eElem);
}
});

View File

@ -0,0 +1,218 @@
// © XlXi 2023
$brickColors: (
1: #f2f3f3, // White
2: #a1a5a2, // Grey
3: #f9e999, // Light yellow
5: #d7c59a, // Brick yellow
6: #c2dab8, // Light green (Mint)
9: #e8bac8, // Light reddish violet
11: #80bbdb, // Pastel Blue
12: #cb8442, // Light orange brown
18: #cc8e69, // Nougat
21: #c4281c, // Bright red
22: #c470a0, // Med. reddish violet
23: #0d69ac, // Bright blue
24: #f5cd30, // Bright yellow
25: #624732, // Earth orange
26: #1b2a35, // Black
27: #6d6e6c, // Dark grey
28: #287f47, // Dark green
29: #a1c48c, // Medium green
36: #f3cf9b, // Lig. Yellowich orange
37: #4b974b, // Bright green
38: #a05f35, // Dark orange
39: #c1cade, // Light bluish violet
40: #ececec, // Transparent
41: #cd544b, // Tr. Red
42: #c1dff0, // Tr. Lg blue
43: #7bb6e8, // Tr. Blue
44: #f7f18d, // Tr. Yellow
45: #b4d2e4, // Light blue
47: #d9856c, // Tr. Flu. Reddish orange
48: #84b68d, // Tr. Green
49: #f8f184, // Tr. Flu. Green
50: #ece8de, // Phosph. White
100: #eec4b6, // Light red
101: #da867a, // Medium red
102: #6e99ca, // Medium blue
103: #c7c1b7, // Light grey
104: #6b327c, // Bright violet
105: #e29b40, // Br. yellowish orange
106: #da8541, // Bright orange
107: #008f9c, // Bright bluish green
108: #685c43, // Earth yellow
110: #435493, // Bright bluish violet
111: #bfb7b1, // Tr. Brown
112: #6874ac, // Medium bluish violet
113: #e4adc8, // Tr. Medi. reddish violet
115: #c7d23c, // Med. yellowish green
116: #55a5af, // Med. bluish green
118: #b7d7d5, // Light bluish green
119: #a4bd47, // Br. yellowish green
120: #d9e4a7, // Lig. yellowish green
121: #e7ac58, // Med. yellowish orange
123: #d36f4c, // Br. reddish orange
124: #923978, // Bright reddish violet
125: #eab892, // Light orange
126: #a5a5cb, // Tr. Bright bluish violet
127: #dcbc81, // Gold
128: #ae7a59, // Dark nougat
131: #9ca3a8, // Silver
133: #d5733d, // Neon orange
134: #d8dd56, // Neon green
135: #74869d, // Sand blue
136: #877c90, // Sand violet
137: #e09864, // Medium orange
138: #958a73, // Sand yellow
140: #203a56, // Earth blue
141: #27462d, // Earth green
143: #cfe2f7, // Tr. Flu. Blue
145: #7988a1, // Sand blue metallic
146: #958ea3, // Sand violet metallic
147: #938767, // Sand yellow metallic
148: #575857, // Dark grey metallic
149: #161d32, // Black metallic
150: #abadac, // Light grey metallic
151: #789082, // Sand green
153: #957977, // Sand red
154: #7b2e2f, // Dark red
157: #fff67b, // Tr. Flu. Yellow
158: #e1a4c2, // Tr. Flu. Red
168: #756c62, // Gun metallic
176: #97695b, // Red flip/flop
178: #b48455, // Yellow flip/flop
179: #898788, // Silver flip/flop
180: #d7a94b, // Curry
190: #f9d62e, // Fire Yellow
191: #e8ab2d, // Flame yellowish orange
192: #694028, // Reddish brown
193: #cf6024, // Flame reddish orange
194: #a3a2a5, // Medium stone grey
195: #4667a4, // Royal blue
196: #23478b, // Dark Royal blue
198: #8e4285, // Bright reddish lilac
199: #635f62, // Dark stone grey
200: #828a5d, // Lemon metalic
208: #e5e4df, // Light stone grey
209: #b08e44, // Dark Curry
210: #709578, // Faded green
211: #79b5b5, // Turquoise
212: #9fc3e9, // Light Royal blue
213: #6c81b7, // Medium Royal blue
216: #8f4c2a, // Rust
217: #7c5c46, // Brown
218: #96709f, // Reddish lilac
219: #6b629b, // Lilac
220: #a7a9ce, // Light lilac
221: #cd6298, // Bright purple
222: #e4adc8, // Light purple
223: #dc9095, // Light pink
224: #f0d5a0, // Light brick yellow
225: #ebb87f, // Warm yellowish orange
226: #fdea8d, // Cool yellow
232: #7dbbdd, // Dove blue
268: #342b75, // Medium lilac
301: #506d54, // Slime green
302: #5b5d69, // Smoky grey
303: #0010b0, // Dark blue
304: #2c651d, // Parsley green
305: #527cae, // Steel blue
306: #335882, // Storm blue
307: #102adc, // Lapis
308: #3d1585, // Dark indigo
309: #348e40, // Sea green
310: #5b9a4c, // Shamrock
311: #9fa1ac, // Fossil
312: #592259, // Mulberry
313: #1f801d, // Forest green
314: #9fadc0, // Cadet blue
315: #0989cf, // Electric blue
316: #7b007b, // Eggplant
317: #7c9c6b, // Moss
318: #8aab85, // Artichoke
319: #b9c4b1, // Sage green
320: #cacbd1, // Ghost grey
321: #a75e9b, // Lilac
322: #7b2f7b, // Plum
323: #94be81, // Olivine
324: #a8bd99, // Laurel green
325: #dfdfde, // Quill grey
327: #970000, // Crimson
328: #b1e5a6, // Mint
329: #98c2db, // Baby blue
330: #ff98dc, // Carnation pink
331: #ff5959, // Persimmon
332: #750000, // Maroon
333: #efb838, // Gold
334: #f8d96d, // Daisy orange
335: #e7e7ec, // Pearl
336: #c7d4e4, // Fog
337: #ff9494, // Salmon
338: #be6862, // Terra Cotta
339: #562424, // Cocoa
340: #f1e7c7, // Wheat
341: #fef3bb, // Buttermilk
342: #e0b2d0, // Mauve
343: #d490bd, // Sunrise
344: #965555, // Tawny
345: #8f4c2a, // Rust
346: #d3be96, // Cashmere
347: #e2dcbc, // Khaki
348: #edeaea, // Lily white
349: #e9dada, // Seashell
350: #883e3e, // Burgundy
351: #bc9b5d, // Cork
352: #c7ac78, // Burlap
353: #cabfa3, // Beige
354: #bbb3b2, // Oyster
355: #6c584b, // Pine Cone
356: #a0844f, // Fawn brown
357: #958988, // Hurricane grey
358: #aba89e, // Cloudy grey
359: #af9483, // Linen
360: #966766, // Copper
361: #564236, // Dirt brown
362: #7e683f, // Bronze
363: #69665c, // Flint
364: #5a4c42, // Dark taupe
365: #6a3909, // Burnt Sienna
1001: #f8f8f8, // Institutional white
1002: #cdcdcd, // Mid gray
1003: #111111, // Really black
1004: #ff0000, // Really red
1005: #ffaf00, // Deep orange
1006: #b480ff, // Alder
1007: #a34b4b, // Dusty Rose
1008: #c1be42, // Olive
1009: #ffff00, // New Yeller
1010: #0000ff, // Really blue
1011: #002060, // Navy blue
1012: #2154b9, // Deep blue
1013: #04afec, // Cyan
1014: #aa5500, // CGA brown
1015: #aa00aa, // Magenta
1016: #ff66cc, // Pink
1017: #ffaf00, // Deep orange
1018: #12eed4, // Teal
1019: #00ffff, // Toothpaste
1020: #00ff00, // Lime green
1021: #3a7d15, // Camo
1022: #7f8e64, // Grime
1023: #8c5b9f, // Lavender
1024: #afddff, // Pastel light blue
1025: #ffc9c9, // Pastel orange
1026: #b1a7ff, // Pastel violet
1027: #9ff3e9, // Pastel blue-green
1028: #ccffcc, // Pastel green
1029: #ffffcc, // Pastel yellow
1030: #ffcc99, // Pastel brown
1031: #6225d1, // Royal purple
1032: #ff00bf // Hot pink
);
@each $color-id, $color in $brickColors {
.vb-bc-#{$color-id} {
background-color: $color;
}
}

View File

@ -1,4 +1,4 @@
// © XlXi 2021
// © XlXi 2023
@function tint-color($color, $percent){
@return mix(white, $color, $percent);

View File

@ -1,10 +1,11 @@
// © XlXi 2021
// © XlXi 2023
@use "sass:math";
// Lumen 5.0.1
// Bootswatch
@import "BrickColors";
@import "./scss/fontawesome.scss";
@import "./scss/brands.scss";
@import "./scss/duotone.scss";
@ -45,6 +46,22 @@ img.twemoji {
// Shop
.vb-avatar-editor-card {
height: 300px;
}
.vb-wardrobe-nav > .nav-item > .nav-link {
padding-right: 0.5rem!important;
padding-left: 0.5rem!important;
padding-bottom: 0!important;
padding-top: 0!important;
}
.vb-wardrobe-nav > .nav-item > .nav-link:not(:last-child) {
margin-left: 0.25rem!important;
}
.virtubrick-tokens {
background: 0 1px/contain no-repeat url("/images/symbols/token.svg");
color: #e59800!important;
@ -154,6 +171,11 @@ img.twemoji {
background-size: cover;
}
.virtubrick-item-card > span > a > img {
background: url("/Images/Item-Image-Vignette.png");
background-size: cover;
}
.virtubrick-game-card {
@media (max-width: 576px) {
width: math.div(100%, 2)
@ -176,6 +198,28 @@ img.twemoji {
}
}
.virtubrick-avatar-card {
@media (max-width: 576px) {
width: math.div(100%, 2)
}
@media (min-width: 576px) {
width: math.div(100%, 3)
}
@media (min-width: 768px) {
width: math.div(100%, 4)
}
@media (min-width: 992px) {
width: math.div(100%, 5)
}
@media (min-width: 1200px) {
width: 157px;
}
}
.virtubrick-shop-overlay {
position: absolute;
width: 100%;
@ -538,6 +582,65 @@ html {
}
}
// Avatar
.vb-character {
border: 1px solid $gray-500;
transition: background-color .1s;
margin: 0.125rem;
&-head {
margin: 0.125rem auto;
width: 50px!important;
height: 50px!important;
}
&-limb {
width: 50px!important;
height: 100px!important;
}
&-torso {
width: 104px!important;
height: 100px!important;
}
}
$hex-size: 32px;
$hex-margin: .04rem;
$hex-calc: calc(1.732 * $hex-size + 4 * $hex-margin - 1px);
.vb-hex {
display: flex;
text-align: left;
&-container {
font-size: 0;
& div {
width: $hex-size;
height: calc($hex-size * 1.1547);
margin: $hex-margin;
margin-bottom: calc($hex-margin - $hex-size * 0.2885);
display: inline-block;
font-size: initial;
clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
border: 0;
cursor: pointer;
}
&::before {
content: "";
width: calc($hex-size/2 + $hex-margin);
float: left;
height: 120%;
shape-outside: repeating-linear-gradient(
#0000 0 calc($hex-calc - 3px),
#000 0 $hex-calc);
}
}
}
// Navbar
.navbar {

View File

@ -2,22 +2,39 @@
@section('title', 'Avatar Editor')
@section('page-specific')
<script src="{{ mix('js/AvatarEditor.js') }}"></script>
@endsection
@section('content')
<div class="alert alert-yellow virtubrick-alert text-black">
<p>Todo:</p>
<ul>
<li>Section for wearing outfits</li>
</ul>
</div>
<div class="container my-2">
<h4>Avatar Editor</h4>
<div class="row mt-2">
<div class="col-3">
<div class="card text-center" id="vb-character-thumbnail">
<div
class="row mt-2"
id="vb-avatar-editor"
data-asset-thumbnail-2d="{{ route('thumbnails.v1.user', ['id' => Auth::user()->id, 'position' => 'full', 'type' => '2d']) }}"
data-asset-thumbnail-3d="{{ route('thumbnails.v1.user', ['id' => Auth::user()->id, 'position' => 'full', 'type' => '3d']) }}"
data-asset-name="{{ Auth::user()->username }}"
data-renderable3d="1"
>
<div class="col-lg-3">
<div class="card text-center vb-avatar-editor-card">
<img src="{{ Auth::user()->getImageUrl() }}" class="img-fluid vb-charimg" />
</div>
<h4 class="mt-3">Colors</h4>
<div class="card p-2" id="vb-character-colors">
<div class="card p-4">
<x-loader />
</div>
</div>
<div class="col-9">
<ul class="nav nav-tabs" id="vb-character-tabs">
<div class="mt-lg-0 mt-4 col-lg-9">
<ul class="nav nav-tabs">
<li class="nav-item">
<button class="nav-link active disabled" disabled>Wardrobe</button>
</li>
@ -25,24 +42,15 @@
<button class="nav-link disabled" disabled>Outfits</button>
</li>
</ul>
<div class="card p-2" id="vb-character-editor">
<div class="card p-2">
<x-loader />
</div>
<h4 class="mt-3">Currently Wearing</h4>
<div class="card p-2" id="vb-character-wearing">
<div class="card p-2">
<x-loader />
</div>
</div>
</div>
<p>Todo:</p>
<ul>
<li>Character Preview</li>
<li>3d Character Preview</li>
<li>Redraw button</li>
<li>Section for changing body colors</li>
<li>Section for wearing items</li>
<li>Section for taking off worn items</li>
</ul>
</div>
@endsection

View File

@ -8,21 +8,35 @@ Route::get('/ping', function() {
Route::group(['as' => 'maintenance.', 'prefix' => 'maintenance'], function() {
Route::group(['as' => 'v1.', 'prefix' => 'v1'], function() {
Route::post('/bypass', 'MaintenanceController@bypass')->name('bypass')->middleware('throttle:100,30');
Route::post('/bypass', 'MaintenanceController@bypass')->name('bypass')->middleware('throttle:20,30,maintenance');
});
});
Route::middleware('auth')->group(function () {
Route::group(['as' => 'client.', 'prefix' => 'auth'], function() {
Route::group(['as' => 'v1.', 'prefix' => 'v1'], function() {
Route::get('/generate-token', 'ClientController@generateAuthTicket')->name('generatetoken')->middleware('throttle:40,10');
Route::get('/generate-token', 'ClientController@generateAuthTicket')->name('generatetoken')->middleware('throttle:40,10,clientticket');
});
});
Route::group(['as' => 'feed.', 'prefix' => 'feed'], function() {
Route::group(['as' => 'v1.', 'prefix' => 'v1'], function() {
Route::get('/list-json', 'FeedController@listJson')->name('list');
Route::post('/share', 'FeedController@share')->name('share')->middleware('throttle:3,2');
Route::post('/share', 'FeedController@share')->name('share')->middleware('throttle:3,2,comments');
});
});
Route::group(['as' => 'avatar.', 'prefix' => 'avatar'], function() {
Route::middleware('throttle:100,10,renders')->group(function () {
Route::group(['as' => 'v1.', 'prefix' => 'v1'], function() {
Route::get('/list', 'AvatarController@listAssets')->name('list');
Route::get('/wearing', 'AvatarController@listWearing')->name('listWearing');
Route::get('/body-color', 'AvatarController@getBodyColors')->name('getBodyColors');
Route::post('/wear', 'AvatarController@wearAsset')->name('wear');
Route::post('/unwear', 'AvatarController@removeAsset')->name('remove');
Route::post('/redraw', 'AvatarController@redrawUser')->name('redraw');
Route::post('/set-body-color', 'AvatarController@setBodyColor')->name('setBodyColor');
});
});
});
@ -43,7 +57,7 @@ Route::middleware('auth')->group(function () {
Route::group(['as' => 'comments.', 'prefix' => 'comments'], function() {
Route::group(['as' => 'v1.', 'prefix' => 'v1'], function() {
Route::get('/list-json', 'CommentsController@listJson')->name('list');
Route::post('/share', 'CommentsController@share')->name('share')->middleware(['auth', 'throttle:3,2']);
Route::post('/share', 'CommentsController@share')->name('share')->middleware(['auth', 'throttle:3,2,comments']);
});
});

View File

@ -127,6 +127,10 @@ Route::group(['as' => 'auth.', 'namespace' => 'Auth'], function() {
Route::withoutMiddleware(['csrf'])->group(function () {
Route::group(['as' => 'client.'], function() {
Route::get('/asset', 'ClientController@asset')->name('asset');
Route::group(['prefix' => 'asset'], function() {
Route::get('/CharacterFetch.ashx', 'ClientAvatarController@characterFetch')->name('characterFetch');
Route::get('/BodyColors.ashx', 'ClientAvatarController@bodyColors')->name('bodyColors');
});
Route::group(['as' => 'game.', 'prefix' => 'game'], function() {
Route::post('/PlaceLauncher', 'ClientGameController@placeLauncher')->name('placelauncher');

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
<External>null</External>
<External>nil</External>
<Item class="BodyColors">
<Properties>
<int name="HeadColor"></int>
<int name="LeftArmColor"></int>
<int name="LeftLegColor"></int>
<string name="Name">Body Colors</string>
<int name="RightArmColor"></int>
<int name="RightLegColor"></int>
<int name="TorsoColor"></int>
<bool name="archivable">true</bool>
</Properties>
</Item>
</roblox>

View File

@ -12,6 +12,7 @@ mix.js('resources/js/app.js', 'public/js')
.js('resources/js/pages/Shop.js', 'public/js')
.js('resources/js/pages/Games.js', 'public/js')
.js('resources/js/pages/Transactions.js', 'public/js')
.js('resources/js/pages/AvatarEditor.js', 'public/js')
.js('resources/js/pages/Item.js', 'public/js')
.js('resources/js/pages/Place.js', 'public/js')
.js('resources/js/pages/AppDeployer.js', 'public/js/adm')