36 Commits
Author SHA1 Message Date
stijn 213f9987d9 Remove particle component and its associated routes and security controls. 2025-07-03 20:08:56 +02:00
stijn 48cac607de Add route for lazy-loaded Login component. 2025-07-03 20:02:07 +02:00
stijn 6ed2e15017 Parametrize notification server URL configuration for improved flexibility. 2025-06-29 03:17:25 +02:00
stijn 7fc25f46f3 Add endpoints, services, and security controls for particle file management, including save and download APIs. 2025-06-29 03:15:39 +02:00
stijn c72703ea32 Refactor user privilege handling to use Optional instead of null checks. Remove unused cache entries and update security configuration to refine access controls. 2025-06-23 21:34:54 +02:00
auto e837a9216d Fix slider sticking out of page 2025-06-23 00:25:31 +02:00
auto d4363b3a8a Add particle type selection, size control, and enhance particle property handling 2025-06-23 00:23:03 +02:00
auto 1e5862bae6 Add new particle types and enhance particle attributes handling 2025-06-23 00:04:30 +02:00
auto daf88ea437 Add opacity control for intersection plane 2025-06-22 23:24:06 +02:00
auto 9abd570b87 Add support for darkmode 2025-06-22 23:15:06 +02:00
auto 5284d498f3 Add a reset camera button and implement default camera reset functionality 2025-06-22 20:53:11 +02:00
auto c3a7be82e9 Add an option to highlight particles 2025-06-22 20:46:08 +02:00
auto fdb57289f8 Fix weird interactions between plane and player model 2025-06-22 20:20:40 +02:00
auto 60c1329163 Switch to steve 2025-06-22 20:17:02 +02:00
auto 0e71c0f581 Fix textures overlapping and glitching for legs 2025-06-22 20:15:35 +02:00
auto eb67a33331 Change default camera position 2025-06-22 20:12:25 +02:00
auto 39f20796ce Simplify UV mapping logic and remove unnecessary face rotation handling 2025-06-22 20:08:42 +02:00
auto e00165c56f Fix uv rotation 2025-06-22 20:04:26 +02:00
auto 02c6497700 Fix mapping 2025-06-22 20:01:11 +02:00
auto 0efd476676 Initial attempt at textures 2025-06-22 19:53:27 +02:00
auto 237518638c Increase particles-list height in FramesComponent to 550px for improved visibility. 2025-06-22 19:34:20 +02:00
auto fea1a98cea Adjust RendererService to use dynamic container height and update OrbitControls limits 2025-06-22 19:27:55 +02:00
auto ecd9b3d824 Modularize renderer and plane control functionality into RenderContainerComponent 2025-06-22 19:23:54 +02:00
auto 9808b5d63d Add manual plane orientation controls with lock/unlock functionality
Implemented a UI overlay in `ParticlesComponent` for manual plane orientation selection with buttons for different orientations. Added lock/unlock toggle to control automatic orientation adjustment. Refactored `IntersectionPlaneService` to support locked state and manual orientation updates. Updated styles and layout to integrate the new controls seamlessly.
2025-06-22 19:16:32 +02:00
auto c13b7077a7 Remove unused OnInit lifecycle hook from ParticlesComponent and cleanup redundant comments. 2025-06-22 19:08:28 +02:00
auto 023ae809ef Modularize ParticlesComponent by separating particle, frame, and property functionalities into dedicated components
Refactored `ParticlesComponent` to use `ParticleComponent`, `FramesComponent`, and `PropertiesComponent` for better organization and reusability. Updated layout, improved UI structure, and centralized particle management logic. Enhanced clipboard functionality with a new JSON copy feature.
2025-06-22 19:06:52 +02:00
auto 3a6f137c9a Update ParticlesComponent to allow dynamic plane position limits and improve slider configuration
Modified plane position logic to use dynamic min/max offsets based on orientation. Enhanced slider component with binding to these offsets and updated display text for clarity. Refactored `IntersectionPlaneService` to expose new methods (`getMaxOffset`, `getMinOffset`) and adjusted position defaults.
2025-06-22 17:59:29 +02:00
auto cb8447a096 Correct default plane locations 2025-06-22 17:38:28 +02:00
auto 3e98e1a498 add plane orientation for looking from above/below 2025-06-22 17:32:32 +02:00
auto 4c31a91bb4 Add modular services for Three.js integration in ParticlesComponent
Implemented dedicated Angular services (e.g., RendererService, InputHandlerService, PlayerModelService) to modularize and simplify Three.js integration within the `ParticlesComponent`. Refactored component logic to delegate rendering, input handling, and model creation to respective services. Updated particle data models and removed redundant logic from the component.
2025-06-22 17:26:10 +02:00
auto d6faaba01c Update security configuration to allow unrestricted access
Changed default security rule to permit all requests instead of requiring authentication. Adjusted `SecurityConfig` to ensure open access for all endpoints.
2025-06-22 01:15:45 +02:00
auto 56175e62d6 Remove RemoveTrailingPeriodPipe import from AboutComponent 2025-06-22 01:10:53 +02:00
auto 283838f444 Enhance ParticlesComponent to adjust plane position dynamically based on rotation angle. 2025-06-22 01:10:21 +02:00
auto 49fe335c73 Refactor ParticlesComponent: simplify plane creation, remove legacy handler, and enhance quadrant logic with material updates. 2025-06-22 01:03:17 +02:00
auto 1875f050c6 Add orbit controls and mouse interaction to ParticlesComponent
Integrated Three.js `OrbitControls` for smoother camera navigation and implemented new mouse interaction methods (`onMouseDown`, `onMouseUp`, `onMouseMove`) for enhanced usability. Adjusted scene, camera, and renderer setup for better responsiveness.
2025-06-22 00:55:38 +02:00
auto 52d8658be3 Add particle creator component with Three.js integration CREDITS: Junie
Introduced the `ParticlesComponent` for creating and visualizing particles using Three.js. Added associated routes, HTML, SCSS, and tests. Updated `package.json` to include `three` and `@types/three` as dependencies. Adjusted app navigation to include the particles page.
2025-06-22 00:40:16 +02:00
48 changed files with 2655 additions and 313 deletions
@@ -36,11 +36,11 @@ public class SecurityConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http return http
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/login/userLogin/**", "/login/requestNewUserLogin/**").permitAll()
.requestMatchers("/team/**", "/history/**").permitAll()
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue()) .requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.anyRequest().authenticated() // .requestMatchers("/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.requestMatchers("/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.anyRequest().permitAll()
) )
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
@@ -83,8 +83,6 @@ public class LoginController implements LoginApi {
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login") @RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
@Override @Override
public ResponseEntity<String> login(String code) { public ResponseEntity<String> login(String code) {
CacheEntry cacheEntry1 = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
cache.put("23232323", cacheEntry1);
if (code == null) { if (code == null) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
@@ -134,12 +132,12 @@ public class LoginController implements LoginApi {
Instant now = Instant.now(); Instant now = Instant.now();
//TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour) //TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour)
Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30)); Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30));
CompletableFuture<PrivilegedUser> privilegedUserCompletableFuture = new CompletableFuture<>(); CompletableFuture<Optional<PrivilegedUser>> privilegedUserCompletableFuture = new CompletableFuture<>();
List<PermissionClaimDto> claimList = new ArrayList<>(); List<PermissionClaimDto> claimList = new ArrayList<>();
Connection.getConnection(Databases.DEFAULT) Connection.getConnection(Databases.DEFAULT)
.runQuery(sqlSession -> { .runQuery(sqlSession -> {
try { try {
PrivilegedUser privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class) Optional<PrivilegedUser> privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class)
.getUserByUuid(uuid.toString()); .getUserByUuid(uuid.toString());
privilegedUserCompletableFuture.complete(privilegedUser); privilegedUserCompletableFuture.complete(privilegedUser);
@@ -148,17 +146,15 @@ public class LoginController implements LoginApi {
privilegedUserCompletableFuture.completeExceptionally(e); privilegedUserCompletableFuture.completeExceptionally(e);
} }
}); });
PrivilegedUser privilegedUser = privilegedUserCompletableFuture.join(); Optional<PrivilegedUser> privilegedUser = privilegedUserCompletableFuture.join();
claimList.add(PermissionClaimDto.USER); claimList.add(PermissionClaimDto.USER);
if (privilegedUser != null) { privilegedUser.ifPresent(user -> user.getPermissions().forEach(permission -> {
privilegedUser.getPermissions().forEach(permission -> { try {
try { claimList.add(PermissionClaimDto.valueOf(permission));
claimList.add(PermissionClaimDto.valueOf(permission)); } catch (IllegalArgumentException e) {
} catch (IllegalArgumentException e) { log.warn("Received invalid permission claim: {}", permission);
log.warn("Received invalid permission claim: {}", permission); }
} }));
});
}
JwtClaimsSet claims = JwtClaimsSet.builder() JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("altitudeweb") .issuer("altitudeweb")
.claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList()) .claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList())
@@ -0,0 +1,176 @@
package com.alttd.altitudeweb.controllers.particles;
import com.alttd.altitudeweb.api.ParticlesApi;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@Slf4j
@RequiredArgsConstructor
@RestController
public class ParticleController implements ParticlesApi {
@Value("${login.secret:#{null}}")
private String loginSecret;
@Value("${particles.file_path}")
private String particlesFilePath;
@Value("${notification.server.url:http://localhost:8080}")
private String notificationServerUrl;
@Override
public ResponseEntity<Resource> downloadFile(String authorization, String filename) throws Exception {
if (authorization == null || !authorization.equals(loginSecret)) {
return ResponseEntity.status(401).build();
}
File file = new File(particlesFilePath);
if (!file.exists() || !file.isDirectory()) {
log.error("Particles file path {} is not a directory, not downloading particles file", particlesFilePath);
return ResponseEntity.status(404).build();
}
File targetFile = new File(file, filename);
return getFileForDownload(targetFile, filename);
}
@Override
public ResponseEntity<Resource> downloadFileForUser(String authorization, String uuid, String filename) throws Exception {
if (authorization == null || !authorization.equals(loginSecret)) {
return ResponseEntity.status(401).build();
}
File file = new File(particlesFilePath);
if (!file.exists() || !file.isDirectory()) {
log.error("Particles file path {} is not a directory, not downloading particles user file", particlesFilePath);
return ResponseEntity.status(404).build();
}
File targetDir = new File(file, uuid);
if (targetDir.exists()) {
return getFileForDownload(targetDir, filename);
} else {
log.warn("User {} does not have a directory for particles files", uuid);
return ResponseEntity.notFound().build();
}
}
private ResponseEntity<Resource> getFileForDownload(File file, String filename) {
File targetFile = new File(file, filename);
if (!targetFile.exists()) {
log.warn("Particles file {} does not exist", targetFile.getAbsolutePath());
return ResponseEntity.notFound().build();
}
if (!targetFile.isFile()) {
log.warn("Particles file {} is not a file", targetFile.getAbsolutePath());
return ResponseEntity.status(404).build();
}
try {
Path path = targetFile.toPath();
ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));
return ResponseEntity.ok()
.contentLength(targetFile.length())
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.body(resource);
} catch (IOException e) {
log.error("Failed to read particles file {}: {}", targetFile.getAbsolutePath(), e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@Override
public ResponseEntity<Void> saveFile(String filename, MultipartFile content) throws Exception {
File file = new File(particlesFilePath);
if (!file.exists() || !file.isDirectory()) {
log.error("Particles file path {} is not a directory, not saving particles file", particlesFilePath);
return ResponseEntity.status(404).build();
}
ResponseEntity<Void> voidResponseEntity = writeContentToFile(file, filename, content);
notifyServerOfFileUpload(filename);
return voidResponseEntity;
}
@Override
public ResponseEntity<Void> saveFileForUser(String uuid, String filename, MultipartFile content) throws Exception {
File file = new File(particlesFilePath);
if (!file.exists() || !file.isDirectory()) {
log.error("Particles file path {} is not a directory, not saving particles user file", particlesFilePath);
return ResponseEntity.status(404).build();
}
File targetDir = new File(file, uuid);
if (!file.exists()) {
log.debug("Creating particles directory {}", targetDir.getAbsolutePath());
if (targetDir.mkdirs()) {
log.info("Created particles user directory {}", targetDir.getAbsolutePath());
}
}
ResponseEntity<Void> voidResponseEntity = writeContentToFile(file, filename, content);
notifyServerOfFileUpload(uuid, filename);
return voidResponseEntity;
}
private void notifyServerOfFileUpload(String filename) {
String notificationUrl = String.format("%s/notify/%s.json", notificationServerUrl, filename);
sendNotification(notificationUrl, String.format("file upload: %s", filename));
}
private void notifyServerOfFileUpload(String uuid, String filename) {
String notificationUrl = String.format("%s/notify/%s/%s.json", notificationServerUrl, uuid, filename);
sendNotification(notificationUrl, String.format("file upload for user %s: %s", uuid, filename));
}
private void sendNotification(String notificationUrl, String logDescription) {
try {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.getForEntity(notificationUrl, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
log.info("Successfully notified server of {}", logDescription);
} else {
log.warn("Failed to notify server of {}, status: {}",
logDescription, response.getStatusCode());
}
} catch (Exception e) {
log.error("Error notifying server of {}", logDescription, e);
}
}
private ResponseEntity<Void> writeContentToFile(File dir, String filename, MultipartFile content) {
File targetFile = new File(dir, filename);
if (!Files.isWritable(targetFile.toPath())) {
log.error("Particles file {} is not writable", targetFile.getAbsolutePath());
return ResponseEntity.status(403).build();
}
if (targetFile.exists()) {
log.warn("Overwriting existing particles file {}", targetFile.getAbsolutePath());
}
try {
content.transferTo(targetFile);
} catch (Exception e) {
log.error("Failed to write particles file {}", targetFile.getAbsolutePath(), e);
return ResponseEntity.status(500).build();
}
return ResponseEntity.ok().build();
}
}
@@ -6,4 +6,6 @@ database.user=${DB_USER:root}
database.password=${DB_PASSWORD:root} database.password=${DB_PASSWORD:root}
cors.allowed-origins=${CORS:https://alttd.com} cors.allowed-origins=${CORS:https://alttd.com}
login.secret=${LOGIN_SECRET:SET_TOKEN} login.secret=${LOGIN_SECRET:SET_TOKEN}
particles.file_path=${user.home}/.altitudeweb/particles
notification.server.url=${SERVER_IP:10.0.0.107}:${SERVER_PORT:8080}
logging.level.com.alttd.altitudeweb=INFO logging.level.com.alttd.altitudeweb=INFO
@@ -1,8 +1,10 @@
package com.alttd.altitudeweb.database.web_db; package com.alttd.altitudeweb.database.web_db;
import org.apache.ibatis.annotations.*; import org.apache.ibatis.annotations.*;
import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface PrivilegedUserMapper { public interface PrivilegedUserMapper {
@@ -23,7 +25,7 @@ public interface PrivilegedUserMapper {
@Result(property = "permissions", column = "id", javaType = List.class, @Result(property = "permissions", column = "id", javaType = List.class,
many = @Many(select = "getPermissionsForUser")) many = @Many(select = "getPermissionsForUser"))
}) })
PrivilegedUser getUserByUuid(@Param("uuid") String uuid); Optional<PrivilegedUser> getUserByUuid(@Param("uuid") String uuid);
/** /**
* Retrieves all privileged users with their permissions * Retrieves all privileged users with their permissions
+2
View File
@@ -22,8 +22,10 @@
"@angular/platform-browser": "^19.2.0", "@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0", "@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0", "@angular/router": "^19.2.0",
"@types/three": "^0.177.0",
"ngx-cookie-service": "^19.1.2", "ngx-cookie-service": "^19.1.2",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"three": "^0.177.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.15.0" "zone.js": "~0.15.0"
}, },
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+1 -3
View File
@@ -2,15 +2,13 @@ import {Component} from '@angular/core';
import {ScrollService} from '../scroll/scroll.service'; import {ScrollService} from '../scroll/scroll.service';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '../header/header.component';
import {RemoveTrailingPeriodPipe} from "../util/RemoveTrailingPeriodPipe";
@Component({ @Component({
selector: 'app-about', selector: 'app-about',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
HeaderComponent, HeaderComponent
RemoveTrailingPeriodPipe
], ],
templateUrl: './about.component.html', templateUrl: './about.component.html',
styleUrl: './about.component.scss' styleUrl: './about.component.scss'
+8 -2
View File
@@ -5,6 +5,10 @@ export const routes: Routes = [
path: '', path: '',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent) loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
}, },
{
path: 'particles',
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
},
{ {
path: 'map', path: 'map',
loadComponent: () => import('./map/map.component').then(m => m.MapComponent) loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
@@ -105,6 +109,8 @@ export const routes: Routes = [
path: 'forms', path: 'forms',
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent) loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
}, },
{
path: 'login',
loadComponent: () => import('./login/login.component').then(m => m.LoginDialogComponent)
},
]; ];
@@ -1,79 +0,0 @@
<ng-container>
<app-header [current_page]="'community'" height="460px" background_image="/public/img/backgrounds/community.jpg"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Community</h1>
<h2>Talented people who help Altitude in more than one way.</h2>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<div class="customContainer">
<h2>Current Nitro Boosters</h2>
</div>
</section>
<section id="social" class="darkmodeSectionThree">
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
<h2 class="sectionTitle">Social Media</h2>
</div>
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
<p style="text-align: center;">We're currently not looking for more people to help manage our socials.</p>
</div>
</section>
<section id="crateTeam" class="darkmodeSection">
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
<h2 class="sectionTitle">Crate Team</h2>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
<h2 class="sectionTitle">Event Leaders</h2>
</div>
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
<p style="text-align: center;">We're currently not looking for more Event Leaders.</p>
</div>
</section>
<section class="darkmodeSection">
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
<h2 class="sectionTitle">Event Team</h2>
</div>
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
<div style="flex-direction: column;">
<p style="text-align: center;">We occasionally open applications for the event team.</p>
<p style="text-align: center;">If you're interested in joining you simply need to keep an eye on the Discord
announcements.</p>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
<h2 class="sectionTitle">YouTubers & Streamers</h2>
</div>
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
<div style="flex-direction: column;">
<p style="text-align: center;"><a style="cursor: pointer;" id="reqButton">Show Requirements...</a></p>
</div>
</div>
<div id="req" class="hide" style="display: flex; justify-content: center; padding-bottom: 30px;">
<div style="flex-direction: column; justify-content: center; max-width: 800px;">
<p style="text-align: center;"><span style="font-family: 'opensans-bold', sans-serif;">Requirements:</span>
</p>
<p style="text-align: center;">You need to have at least one recent stream/video on Altitude which we can use
to gauge if your audience enjoys your content on Altitude.</p>
<br>
<p style="text-align: center;">Twitch: You need to be affiliate and get at least 5 viewers on average while
streaming on Altitude.</p>
<p style="text-align: center;">YouTube videos: You need at least 500 subs and have at least 200 views per
video within a week on average for Altitude content.</p>
<p style="text-align: center;">YouTube streamers: You need at least 500 subs and have at least 5 viewers on
average while streaming on Altitude.</p>
<br>
<p style="text-align: center;"><span style="font-family: 'opensans-bold', sans-serif;">Note:</span> Before
accepting or denying you we will watch your latest video/stream on Altitude (so keep your broadcasts public
on twitch).</p>
</div>
</div>
</section>
</main>
</ng-container>
@@ -1,12 +0,0 @@
.customContainer {
width: 80%;
max-width: 1020px;
margin: auto;
padding: 80px 0;
text-align: center;
}
.hide {
display: none !important;
}
@@ -1,14 +0,0 @@
import {Component} from '@angular/core';
import {HeaderComponent} from "../header/header.component";
@Component({
selector: 'app-community',
imports: [
HeaderComponent
],
templateUrl: './community.component.html',
styleUrl: './community.component.scss'
})
export class CommunityComponent {
}
@@ -1,53 +0,0 @@
<ng-container>
<app-header [current_page]="'nickgenerator'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Nickname Generator</h1>
<h2>Customize your in-game nickname</h2>
<h3 style="font-family: 'minecraft-text', sans-serif; font-size: 0.8rem; margin-top: 10px;">Made by TheParm</h3>
</div>
</app-header>
<main>
<!-- <section class="darkmodeSection">
<div class="container containerNick">
<div style="padding: 0 5% 0 5%;">
<div id="parts" class="previewNickDiv">
</div>
<div class="previewNickDiv">
<input type="button" class="button" value="Add Part" onclick="addPart()"/>
<input type="button" class="button" value="Remove Part" onclick="deletePart()"/>
</div>
<br><br><br><br>
<div id="commandTry" class="previewNickDiv">
<div id="try" class="command darkBg"></div>
<input type="button" class="button copy" value="Copy" onclick="copy(this)"/>
</div>
<div id="commandRequest" class="previewNickDiv">
<div id="request" class="command darkBg"></div>
<input type="button" class="button copy" value="Copy" onclick="copy(this)"/>
</div>
<div id="preview" class="preview darkBg previewNickDiv">
</div>
<div id="template" class='part' style="display: none">
<p style="font-family: 'minecraft-text', sans-serif">
Text: <input type="text" id="text" class="textPart" size=18 oninput="inputChanged()"/>
Gradient: <input type="checkbox" id="grad" class="gradPart" oninput="onGradient(this)"/>
<input id="colorA" type="text" class="coloris colorAPart color" value="#ffffff" oninput="inputChanged()"/>
<input id="colorB" type="text" class="coloris colorBPart color" value="#ffffff" oninput="inputChanged()"/>
Continuation: <input type="checkbox" id="cont" class="contPart" disabled oninput="onContinuation(this)"/>
<span id="invalid" class="invalidPart" style="display: none">(min 1 - max 16 chars)</span>
</p>
</div>
<div style="margin-top: 20px; text-align: center;">
<p style="font-family: 'minecraft-text', sans-serif">
Usage: Add as many parts as you wish, then apply the color and/or gradient, and copy/paste the command
into the minecraft chat. The total length of the nickname should be between 3 and 16 characters. Use the
continuation checkbox to continue the gradient from the last gradient color.
</p>
</div>
</div>
</div>
</section> -->
</main>
</ng-container>
@@ -1,14 +0,0 @@
import {Component} from '@angular/core';
import {HeaderComponent} from "../header/header.component";
@Component({
selector: 'app-nickgenerator',
imports: [
HeaderComponent
],
templateUrl: './nickgenerator.component.html',
styleUrl: './nickgenerator.component.scss'
})
export class NickgeneratorComponent {
}
@@ -1,82 +0,0 @@
<ng-container>
<app-header [current_page]="'nicknames'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>How To Get A Nickname</h1>
<h2>Personalize your writing and nickname in-game by choosing some of the millions of custom colors that we have
to offer!</h2>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Creating a Nickname</h2>
<p>Donors that have the Duke or Archduke rank have the ability to create custom nicknames for themselves. A
nickname should be similar enough to the players username for that player to be identifiable, and it
<span style="font-family: 'opensans-bold', sans-serif;">must not exceed 16 characters in length</span>. In
general, a nickname should include the main part of a players actual username to avoid confusion.</p>
</div>
<div class="columnParagraph">
<h2>Make it Your Own</h2>
<p>Altitude now supports the full range of RGB colors, which includes more than 16 million different hues.
In addition, the previous basic color codes still exist and are able to be used in combination with the
RGB colors. You can still use <span style="font-family: 'opensans-bold', sans-serif;">/colors</span> to
see the basic color codes that are available for use. You can also make use of <span
style="font-family: 'opensans-bold', sans-serif;">/colorsextra</span> which shows many different RGB
color options. These colors will be in the form of <span
style="font-family: 'opensans-bold', sans-serif;">#XXXXXX</span>; write down the colors that you like so
that you can easily use them later.</p>
<img ngSrc="/public/img/random/colors.png" alt="RGB colors" style="width: 100%; padding:0;" height="130"
width="480">
<p>Output of /colorsextra</p>
<p>Furthermore, you can also use the <a [routerLink]="['/nickgenerator']">nickname generation tool</a> to
make and preview potential nicknames - as well as copying the commands to use in-game.</p>
<p>Players are encouraged to play around and personalize their nicknames, as long as they follow the rules
and are legible! There are endless possibilities beyond simple gradients as well; players can create any
combination of standard colors and gradients that they choose.</p>
<p>Even if you arent a Duke or Archduke, you can try out colors like this on signs in-game.</p>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<h2>Request Your Nickname</h2>
<p>Players can only submit a new nickname request using <span
style="font-family: 'opensans-bold', sans-serif;">/nick request &lt;name&gt;</span> once per day. When a
nickname is requested, it will notify staff for near-instant approval. Creating custom nicknames using RGB
can be difficult, so we have implemented <span style="font-family: 'opensans-bold', sans-serif;">/nick try &lt;name&gt;</span>
so that you can experiment with your nicknames as many times as you want. It is highly encouraged to use
this command before submitting a formal request to staff. You can select an RGB color by putting the <span
style="font-family: 'opensans-bold', sans-serif;">#XXXXXX</span> code into curly braces before your
text.</p>
<p>We also support gradients in nicknames. You can specify the start and finish color and your nickname will
automatically find a gradient that goes between the two endpoints. <span
style="font-family: 'opensans-bold', sans-serif;">The symbol &gt; is used to start a gradient, and the symbol &lt; is used to close a gradient.</span>
You can have as many gradients as you want in your nickname, as long as it is readable.</p>
<p><span style="font-family: 'opensans-bold', sans-serif;">/nick try &lbrace;#003380&rbrace;Player</span>
would result in a blue name that read as “Player”.</p>
<p><span style="font-family: 'opensans-bold', sans-serif;">/nick try &lbrace;#003380>&rbrace;Player&lbrace;#0000FF<&rbrace;</span>
would result in the nickname reading as “Player” with a blue gradient fading across the letters in the
name.</p>
</div>
<div class="columnParagraph">
<h2>Useful Commands</h2>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/nick help</span> - Shows a list of all useful
nickname commands and how to use them.
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/nick try name</span> - Allows players to see
how a nickname would look as many times as you like.
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/nick request name</span> - Allows players to
submit a new nickname to staff for approval. This function can only be used once per day.
</li>
</ul>
</div>
</div>
</section>
</section>
</main>
</ng-container>
@@ -1,18 +0,0 @@
import {Component} from '@angular/core';
import {HeaderComponent} from "../header/header.component";
import {NgOptimizedImage} from '@angular/common';
import {RouterLink} from '@angular/router';
@Component({
selector: 'app-nicknames',
imports: [
HeaderComponent,
NgOptimizedImage,
RouterLink
],
templateUrl: './nicknames.component.html',
styleUrl: './nicknames.component.scss'
})
export class NicknamesComponent {
}
@@ -0,0 +1,48 @@
<div class="card-div">
<mat-card>
<mat-card-header>
<mat-card-title>Frames</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="frames-container">
<mat-tab-group [selectedIndex]="frames.indexOf(currentFrame)"
(selectedIndexChange)="switchFrame(frames[$event])">
<mat-tab *ngFor="let frameId of frames" [label]="frameId">
<div class="frame-content">
<h3>Particles in {{ frameId }}</h3>
<div class="particles-list">
<div *ngFor="let particle of particleData.frames[frameId]; let i = index" class="particle-item">
<span class="particle-item-text">
Particle {{ i + 1 }}: ({{ particle.x.toFixed(2) }}, {{ particle.y.toFixed(2) }}
, {{ particle.z.toFixed(2) }})
</span>
<button mat-icon-button (click)="removeParticle(frameId, i)">
<mat-icon>delete</mat-icon>
</button>
<button mat-icon-button (click)="highlightParticle(frameId, i)">
<mat-icon>lightbulb</mat-icon>
</button>
</div>
<div *ngIf="!particleData.frames[frameId] || particleData.frames[frameId].length === 0"
class="no-particles">
No particles in this frame. Click on the plane to add particles.
</div>
</div>
<div class="frame-actions">
<button mat-raised-button color="warn" (click)="removeFrame(frameId)"
[disabled]="frames.length <= 1">
Remove Frame
</button>
</div>
</div>
</mat-tab>
</mat-tab-group>
<div class="add-frame">
<button mat-raised-button color="primary" (click)="addFrame()">
Add New Frame
</button>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,61 @@
.frames-container {
margin-top: 10px;
}
.frame-content {
padding: 15px;
}
.particles-list {
height: 550px;
overflow-y: auto;
border: 1px solid #eee;
border-radius: 4px;
padding: 10px;
margin-bottom: 15px;
}
.particle-item {
display: flex;
align-items: center;
padding: 8px;
border-bottom: 1px solid #eee;
gap: 10px;
}
.particle-item-text {
flex-grow: 1;
}
.particle-item:last-child {
border-bottom: none;
}
.no-particles {
padding: 20px;
text-align: center;
color: var(--color-primairy);
}
.frame-actions {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.add-frame {
margin-top: 15px;
display: flex;
justify-content: center;
}
.card-div {
mat-card {
background-color: var(--color-primary);
color: var(--font-color);
}
}
span {
color: var(--font-color);
}
@@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NicknamesComponent } from './nicknames.component'; import { FramesComponent } from './frames.component';
describe('NicknamesComponent', () => { describe('FramesComponent', () => {
let component: NicknamesComponent; let component: FramesComponent;
let fixture: ComponentFixture<NicknamesComponent>; let fixture: ComponentFixture<FramesComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NicknamesComponent] imports: [FramesComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(NicknamesComponent); fixture = TestBed.createComponent(FramesComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -0,0 +1,89 @@
import {Component} from '@angular/core';
import {MatButton, MatIconButton} from "@angular/material/button";
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
import {MatTab, MatTabGroup} from "@angular/material/tabs";
import {NgForOf, NgIf} from "@angular/common";
import {ParticleData} from '../../models/particle.model';
import {MatIcon} from '@angular/material/icon';
import {ParticleManagerService} from '../../services/particle-manager.service';
import {FrameManagerService} from '../../services/frame-manager.service';
@Component({
selector: 'app-frames',
imports: [
MatButton,
MatCard,
MatCardContent,
MatCardHeader,
MatCardTitle,
MatIcon,
MatIconButton,
MatTab,
MatTabGroup,
NgForOf,
NgIf
],
templateUrl: './frames.component.html',
styleUrl: './frames.component.scss'
})
export class FramesComponent {
constructor(
private particleManagerService: ParticleManagerService,
private frameManagerService: FrameManagerService) {
}
/**
* Get the particle data
*/
public get particleData(): ParticleData {
return this.particleManagerService.getParticleData();
}
/**
* Get the current frame
*/
public get currentFrame(): string {
return this.particleManagerService.getCurrentFrame();
}
/**
* Get all frames
*/
public get frames(): string[] {
return this.particleManagerService.getFrames();
}
/**
* Add a new frame
*/
public addFrame(): void {
this.frameManagerService.addFrame();
}
/**
* Switch to a different frame
*/
public switchFrame(frameId: string): void {
this.frameManagerService.switchFrame(frameId);
}
/**
* Remove a particle
*/
public removeParticle(frameId: string, index: number): void {
this.particleManagerService.removeParticle(frameId, index);
}
/**
* Remove a frame
*/
public removeFrame(frameId: string): void {
this.frameManagerService.removeFrame(frameId);
}
public highlightParticle(frameId: string, i: number) {
this.particleManagerService.highlightParticle(frameId, i);
}
}
@@ -0,0 +1,37 @@
<div class="card-div">
<mat-card class="particle-card">
<mat-card-header>
<mat-card-title>Particle Properties</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="particle-properties">
<div class="property-row">
<div class="color-picker">
<input type="color" [(ngModel)]="selectedColor">
<span>Current color: {{ selectedColor }}</span>
</div>
<mat-form-field appearance="fill" class="type-field">
<mat-label>Select Particle Type</mat-label>
<input type="text"
placeholder="Search for a particle type"
matInput
[formControl]="particleTypeControl"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
<mat-option *ngFor="let type of filteredParticleTypes | async" [value]="type">
{{ type }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
<div class="size-slider">
<mat-slider min="0.1" max="4" step="0.1" class="full-width">
<input matSliderThumb [(ngModel)]="selectedSize">
</mat-slider>
<span>Size: {{ selectedSize }}</span>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,61 @@
.particle-card {
margin-top: 20px;
margin-bottom: 20px;
}
.particle-properties {
display: flex;
flex-direction: column;
gap: 20px;
}
.property-row {
display: flex;
align-items: center;
gap: 15px;
width: 100%;
}
.type-field {
flex: 1;
min-width: 20ch;
max-width: 40ch;
}
.color-picker {
display: flex;
flex: 1;
align-items: center;
gap: 10px;
}
.color-picker input[type="color"] {
width: 40px;
height: 40px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.card-div {
mat-card {
background-color: var(--color-primary);
color: var(--font-color);
}
}
.full-width {
width: 100%;
}
.size-slider {
width: 95%;
display: flex;
flex-direction: column;
margin-top: 5px;
}
span {
color: var(--font-color);
font-size: 0.9rem;
}
@@ -0,0 +1,23 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ParticleComponent} from './particle.component';
describe('ParticleComponent', () => {
let component: ParticleComponent;
let fixture: ComponentFixture<ParticleComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ParticleComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ParticleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,115 @@
import {Component, OnInit} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {ParticleManagerService} from '../../services/particle-manager.service';
import {Particle} from '../../models/particle.model';
import {MatSliderModule} from '@angular/material/slider';
import {MatSelectModule} from '@angular/material/select';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {Observable} from 'rxjs';
import {map, startWith} from 'rxjs/operators';
import {AsyncPipe, NgForOf} from '@angular/common';
@Component({
selector: 'app-particle',
imports: [
MatCard,
MatCardContent,
MatCardHeader,
MatCardTitle,
ReactiveFormsModule,
FormsModule,
MatSliderModule,
MatSelectModule,
MatFormFieldModule,
MatInputModule,
MatAutocompleteModule,
NgForOf,
AsyncPipe
],
templateUrl: './particle.component.html',
styleUrl: './particle.component.scss'
})
export class ParticleComponent implements OnInit {
// Available particle types from the enum
particleTypes = Object.values(Particle);
// Form control for the particle type dropdown with filtering
particleTypeControl = new FormControl();
filteredParticleTypes: Observable<string[]>;
constructor(
private particleManagerService: ParticleManagerService,
) {
this.filteredParticleTypes = this.particleTypeControl.valueChanges.pipe(
startWith(''),
map(value => this._filterParticleTypes(value || ''))
);
}
ngOnInit() {
// Initialize the particle type control with the current value
this.particleTypeControl.setValue(this.selectedParticle);
// Update the selected particle when the control value changes
this.particleTypeControl.valueChanges.subscribe(value => {
if (value && Object.values(Particle).includes(value)) {
this.selectedParticle = value;
}
});
}
private _filterParticleTypes(value: string): string[] {
const filterValue = value.toLowerCase();
return this.particleTypes.filter(type => type.toLowerCase().includes(filterValue));
}
// Display function for the autocomplete
displayFn(particle: string): string {
return particle ? particle : '';
}
/**
* Get the selected color
*/
public get selectedColor(): string {
return this.particleManagerService.getSelectedColor();
}
/**
* Set the selected color
*/
public set selectedColor(color: string) {
this.particleManagerService.setSelectedColor(color);
}
/**
* Get the selected particle type
*/
public get selectedParticle(): Particle {
return this.particleManagerService.particle;
}
/**
* Set the selected particle type
*/
public set selectedParticle(particle: Particle) {
this.particleManagerService.particle = particle;
}
/**
* Get the selected particle size
*/
public get selectedSize(): number {
return this.particleManagerService.size;
}
/**
* Set the selected particle size
*/
public set selectedSize(size: number) {
this.particleManagerService.size = size;
}
}
@@ -0,0 +1,93 @@
<div class="card-div">
<mat-card>
<mat-card-header>
<mat-card-title>Particle Properties</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Particle Name</mat-label>
<input matInput [(ngModel)]="particleData.particle_name" placeholder="Enter particle name">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Display Name</mat-label>
<input matInput [(ngModel)]="particleData.display_name" placeholder="Enter display name">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Particle Type</mat-label>
<mat-select [(ngModel)]="particleData.particle_type">
<mat-option *ngFor="let type of particleTypes" [value]="type">{{ type }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Lore</mat-label>
<textarea matInput [(ngModel)]="particleData.lore" placeholder="Enter lore"></textarea>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Display Item</mat-label>
<input matInput [(ngModel)]="particleData.display_item" placeholder="Enter display item">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Permission</mat-label>
<input matInput [(ngModel)]="particleData.permission" placeholder="Enter permission">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Package Permission</mat-label>
<input matInput [(ngModel)]="particleData.package_permission" placeholder="Enter package permission">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Frame Delay</mat-label>
<input matInput type="number" [(ngModel)]="particleData.frame_delay" placeholder="Enter frame delay">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Repeat</mat-label>
<input matInput type="number" [(ngModel)]="particleData.repeat" placeholder="Enter repeat count">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Repeat Delay</mat-label>
<input matInput type="number" [(ngModel)]="particleData.repeat_delay"
placeholder="Enter repeat delay">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Random Offset</mat-label>
<input matInput type="number" [(ngModel)]="particleData.random_offset"
placeholder="Enter random offset">
</mat-form-field>
</div>
<div class="form-row">
<mat-checkbox [(ngModel)]="particleData.stationary"><span>Stationary</span></mat-checkbox>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,14 @@
.form-row {
margin-bottom: 15px;
}
.card-div {
mat-card {
background-color: var(--color-primary);
color: var(--font-color);
}
}
span {
color: var(--font-color);
}
@@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NickgeneratorComponent } from './nickgenerator.component'; import { PropertiesComponent } from './properties.component';
describe('NickgeneratorComponent', () => { describe('PropertiesComponent', () => {
let component: NickgeneratorComponent; let component: PropertiesComponent;
let fixture: ComponentFixture<NickgeneratorComponent>; let fixture: ComponentFixture<PropertiesComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NickgeneratorComponent] imports: [PropertiesComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(NickgeneratorComponent); fixture = TestBed.createComponent(PropertiesComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -0,0 +1,43 @@
import {Component} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
import {MatCheckbox} from "@angular/material/checkbox";
import {MatFormField, MatInput, MatLabel} from "@angular/material/input";
import {NgForOf} from "@angular/common";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {ParticleData, ParticleType} from '../../models/particle.model';
import {MatSelect} from '@angular/material/select';
import {MatOption} from '@angular/material/core';
import {ParticleManagerService} from '../../services/particle-manager.service';
@Component({
selector: 'app-particle-properties',
imports: [
MatCard,
MatCardContent,
MatCardHeader,
MatCardTitle,
MatCheckbox,
MatFormField,
MatInput,
MatLabel,
MatOption,
MatSelect,
NgForOf,
ReactiveFormsModule,
FormsModule
],
templateUrl: './properties.component.html',
styleUrl: './properties.component.scss'
})
export class PropertiesComponent {
public particleTypes = Object.values(ParticleType);
constructor(
private particleManagerService: ParticleManagerService,
) {
}
public get particleData(): ParticleData {
return this.particleManagerService.getParticleData();
}
}
@@ -0,0 +1,54 @@
<div #rendererContainer class="renderer-container">
<div class="plane-controls-overlay">
<div>
<mat-form-field appearance="outline" style="width: 10ch">
<mat-label>Opacity</mat-label>
<input matInput type="number" [(ngModel)]="opacity" min="0" max="1" step="0.01" placeholder="">
</mat-form-field>
</div>
<div class="button-row">
<button mat-mini-fab color="primary" (click)="resetCamera()"
matTooltip="Reset camera">
<mat-icon>location_searching</mat-icon>
</button>
<button mat-mini-fab color="primary" (click)="togglePlaneLock()"
[matTooltip]="isPlaneLocked ? 'Unlock Plane' : 'Lock Plane'">
<mat-icon>{{ isPlaneLocked ? 'lock' : 'lock_open' }}</mat-icon>
</button>
</div>
<div *ngIf="isPlaneLocked" class="plane-orientation-buttons">
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_ABOVE)"
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_ABOVE"
matTooltip="Vertical Above">
<mat-icon>arrow_upward</mat-icon>
</button>
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_BELOW)"
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_BELOW"
matTooltip="Vertical Below">
<mat-icon>arrow_downward</mat-icon>
</button>
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_FRONT)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_FRONT"
matTooltip="Horizontal Front">
<mat-icon>arrow_forward</mat-icon>
</button>
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_BEHIND)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_BEHIND"
matTooltip="Horizontal Behind">
<mat-icon>arrow_back</mat-icon>
</button>
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_RIGHT)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_RIGHT"
matTooltip="Horizontal Right">
<mat-icon>arrow_right</mat-icon>
</button>
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_LEFT)"
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_LEFT"
matTooltip="Horizontal Left">
<mat-icon>arrow_left</mat-icon>
</button>
</div>
</div>
</div>
@@ -0,0 +1,53 @@
.renderer-container {
height: 1000px;
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden;
background-color: var(--color-primairy);
display: flex;
justify-content: center;
align-items: center;
position: relative; /* Added for absolute positioning of overlay */
}
.plane-controls-overlay {
position: absolute;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
z-index: 10;
}
.button-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 10px;
}
.plane-orientation-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 10px;
margin-top: 10px;
}
.plane-orientation-buttons button {
opacity: 0.7;
transition: opacity 0.2s, transform 0.2s;
}
.plane-orientation-buttons button:hover {
opacity: 1;
transform: scale(1.1);
}
.plane-orientation-buttons button.active {
opacity: 1;
transform: scale(1.1);
box-shadow: 0 0 10px var(--color-tertiary);
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RenderContainerComponent } from './render-container.component';
describe('RenderContainerComponent', () => {
let component: RenderContainerComponent;
let fixture: ComponentFixture<RenderContainerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RenderContainerComponent]
})
.compileComponents();
fixture = TestBed.createComponent(RenderContainerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,123 @@
import {AfterViewInit, Component, ElementRef, OnDestroy, ViewChild} from '@angular/core';
import {MatMiniFabButton} from '@angular/material/button';
import {NgIf} from '@angular/common';
import {IntersectionPlaneService, PlaneOrientation} from '../../services/intersection-plane.service';
import {MatIcon} from '@angular/material/icon';
import {MatTooltip} from '@angular/material/tooltip';
import {RendererService} from '../../services/renderer.service';
import {PlayerModelService} from '../../services/player-model.service';
import {InputHandlerService} from '../../services/input-handler.service';
import {FormsModule} from '@angular/forms';
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
@Component({
selector: 'app-render-container',
imports: [
MatIcon,
MatMiniFabButton,
MatTooltip,
NgIf,
FormsModule,
MatInput,
MatFormField,
MatLabel
],
templateUrl: './render-container.component.html',
styleUrl: './render-container.component.scss'
})
export class RenderContainerComponent implements AfterViewInit, OnDestroy {
@ViewChild('rendererContainer') rendererContainer!: ElementRef;
constructor(
private intersectionPlaneService: IntersectionPlaneService,
private playerModelService: PlayerModelService,
private inputHandlerService: InputHandlerService,
private rendererService: RendererService,
) {
}
ngAfterViewInit(): void {
this.initializeScene();
this.animate();
}
/**
* Clean up resources when component is destroyed
*/
ngOnDestroy(): void {
if (this.rendererService.renderer) {
this.inputHandlerService.cleanup(this.rendererService.renderer.domElement);
}
}
/**
* Initialize the 3D scene and all related components
*/
private initializeScene(): void {
this.rendererService.initializeRenderer(this.rendererContainer);
this.playerModelService.loadSkinTexture('/public/img/skins/steve.png')
.then(() => {
// Then create the player model with the texture applied
this.playerModelService.createPlayerModel();
});
this.intersectionPlaneService.createIntersectionPlane();
this.inputHandlerService.initializeInputHandlers(this.rendererService.renderer.domElement);
}
/**
* Animation loop
*/
private animate(): void {
requestAnimationFrame(this.animate.bind(this));
this.intersectionPlaneService.updatePlaneOrientation(this.rendererService.camera);
this.rendererService.render();
}
/**
* Get whether the plane is locked
*/
public get isPlaneLocked(): boolean {
return this.intersectionPlaneService.isPlaneLocked();
}
public set opacity(opacity: number) {
this.intersectionPlaneService.currentOpacity = opacity;
}
public get opacity(): number {
return this.intersectionPlaneService.currentOpacity;
}
/**
* Toggle the plane locked state
*/
public togglePlaneLock(): void {
const newLockedState = !this.isPlaneLocked;
this.intersectionPlaneService.setPlaneLocked(newLockedState);
}
public resetCamera(): void {
this.rendererService.resetCamera();
}
/**
* Get the current plane orientation
*/
public get currentPlaneOrientation(): PlaneOrientation {
return this.intersectionPlaneService.getCurrentOrientation();
}
/**
* Set the plane orientation
*/
public setPlaneOrientation(orientation: PlaneOrientation): void {
this.intersectionPlaneService.setPlaneOrientation(orientation);
}
/**
* Get all available plane orientations
*/
public get planeOrientations(): typeof PlaneOrientation {
return PlaneOrientation;
}
}
@@ -0,0 +1,54 @@
/**
* Defines the types of particles available in the system
*/
export enum Particle {
DUST = 'DUST',
DUST_COLOR_TRANSITION = 'DUST_COLOR_TRANSITION',
TINTED_LEAVES = 'TINTED_LEAVES'
// Other particle types can be added later
}
export enum ParticleType {
HEAD = 'HEAD',
TRAIL = 'TRAIL',
BREAK_PLACE_BLOCK = 'BREAK_PLACE_BLOCK',
DEATH = 'DEATH',
KILL = 'KILL',
CLICK_BLOCK = 'CLICK_BLOCK',
TELEPORT_ARRIVE = 'TELEPORT',
}
/**
* Represents a single particle's information
*/
export interface ParticleInfo {
particle_type: Particle;
x: number;
y: number;
z: number;
extra?: number;
color?: string;
color_gradient_end?: string;
size?: number;
}
/**
* Represents the complete particle data structure
*/
export interface ParticleData {
particle_name: string;
display_name: string;
particle_type: string;
lore: string;
display_item: string;
permission: string;
package_permission: string;
frame_delay: number;
repeat: number;
repeat_delay: number;
random_offset: number;
stationary: boolean;
frames: {
[frameId: string]: ParticleInfo[];
};
}
@@ -0,0 +1,40 @@
<app-header current_page="particles" height="200px" background_image="/public/img/backgrounds/staff.png"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Particle Creator</h1>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<section class="column">
<div class="renderer-section column">
<div class="flex row">
<div class="flex side-column">
<app-particle-properties></app-particle-properties>
</div>
<div class="flex middle-column">
<app-render-container></app-render-container>
<div class="plane-controls">
<label>Plane Position (Z-axis):</label>
<mat-slider [min]="minOffset" [max]="maxOffset" step="1" #planeSlider>
<input matSliderThumb [(ngModel)]="planePosition" (input)="updatePlanePosition($event)">
</mat-slider>
<span>{{ planePosition }} offset from center</span>
</div>
</div>
<div class="flex side-column">
<app-particle></app-particle>
<app-frames></app-frames>
<div>
<button mat-fab extended (click)="copyJson()">
<mat-icon>content_copy</mat-icon>
Copy JSON to clipboard
</button>
</div>
</div>
</div>
</div>
</section>
</section>
</main>
@@ -0,0 +1,125 @@
.renderer-section {
flex: 1;
min-width: 300px;
display: flex;
}
.side-column {
flex: 1;
flex-direction: column;
gap: 20px;
}
.middle-column {
flex: 2;
flex-direction: column;
gap: 20px;
}
label, span {
color: var(--font-color);
}
.plane-controls {
margin-top: 10px;
padding: 10px;
background-color: var(--color-primairy);
border-radius: 4px;
display: flex;
align-items: center;
gap: 10px;
}
.plane-controls mat-slider {
flex: 1;
}
:host ::ng-deep {
.mdc-text-field--outlined {
background-color: var(--color-primary);
}
.mdc-text-field--outlined .mdc-floating-label,
.mdc-text-field--outlined .mdc-text-field__input,
.mat-mdc-form-field-label,
.mat-mdc-select-value-text,
.mat-mdc-select-arrow,
.mat-mdc-checkbox-label,
.mat-mdc-card-header,
.mat-mdc-card-title,
.mat-mdc-card-content {
color: var(--font-color) !important;
}
.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading,
.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__notch,
.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing {
border-color: var(--font-color) !important;
}
// Fix for dropdown menu
.mat-mdc-select-panel {
background-color: var(--color-primary) !important;
}
.mat-mdc-option {
color: var(--font-color) !important;
}
.mat-mdc-option:hover:not(.mdc-list-item--disabled) {
background-color: rgba(255, 255, 255, 0.1) !important;
}
.mat-mdc-option.mat-mdc-option-active {
background-color: rgba(255, 255, 255, 0.2) !important;
}
.mat-mdc-select-panel {
background-color: var(--color-primary) !important;
}
.mat-mdc-option {
color: var(--font-color) !important;
}
.mat-mdc-option:hover:not(.mdc-list-item--disabled) {
background-color: rgba(255, 255, 255, 0.1) !important;
}
.mat-mdc-option.mat-mdc-option-active {
background-color: rgba(255, 255, 255, 0.2) !important;
}
.mat-mdc-tab {
color: var(--font-color) !important;
}
.mat-mdc-tab-header {
background-color: var(--color-primary);
}
.mat-mdc-tab-label-container {
background-color: var(--color-primary);
}
.mdc-tab__content .mdc-tab__text-label {
color: var(--font-color) !important;
}
.mat-mdc-tab-label, .mat-mdc-tab-link {
color: var(--font-color) !important;
}
.mat-mdc-tab-group.mat-primary .mat-mdc-tab:not(.mat-mdc-tab-disabled) .mdc-tab-indicator__content--underline {
border-color: var(--font-color) !important;
}
.mat-mdc-tab-body-content {
background-color: var(--color-primary);
}
.mat-icon {
color: var(--font-color);
}
}
@@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CommunityComponent } from './community.component'; import { ParticlesComponent } from './particles.component';
describe('CommunityComponent', () => { describe('ParticlesComponent', () => {
let component: CommunityComponent; let component: ParticlesComponent;
let fixture: ComponentFixture<CommunityComponent>; let fixture: ComponentFixture<ParticlesComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CommunityComponent] imports: [ParticlesComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(CommunityComponent); fixture = TestBed.createComponent(ParticlesComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -0,0 +1,131 @@
import {Component, ElementRef, ViewChild} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatSelectModule} from '@angular/material/select';
import {MatSliderModule} from '@angular/material/slider';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatTabsModule} from '@angular/material/tabs';
import {MatCardModule} from '@angular/material/card';
import {MatIconModule} from '@angular/material/icon';
import {HeaderComponent} from '../header/header.component';
// Services
import {IntersectionPlaneService} from './services/intersection-plane.service';
import {ParticleManagerService} from './services/particle-manager.service';
// Models
import {PropertiesComponent} from './components/properties/properties.component';
import {ParticleComponent} from './components/particle/particle.component';
import {FramesComponent} from './components/frames/frames.component';
import {MatSnackBar} from '@angular/material/snack-bar';
import {RenderContainerComponent} from './components/render-container/render-container.component';
import {ParticlesService} from '../../api';
@Component({
selector: 'app-particles',
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatButtonModule,
MatInputModule,
MatFormFieldModule,
MatSelectModule,
MatSliderModule,
MatCheckboxModule,
MatTabsModule,
MatCardModule,
MatIconModule,
HeaderComponent,
PropertiesComponent,
ParticleComponent,
FramesComponent,
RenderContainerComponent,
],
templateUrl: './particles.component.html',
styleUrl: './particles.component.scss'
})
export class ParticlesComponent {
@ViewChild('planeSlider') planeSlider!: ElementRef;
constructor(
private intersectionPlaneService: IntersectionPlaneService,
private particleManagerService: ParticleManagerService,
private matSnackBar: MatSnackBar,
private particlesService: ParticlesService,
) {
}
/**
* Update plane position based on slider
*/
public updatePlanePosition(event: Event): void {
const slider = event.target as HTMLInputElement;
const value = Number(slider.value);
this.intersectionPlaneService.updatePlanePosition(value);
}
/**
* Get the current plane position
*/
public get planePosition(): number {
return this.intersectionPlaneService.getPlanePosition();
}
public set planePosition(newPlanePosition: number) {
this.intersectionPlaneService.updatePlanePosition(newPlanePosition);
}
public get maxOffset(): number {
return this.intersectionPlaneService.getMaxOffset();
}
public get minOffset(): number {
return this.intersectionPlaneService.getMinOffset();
}
/**
* Generate JSON output
*/
public generateJson(): string {
return this.particleManagerService.generateJson();
}
public getJsonFile(): Blob {
const jsonContent = this.generateJson();
return new Blob([jsonContent], {type: 'application/json'});
}
public saveJsonToFile(): void {
const jsonContent = this.generateJson();
const blob = new Blob([jsonContent], {type: 'application/json'});
const url = window.URL.createObjectURL(blob);
// Create a temporary link element
const a = document.createElement('a');
a.href = url;
a.download = 'particle-data.json';
// Append to the document, click it, and remove it
document.body.appendChild(a);
a.click();
// Clean up
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
this.matSnackBar.open('JSON file downloaded', '', {duration: 2000});
}
public copyJson() {
navigator.clipboard.writeText(this.generateJson()).then(() => {
this.matSnackBar.open('Copied to clipboard', '', {duration: 2000})
});
//TODO validation
this.particlesService.saveFile(this.particleManagerService.getParticleData().particle_name, this.getJsonFile());
}
}
@@ -0,0 +1,64 @@
import { Injectable } from '@angular/core';
import { ParticleManagerService } from './particle-manager.service';
/**
* Service responsible for managing animation frames
*/
@Injectable({
providedIn: 'root'
})
export class FrameManagerService {
constructor(private particleManager: ParticleManagerService) {}
/**
* Adds a new frame
*/
addFrame(): void {
const frames = this.particleManager.getFrames();
const frameId = `frame${frames.length + 1}`;
frames.push(frameId);
this.particleManager.setFrames(frames);
const particleData = this.particleManager.getParticleData();
particleData.frames[frameId] = [];
this.particleManager.setParticleData(particleData);
this.switchFrame(frameId);
}
/**
* Switches to a different frame
*/
switchFrame(frameId: string): void {
this.particleManager.setCurrentFrame(frameId);
this.particleManager.clearParticleVisuals();
this.particleManager.renderFrameParticles(frameId);
}
/**
* Removes a frame
*/
removeFrame(frameId: string): void {
const frames = this.particleManager.getFrames();
const index = frames.indexOf(frameId);
if (index !== -1) {
frames.splice(index, 1);
this.particleManager.setFrames(frames);
const particleData = this.particleManager.getParticleData();
delete particleData.frames[frameId];
this.particleManager.setParticleData(particleData);
// Switch to first frame if we removed the current one
if (frameId === this.particleManager.getCurrentFrame() && frames.length > 0) {
this.switchFrame(frames[0]);
} else if (frames.length === 0) {
// If no frames left, add one
this.addFrame();
}
}
}
}
@@ -0,0 +1,105 @@
import { Injectable, ElementRef } from '@angular/core';
import * as THREE from 'three';
import { RendererService } from './renderer.service';
import { IntersectionPlaneService } from './intersection-plane.service';
import { ParticleManagerService } from './particle-manager.service';
/**
* Service responsible for handling user input interactions
*/
@Injectable({
providedIn: 'root'
})
export class InputHandlerService {
private raycaster = new THREE.Raycaster();
private mouse = new THREE.Vector2();
private isDragging = false;
private mouseDownTime = 0;
constructor(
private rendererService: RendererService,
private intersectionPlaneService: IntersectionPlaneService,
private particleManagerService: ParticleManagerService
) {}
/**
* Initializes input event listeners
*/
initializeInputHandlers(rendererElement: HTMLElement): void {
rendererElement.addEventListener('mousedown', this.onMouseDown.bind(this));
rendererElement.addEventListener('mouseup', this.onMouseUp.bind(this));
rendererElement.addEventListener('mousemove', this.onMouseMove.bind(this));
window.addEventListener('resize', this.onWindowResize.bind(this));
}
/**
* Handles mouse down event
*/
private onMouseDown(event: MouseEvent): void {
this.isDragging = false;
this.mouseDownTime = Date.now();
}
/**
* Handles mouse up event
*/
private onMouseUp(event: MouseEvent): void {
// If mouse was down for less than 200ms and didn't move much, consider it a click, not a drag
if (Date.now() - this.mouseDownTime < 200 && !this.isDragging) {
this.handlePlaneClick(event);
}
this.isDragging = false;
}
/**
* Handles mouse move event
*/
private onMouseMove(event: MouseEvent): void {
// If mouse moves while button is pressed, it's a drag
if (event.buttons > 0) {
this.isDragging = true;
}
}
/**
* Handles mouse click on the plane
*/
private handlePlaneClick(event: MouseEvent): void {
// Calculate mouse position in normalized device coordinates
const rect = this.rendererService.renderer.domElement.getBoundingClientRect();
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// Update the picking ray with the camera and mouse position
this.raycaster.setFromCamera(this.mouse, this.rendererService.camera);
// Calculate objects intersecting the picking ray
const intersects = this.raycaster.intersectObject(this.intersectionPlaneService.getIntersectionPlane());
if (intersects.length > 0) {
const point = intersects[0].point;
this.particleManagerService.addParticle(point.x, point.y, point.z);
}
}
/**
* Handles window resize event
*/
private onWindowResize(): void {
// This is delegated to the renderer service
const container = this.rendererService.renderer.domElement.parentElement;
if (container) {
this.rendererService.onWindowResize(new ElementRef(container));
}
}
/**
* Removes event listeners
*/
cleanup(rendererElement: HTMLElement): void {
rendererElement.removeEventListener('mousedown', this.onMouseDown.bind(this));
rendererElement.removeEventListener('mouseup', this.onMouseUp.bind(this));
rendererElement.removeEventListener('mousemove', this.onMouseMove.bind(this));
window.removeEventListener('resize', this.onWindowResize.bind(this));
}
}
@@ -0,0 +1,265 @@
import {Injectable} from '@angular/core';
import * as THREE from 'three';
import {RendererService} from './renderer.service';
/**
* Represents the possible orientations of the intersection plane
*/
export enum PlaneOrientation {
VERTICAL_ABOVE,
VERTICAL_BELOW,
HORIZONTAL_FRONT,
HORIZONTAL_BEHIND,
HORIZONTAL_RIGHT,
HORIZONTAL_LEFT
}
/**
* Service responsible for managing the intersection plane
*/
@Injectable({
providedIn: 'root'
})
export class IntersectionPlaneService {
private intersectionPlane!: THREE.Mesh;
private planePosition: number = 0; // Position in 1/16th of a block
private currentOrientation: PlaneOrientation = PlaneOrientation.HORIZONTAL_FRONT;
private planeLocked: boolean = false;
private opacity: number = 0.05;
constructor(private rendererService: RendererService) {
}
/**
* Creates the intersection plane and adds it to the scene
*/
createIntersectionPlane(): THREE.Mesh {
const planeGeometry = new THREE.PlaneGeometry(3, 3);
const planeMaterial = new THREE.MeshBasicMaterial({
color: 0x00AA00,
transparent: true,
opacity: 0.05,
side: THREE.DoubleSide
});
this.intersectionPlane = new THREE.Mesh(planeGeometry, planeMaterial);
this.intersectionPlane.position.z = 0;
// Center the plane vertically with the player (player is about 2 blocks tall)
this.intersectionPlane.position.y = 1;
this.rendererService.scene.add(this.intersectionPlane);
this.intersectionPlane.renderOrder = 1;
return this.intersectionPlane;
}
/**
* Determines the plane orientation based on camera position
*/
private determinePlaneOrientation(camera: THREE.Camera): PlaneOrientation {
// Check if camera is looking from above or below first
const verticalAngle = Math.atan2(
camera.position.y,
Math.sqrt(camera.position.x * camera.position.x + camera.position.z * camera.position.z)
);
// Threshold angle for considering the camera to be above/below (about 45 degrees)
const verticalThreshold = Math.PI / 4;
if (verticalAngle > verticalThreshold) {
return PlaneOrientation.VERTICAL_ABOVE;
} else if (verticalAngle < -verticalThreshold) {
return PlaneOrientation.VERTICAL_BELOW;
} else {
// Calculate the angle between camera and player (in the XZ plane)
const cameraAngle = Math.atan2(
camera.position.x,
camera.position.z
);
// Determine which quadrant the camera is in with a 45-degree offset
const quadrant = Math.floor((cameraAngle + Math.PI + Math.PI / 4) / (Math.PI / 2)) % 4;
switch (quadrant) {
case 0:
return PlaneOrientation.HORIZONTAL_FRONT;
case 1:
return PlaneOrientation.HORIZONTAL_RIGHT;
case 2:
return PlaneOrientation.HORIZONTAL_BEHIND;
case 3:
return PlaneOrientation.HORIZONTAL_LEFT;
default:
return PlaneOrientation.HORIZONTAL_FRONT;
}
}
}
/**
* Updates the plane orientation based on camera position
*/
public updatePlaneOrientation(camera: THREE.Camera): void {
if (!this.intersectionPlane) return;
if (!this.planeLocked) {
this.currentOrientation = this.determinePlaneOrientation(camera);
}
this.updateIntersectionPlaneOrientation()
//Restrict plane position to the new bounds and update it
this.planePosition = Math.max(this.getMinOffset(), Math.min(this.getMaxOffset(), this.planePosition));
this.updatePlanePosition(this.planePosition);
}
/**
* Updates the plane position based on slider value
*/
public updatePlanePosition(value: number): void {
this.planePosition = value;
// Convert from 1/16th block to Three.js units
const position = (this.planePosition / 16)
this.intersectionPlane.position.y = 0.8;
this.intersectionPlane.position.x = 0;
this.intersectionPlane.position.z = 0;
// Position based on the current orientation
switch (this.currentOrientation) {
case PlaneOrientation.VERTICAL_ABOVE:
this.intersectionPlane.position.y = 0.8 - position;
break;
case PlaneOrientation.VERTICAL_BELOW:
this.intersectionPlane.position.y = 0.8 + position;
break;
case PlaneOrientation.HORIZONTAL_FRONT:
this.intersectionPlane.position.z = position;
break;
case PlaneOrientation.HORIZONTAL_BEHIND:
this.intersectionPlane.position.z = -position;
break;
case PlaneOrientation.HORIZONTAL_RIGHT:
this.intersectionPlane.position.x = position;
break;
case PlaneOrientation.HORIZONTAL_LEFT:
this.intersectionPlane.position.x = -position;
break;
}
}
/**
* Updates the plane material color
*/
private updatePlaneMaterial(color: number): void {
this.intersectionPlane.material = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: this.opacity,
side: THREE.DoubleSide
});
}
public set currentOpacity(opacity: number) {
this.opacity = opacity;
}
public get currentOpacity(): number {
return this.opacity;
}
/**
* Gets the intersection plane
*/
public getIntersectionPlane(): THREE.Mesh {
return this.intersectionPlane;
}
/**
* Gets the current plane position
*/
public getPlanePosition(): number {
return this.planePosition;
}
public getMaxOffset(): number {
switch (this.currentOrientation) {
case PlaneOrientation.VERTICAL_ABOVE:
case PlaneOrientation.VERTICAL_BELOW:
return 16;
case PlaneOrientation.HORIZONTAL_FRONT:
case PlaneOrientation.HORIZONTAL_BEHIND:
return 8;
case PlaneOrientation.HORIZONTAL_RIGHT:
case PlaneOrientation.HORIZONTAL_LEFT:
return 8;
}
}
public getMinOffset(): number {
return this.getMaxOffset() * -1;
}
/**
* Gets the current plane orientation
*/
public getCurrentOrientation(): PlaneOrientation {
return this.currentOrientation;
}
/**
* Sets the plane orientation manually
*/
public setPlaneOrientation(orientation: PlaneOrientation): void {
this.currentOrientation = orientation;
this.updateIntersectionPlaneOrientation();
this.updatePlanePosition(this.planePosition);
}
private updateIntersectionPlaneOrientation() {
switch (this.currentOrientation) {
case PlaneOrientation.VERTICAL_ABOVE:
this.intersectionPlane.rotation.x = -Math.PI / 2;
this.intersectionPlane.rotation.y = 0;
this.updatePlaneMaterial(0xAA0000);
break;
case PlaneOrientation.VERTICAL_BELOW:
this.intersectionPlane.rotation.x = Math.PI / 2;
this.intersectionPlane.rotation.y = 0;
this.updatePlaneMaterial(0xAA0000);
break;
case PlaneOrientation.HORIZONTAL_FRONT:
this.intersectionPlane.rotation.x = 0;
this.intersectionPlane.rotation.y = 0;
this.updatePlaneMaterial(0x00AA00);
break;
case PlaneOrientation.HORIZONTAL_BEHIND:
this.intersectionPlane.rotation.x = 0;
this.intersectionPlane.rotation.y = Math.PI;
this.updatePlaneMaterial(0x00AA00);
break;
case PlaneOrientation.HORIZONTAL_RIGHT:
this.intersectionPlane.rotation.x = 0;
this.intersectionPlane.rotation.y = Math.PI / 2;
this.updatePlaneMaterial(0x0000AA);
break;
case PlaneOrientation.HORIZONTAL_LEFT:
this.intersectionPlane.rotation.x = 0;
this.intersectionPlane.rotation.y = -Math.PI / 2;
this.updatePlaneMaterial(0x0000AA);
break;
}
}
/**
* Gets whether the plane orientation is locked
*/
public isPlaneLocked(): boolean {
return this.planeLocked;
}
/**
* Sets whether the plane orientation is locked
*/
public setPlaneLocked(locked: boolean): void {
this.planeLocked = locked;
}
}
@@ -0,0 +1,248 @@
import {Injectable} from '@angular/core';
import * as THREE from 'three';
import {RendererService} from './renderer.service';
import {Particle, ParticleData, ParticleInfo, ParticleType} from '../models/particle.model';
/**
* Service responsible for managing particles in the scene
*/
@Injectable({
providedIn: 'root'
})
export class ParticleManagerService {
private particles: THREE.Mesh[] = [];
private particleData: ParticleData = {
particle_name: '',
display_name: '',
particle_type: ParticleType.TRAIL,
lore: '',
display_item: 'DIRT',
permission: '',
package_permission: '',
frame_delay: 1,
repeat: 1,
repeat_delay: 0,
random_offset: 0,
stationary: true,
frames: {
'frame1': []
}
};
private currentFrame: string = 'frame1';
private frames: string[] = ['frame1'];
private selectedColor: string = '#ff0000';
private selectedParticle: Particle = Particle.DUST;
private selectedSize: number = 1;
constructor(private rendererService: RendererService) {
}
/**
* Adds a particle at the specified position
*/
addParticle(x: number, y: number, z: number): void {
// Create a visual representation of the particle
const particleGeometry = new THREE.SphereGeometry(0.03 * this.selectedSize, 16, 16);
const particleMaterial = new THREE.MeshBasicMaterial({color: this.selectedColor});
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
particleMesh.position.set(x, y, z);
this.rendererService.scene.add(particleMesh);
this.particles.push(particleMesh);
// Add to particle data
const hexColor = this.selectedColor.replace('#', '');
//TODO make this work for more than just type DUST
const particleInfo: ParticleInfo = {
particle_type: this.selectedParticle,
x: x,
y: y,
z: z,
color: hexColor,
// color_gradient_end: hexColor2,
extra: 1,
size: this.selectedSize
};
if (!this.particleData.frames[this.currentFrame]) {
this.particleData.frames[this.currentFrame] = [];
}
this.particleData.frames[this.currentFrame].push(particleInfo);
}
/**
* Clears all particle visuals from the scene
*/
clearParticleVisuals(): void {
for (const particle of this.particles) {
this.rendererService.scene.remove(particle);
}
this.particles = [];
}
/**
* Renders particles for a specific frame
*/
renderFrameParticles(frameId: string): void {
if (!this.particleData.frames[frameId]) return;
for (const particleInfo of this.particleData.frames[frameId]) {
const particleGeometry = new THREE.SphereGeometry(0.03 * (particleInfo.size ?? 1), 16, 16);
const color = this.getColor(particleInfo);
const particleMaterial = new THREE.MeshBasicMaterial({color});
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z);
this.rendererService.scene.add(particleMesh);
this.particles.push(particleMesh);
}
}
/**
* Removes a particle from a specific frame
*/
removeParticle(frameId: string, index: number): void {
if (this.particleData.frames[frameId] && this.particleData.frames[frameId].length > index) {
this.particleData.frames[frameId].splice(index, 1);
// Update visuals if this is the current frame
if (frameId === this.currentFrame) {
this.clearParticleVisuals();
this.renderFrameParticles(frameId);
}
}
}
highlightParticle(frameId: string, index: number): void {
if (!(this.particleData.frames[frameId] && this.particleData.frames[frameId].length > index)) {
return;
}
const particleInfo = this.particleData.frames[frameId][index];
const color = this.getColor(particleInfo);
const particleMaterial = new THREE.MeshBasicMaterial({color});
const particleGeometry = new THREE.SphereGeometry(0.03 * (particleInfo.size ?? 1), 16, 16);
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z);
this.rendererService.scene.add(particleMesh);
this.particles.push(particleMesh);
this.animatePulse(particleMesh, 3, () => {
this.rendererService.scene.remove(particleMesh);
this.clearParticleVisuals();
this.renderFrameParticles(this.currentFrame);
});
}
private animatePulse(mesh: THREE.Mesh, cycles: number, onComplete: () => void): void {
const duration = 300;
const maxScale = 0.08 / 0.03;
const startTime = performance.now();
const animate = (time: number) => {
const elapsed = (time - startTime) % duration;
const t = elapsed / (duration / 2);
const scaleFactor = t <= 1 ? 1 + (maxScale - 1) * t : maxScale - (maxScale - 1) * (t - 1);
mesh.scale.setScalar(scaleFactor);
if (time - startTime >= duration * cycles) {
onComplete();
} else {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
private getColor(particleInfo: ParticleInfo) {
if (particleInfo.color) {
const r = parseInt(particleInfo.color.substring(0, 2), 16) / 255;
const g = parseInt(particleInfo.color.substring(2, 4), 16) / 255;
const b = parseInt(particleInfo.color.substring(4, 6), 16) / 255;
return new THREE.Color(r, g, b);
} else {
return new THREE.Color(255, 0, 0);
}
}
/**
* Sets the selected color for new particles
*/
setSelectedColor(color: string): void {
this.selectedColor = color;
}
/**
* Gets the selected color
*/
getSelectedColor(): string {
return this.selectedColor;
}
public get particle(): Particle {
return this.selectedParticle;
}
public set particle(selectedParticle: Particle) {
this.selectedParticle = selectedParticle;
}
public get size(): number {
return this.selectedSize;
}
public set size(selectedSize: number) {
this.selectedSize = selectedSize;
}
/**
* Gets the particle data
*/
getParticleData(): ParticleData {
return this.particleData;
}
/**
* Sets the particle data
*/
setParticleData(data: ParticleData): void {
this.particleData = data;
}
/**
* Gets the current frame
*/
getCurrentFrame(): string {
return this.currentFrame;
}
/**
* Sets the current frame
*/
setCurrentFrame(frameId: string): void {
this.currentFrame = frameId;
}
/**
* Gets all frames
*/
getFrames(): string[] {
return this.frames;
}
/**
* Sets all frames
*/
setFrames(frames: string[]): void {
this.frames = frames;
}
/**
* Generates JSON output of the particle data
*/
generateJson(): string {
return JSON.stringify(this.particleData, null, 2);
}
}
@@ -0,0 +1,254 @@
import {Injectable} from '@angular/core';
import * as THREE from 'three';
import {RendererService} from './renderer.service';
@Injectable({
providedIn: 'root'
})
export class PlayerModelService {
private playerModel!: THREE.Group;
private skinTexture!: THREE.Texture;
private textureLoaded = false;
constructor(private rendererService: RendererService) {
}
/**
* Loads a Minecraft skin texture from a URL
* @param textureUrl The URL of the skin texture to load
* @returns A promise that resolves when the texture is loaded
*/
loadSkinTexture(textureUrl: string): Promise<void> {
return new Promise((resolve) => {
const loader = new THREE.TextureLoader();
loader.load(textureUrl, (texture) => {
// Set texture parameters
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
this.skinTexture = texture;
this.textureLoaded = true;
// If the player model already exists, rebuild it with textures
if (this.playerModel) {
// Remove old model
this.rendererService.scene.remove(this.playerModel);
// Create new model with textures
this.createPlayerModel();
}
resolve();
});
});
}
/**
* Creates a player model with Minecraft-style textures and adds it to the scene
*/
createPlayerModel(): THREE.Group {
this.playerModel = new THREE.Group();
if (this.textureLoaded) {
// Create textured model if texture is loaded
this.createTexturedPlayerModel();
} else {
// Create simple colored model if no texture is loaded
this.createSimplePlayerModel();
}
this.playerModel.renderOrder = 0;
this.rendererService.scene.add(this.playerModel);
return this.playerModel;
}
/**
* Creates a simple colored player model (without textures)
*/
private createSimplePlayerModel(): void {
// Head
const headGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
const headMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.y = 1.35;
this.playerModel.add(head);
// Body
const bodyGeometry = new THREE.BoxGeometry(0.5, 0.7, 0.25);
const bodyMaterial = new THREE.MeshLambertMaterial({color: 0x0000ff});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.y = 0.75;
this.playerModel.add(body);
// Arms
const armGeometry = new THREE.BoxGeometry(0.2, 0.7, 0.25);
const armMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
const leftArm = new THREE.Mesh(armGeometry, armMaterial);
leftArm.position.set(-0.35, 0.75, 0);
this.playerModel.add(leftArm);
const rightArm = new THREE.Mesh(armGeometry, armMaterial);
rightArm.position.set(0.35, 0.75, 0);
this.playerModel.add(rightArm);
// Legs
const legGeometry = new THREE.BoxGeometry(0.25, 0.7, 0.25);
const legMaterial = new THREE.MeshLambertMaterial({color: 0x000000});
const leftLeg = new THREE.Mesh(legGeometry, legMaterial);
leftLeg.position.set(-0.125, 0.15, 0);
this.playerModel.add(leftLeg);
const rightLeg = new THREE.Mesh(legGeometry, legMaterial);
rightLeg.position.set(0.125, 0.15, 0);
this.playerModel.add(rightLeg);
}
/**
* Creates a textured player model using the Minecraft skin
*/
private createTexturedPlayerModel(): void {
// Create the player with properly mapped textures
// Head - 8x8x8 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.5, 0.5, 0.5, // width, height, depth
[
{x: 8, y: 0, w: 8, h: 8}, // top
{x: 16, y: 0, w: 8, h: 8}, // bottom
{x: 16, y: 8, w: 8, h: 8}, // right
{x: 0, y: 8, w: 8, h: 8}, // left
{x: 8, y: 8, w: 8, h: 8}, // front
{x: 24, y: 8, w: 8, h: 8} // back
],
{x: 0, y: 1.35, z: 0} // position
));
// Body - 8x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.5, 0.7, 0.25, // width, height, depth
[
{x: 20, y: 16, w: 8, h: 4}, // top
{x: 28, y: 16, w: 8, h: 4}, // bottom
{x: 28, y: 20, w: 4, h: 12}, // right
{x: 16, y: 20, w: 4, h: 12}, // left
{x: 20, y: 20, w: 8, h: 12}, // front
{x: 32, y: 20, w: 8, h: 12} // back
],
{x: 0, y: 0.75, z: 0} // position
));
// Left Arm - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.2, 0.7, 0.25, // width, height, depth
[
{x: 44, y: 16, w: 4, h: 4}, // top
{x: 48, y: 16, w: 4, h: 4}, // bottom
{x: 48, y: 20, w: 4, h: 12}, // right
{x: 40, y: 20, w: 4, h: 12}, // left
{x: 44, y: 20, w: 4, h: 12}, // front
{x: 52, y: 20, w: 4, h: 12} // back
],
{x: -0.35, y: 0.75, z: 0} // position
));
// Right Arm - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.2, 0.7, 0.25, // width, height, depth
[
{x: 44, y: 16, w: 4, h: 4}, // top - mirror of left arm
{x: 48, y: 16, w: 4, h: 4}, // bottom - mirror of left arm
{x: 40, y: 20, w: 4, h: 12}, // right - mirror of left arm's left
{x: 48, y: 20, w: 4, h: 12}, // left - mirror of left arm's right
{x: 44, y: 20, w: 4, h: 12}, // front - same as left arm
{x: 52, y: 20, w: 4, h: 12} // back - same as left arm
],
{x: 0.35, y: 0.75, z: 0} // position
));
// Left Leg - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.26, 0.7, 0.26, // width, height, depth
[
{x: 4, y: 16, w: 4, h: 4}, // top
{x: 8, y: 16, w: 4, h: 4}, // bottom
{x: 8, y: 20, w: 4, h: 12}, // right
{x: 0, y: 20, w: 4, h: 12}, // left
{x: 4, y: 20, w: 4, h: 12}, // front
{x: 12, y: 20, w: 4, h: 12} // back
],
{x: -0.125, y: 0.15, z: 0} // position
));
// Right Leg - 4x12x4 pixels in the texture
this.playerModel.add(this.createBoxWithUvMapping(
0.26, 0.7, 0.26, // width, height, depth
[
{x: 4, y: 16, w: 4, h: 4}, // top - mirror of left leg
{x: 8, y: 16, w: 4, h: 4}, // bottom - mirror of left leg
{x: 0, y: 20, w: 4, h: 12}, // right - mirror of left leg's left
{x: 8, y: 20, w: 4, h: 12}, // left - mirror of left leg's right
{x: 4, y: 20, w: 4, h: 12}, // front - same as left leg
{x: 12, y: 20, w: 4, h: 12} // back - same as left leg
],
{x: 0.125, y: 0.15, z: 0} // position
));
}
/**
* Creates a box with proper UV mapping for a Minecraft character part
* @param width Width of the box
* @param height Height of the box
* @param depth Depth of the box
* @param uvMapping Array of UV coordinates for each face (top, bottom, right, left, front, back)
* @param position Position of the box
* @returns THREE.Mesh with properly mapped textures
*/
private createBoxWithUvMapping(
width: number,
height: number,
depth: number,
uvMapping: Array<{ x: number, y: number, w: number, h: number }>,
position: { x: number, y: number, z: number }
): THREE.Mesh {
const geometry = new THREE.BoxGeometry(width, height, depth);
// Remap the custom face order to BoxGeometry face order: px, nx, py, ny, pz, nz
const faceOrder = [2, 3, 0, 1, 4, 5]; // right, left, top, bottom, front, back
const textureWidth = 64;
const textureHeight = 64;
const uv = geometry.attributes['uv'];
for (let i = 0; i < 6; i++) {
const face = uvMapping[faceOrder[i]];
const x1 = face.x / textureWidth;
const y1 = 1 - face.y / textureHeight;
const x2 = (face.x + face.w) / textureWidth;
const y2 = 1 - (face.y + face.h) / textureHeight;
let uvs: [number, number][] = [
[x1, y1], // top-left
[x2, y1], // top-right
[x1, y2], // bottom-left
[x2, y2] // bottom-right
];
const uvOffset = i * 8;
for (let j = 0; j < 4; j++) {
uv.array[uvOffset + j * 2] = uvs[j][0];
uv.array[uvOffset + j * 2 + 1] = uvs[j][1];
}
}
uv.needsUpdate = true;
const material = new THREE.MeshBasicMaterial({
map: this.skinTexture,
transparent: true,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(position.x, position.y, position.z);
return mesh;
}
}
@@ -0,0 +1,117 @@
import {ElementRef, Injectable} from '@angular/core';
import * as THREE from 'three';
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
import {ThemeService} from '../../theme/theme.service';
import {THEME_MODE} from '../../constant';
/**
* Service responsible for managing the Three.js rendering environment
*/
@Injectable({
providedIn: 'root'
})
export class RendererService {
scene!: THREE.Scene;
camera!: THREE.PerspectiveCamera;
renderer!: THREE.WebGLRenderer;
controls!: OrbitControls;
private currentTheme: THEME_MODE = THEME_MODE.LIGHT;
constructor(private themeService: ThemeService) {
this.themeService.theme$.subscribe(theme => {
this.currentTheme = theme;
if (this.scene) {
this.setBackgroundColor(theme);
}
})
}
/**
* Initializes the Three.js scene, camera, renderer, and controls
*/
initializeRenderer(container: ElementRef): void {
// Create scene
this.scene = new THREE.Scene();
this.setBackgroundColor(this.currentTheme);
// Get container dimensions
const containerWidth = container.nativeElement.clientWidth;
const containerHeight = container.nativeElement.clientHeight;
// Create camera
this.camera = new THREE.PerspectiveCamera(75, containerWidth / containerHeight, 0.1, 1000);
this.camera.position.set(-1, 2, 3);
this.camera.lookAt(0, 1, 0);
// Create renderer
this.renderer = new THREE.WebGLRenderer({antialias: true});
this.renderer.setSize(containerWidth, containerHeight);
// Center the canvas in the container
this.renderer.domElement.style.display = 'block';
this.renderer.domElement.style.margin = 'auto';
container.nativeElement.appendChild(this.renderer.domElement);
// Initialize orbit controls
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.minDistance = 0.5;
this.controls.maxDistance = 4;
this.controls.target.set(0, 1, 0);
this.controls.update();
// Add lights
this.addLights();
}
private setBackgroundColor(theme: THEME_MODE) {
this.scene.background = new THREE.Color(this.currentTheme === THEME_MODE.DARK ? 0x242526 : 0xFBFBFE);
}
/**
* Resets the camera to its default position and orientation.
*/
public resetCamera(): void {
this.camera.position.set(-1, 2, 3);
this.camera.lookAt(0, 1, 0);
this.controls.target.set(0, 1, 0);
this.controls.update();
}
/**
* Adds lighting to the scene
*/
private addLights(): void {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1);
this.scene.add(directionalLight);
}
/**
* Handles window resize events
*/
onWindowResize(container: ElementRef): void {
const containerWidth = container.nativeElement.clientWidth;
const containerHeight = 400; // Fixed height as defined in CSS
this.camera.aspect = containerWidth / containerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(containerWidth, containerHeight);
}
/**
* Renders the scene
*/
render(): void {
if (this.controls) {
this.controls.update();
}
this.renderer.render(this.scene, this.camera);
}
}
+1
View File
@@ -1,4 +1,5 @@
@import '@angular/material/prebuilt-themes/azure-blue.css'; @import '@angular/material/prebuilt-themes/azure-blue.css';
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
:root { :root {
--white: #FFFFFF; --white: #FFFFFF;
+15
View File
@@ -10,11 +10,18 @@ components:
schemas: schemas:
PermissionClaim: PermissionClaim:
$ref: './schemas/permissions/permissions.yml#/components/schemas/PermissionClaim' $ref: './schemas/permissions/permissions.yml#/components/schemas/PermissionClaim'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
tags: tags:
- name: history - name: history
description: Retrieves punishment history description: Retrieves punishment history
- name: team - name: team
description: Retrieves information about the staff team description: Retrieves information about the staff team
- name: particles
description: All actions related to particles
paths: paths:
/team/{team}: /team/{team}:
$ref: './schemas/team/team.yml#/getTeam' $ref: './schemas/team/team.yml#/getTeam'
@@ -46,3 +53,11 @@ paths:
$ref: './schemas/login/login.yml#/RequestNewUserLogin' $ref: './schemas/login/login.yml#/RequestNewUserLogin'
/login/userLogin/{code}: /login/userLogin/{code}:
$ref: './schemas/login/login.yml#/UserLogin' $ref: './schemas/login/login.yml#/UserLogin'
/files/save/{filename}:
$ref: './schemas/particles/particles.yml#/SaveFile'
/files/save/{uuid}/{filename}:
$ref: './schemas/particles/particles.yml#/SaveFileForUser'
/files/download/{filename}/{secret}:
$ref: './schemas/particles/particles.yml#/DownloadFile'
/files/download/{uuid}/{filename}:
$ref: './schemas/particles/particles.yml#/DownloadFileForUser'
@@ -0,0 +1,176 @@
components:
parameters:
Filename:
name: filename
in: path
required: true
schema:
type: string
description: The name of the file
Secret:
name: Authorization
in: header
required: true
schema:
type: string
description: Secret
schemas:
FileData:
type: object
required:
- content
properties:
content:
type: string
format: binary
description: The content of the file
SaveFile:
post:
tags:
- particles
summary: Save a file
description: Save a file to the server (requires authorization)
operationId: saveFile
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/Filename'
requestBody:
required: true
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/FileData'
responses:
'200':
description: File saved successfully
'401':
description: Unauthorized - Invalid or missing token
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
'403':
description: Forbidden - Insufficient permissions
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
SaveFileForUser:
post:
tags:
- particles
summary: Save a file for a specific user
description: Save a file to the server for a specific user (requires head_mod permission)
operationId: saveFileForUser
security:
- bearerAuth: []
parameters:
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
- $ref: '#/components/parameters/Filename'
requestBody:
required: true
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/FileData'
responses:
'200':
description: File saved successfully
'401':
description: Unauthorized - Invalid or missing token
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
'403':
description: Forbidden - Insufficient permissions
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
DownloadFile:
get:
tags:
- particles
summary: Download a file
description: Download a file from the server using a secret key
operationId: downloadFile
parameters:
- $ref: '#/components/parameters/Secret'
- $ref: '#/components/parameters/Filename'
responses:
'200':
description: File downloaded successfully
content:
application/octet-stream:
schema:
type: string
format: binary
'404':
description: File not found
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
DownloadFileForUser:
get:
tags:
- particles
summary: Download a file for a specific user
description: Download a file from the server for a specific user (requires authorization)
operationId: downloadFileForUser
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/Secret'
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
- $ref: '#/components/parameters/Filename'
responses:
'200':
description: File downloaded successfully
content:
application/octet-stream:
schema:
type: string
format: binary
'401':
description: Unauthorized - Invalid or missing token
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
'404':
description: File not found
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'