19 Commits
Author SHA1 Message Date
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
auto 32a454c034 Refactor permission handling and authentication services
Replaced `PermissionClaim` enum with an OpenAPI-defined schema `PermissionClaimDto` for consistency across frontend and backend. Refactored authentication flow to utilize `AuthService` on the frontend, consolidating JWT handling logic. Removed redundant methods like `saveJwt` and integrated robust permission management throughout the application.
2025-06-21 23:15:46 +02:00
auto 07646e8c42 Implement enhanced login functionality with JWT, role-based permissions, and frontend integration
Added JWT-based login dialog with form validation and secure token handling on the frontend. Updated backend with role-based access control, privilege management, and refined security configurations. Extended database schema for user privileges and permissions.
2025-05-30 23:41:13 +02:00
Peter 20dcebbab9 Added new pages with fitting content: skyblock, custom features, guide book, ranks, commandlist, reducing lag, staff powers 2025-05-30 22:19:36 +02:00
auto c4c17b3adc Add JWT-based login flow with key pair generation
Introduced a secure login flow using JWTs with dynamically generated RSA key pairs stored in the database. Updated relevant APIs, database schema, and services to support login codes, JWT encoding, and secret validation.
2025-05-24 01:33:36 +02:00
auto cf758bfe60 Add endpoints and schema for history retrieval by UUID
Introduced a new API endpoint to fetch all punishment history for a specified UUID. Updated existing schemas, controllers, and mappers to support this functionality. Adjusted login endpoints to improve request handling and streamlined frontend form setup for appeals.
2025-05-03 04:37:47 +02:00
auto 8c7ec0a237 Restrict team member query to global world scope. 2025-05-02 01:33:45 +02:00
auto 80462218a7 Update role label for head mod 2025-05-02 01:32:15 +02:00
auto 26b5f86983 Add rate limiting to LoginController endpoints
Introduced a `@RateLimit` annotation to enforce limits on the `addLogin` and `login` methods in `LoginController`. This restricts the number of requests per minute to improve security and prevent abuse.
2025-04-26 23:14:33 +02:00
auto ba6cf6d938 Add rate limiting to AppealController methods
Introduced @RateLimit annotations to enforce request limits on the AppealController. The overall controller has a global limit of 30 requests per hour, while specific methods for Discord and Minecraft appeals are limited to 3 requests per hour. This aims to prevent abuse and improve system reliability.
2025-04-26 23:13:26 +02:00
auto 643545a18a Add appeal and login functionality structure
Introduces initial structure for appeal and login forms in both the frontend and backend. New controllers, APIs, and components were created, but functionality has not been fully implemented yet. This serves as a foundation for future development of these features.
2025-04-26 20:58:47 +02:00
auto b922487d76 Fix punishment tag colors and avatar image size 2025-04-20 17:29:51 +02:00
auto 54eb1ea735 Fixes #15 2025-04-20 17:19:50 +02:00
Peter 9043c774f7 Refactor ban details layout and enhanced formatting.
Updated the HTML structure and styling for a clearer, responsive layout and better user experience.
2025-04-20 17:00:31 +02:00
auto 43b75b8e74 Fixed punishment removed message closes #12 2025-04-19 22:19:08 +02:00
auto 07c77f5485 Update ban history formatting to include clearer unban details
Modified the history format service to display 'Unbanned by [user]' instead of just the user's name. This provides more explicit context for removed punishments in the UI.
2025-04-19 22:13:19 +02:00
90 changed files with 4560 additions and 108 deletions
+6 -3
View File
@@ -27,14 +27,15 @@ dependencies {
implementation(project(":open_api"))
implementation(project(":database"))
implementation(project(":frontend"))
implementation("org.springframework.boot:spring-boot-starter-web")
annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test")
implementation("com.mysql:mysql-connector-j:8.0.32")
implementation("org.mybatis:mybatis:3.5.13")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-configuration-processor")
implementation("org.springframework.boot:spring-boot-starter-hateoas")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.security:spring-security-oauth2-resource-server")
implementation("org.springframework.security:spring-security-oauth2-jose")
//AOP
implementation("org.aspectj:aspectjrt:1.9.19")
@@ -42,6 +43,8 @@ dependencies {
implementation("org.springframework:spring-aop")
implementation("org.springframework:spring-aspects")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.compileJava {
@@ -10,8 +10,8 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy;
public class AltitudeWebApplication {
public static void main(String[] args) {
SpringApplication.run(AltitudeWebApplication.class, args);
Connection.initDatabases();
SpringApplication.run(AltitudeWebApplication.class, args);
}
}
@@ -0,0 +1,65 @@
package com.alttd.altitudeweb.config;
import com.alttd.altitudeweb.controllers.login.KeyPairService;
import com.alttd.altitudeweb.model.PermissionClaimDto;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.web.SecurityFilterChain;
import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final KeyPairService keyPairService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login/userLogin/**", "/login/requestNewUserLogin/**").permitAll()
.requestMatchers("/team/**", "/history/**").permitAll()
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.build();
}
@Bean
public JwtEncoder jwtEncoder() {
KeyPair keyPair = keyPairService.getJwtSigningKeyPair();
JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.privateKey((RSAPrivateKey) keyPair.getPrivate())
.build();
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtDecoder jwtDecoder() {
KeyPair keyPair = keyPairService.getJwtSigningKeyPair();
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
}
}
@@ -0,0 +1,36 @@
package com.alttd.altitudeweb.controllers.application;
import com.alttd.altitudeweb.api.AppealsApi;
import com.alttd.altitudeweb.services.limits.RateLimit;
import com.alttd.altitudeweb.model.AppealResponseDto;
import com.alttd.altitudeweb.model.DiscordAppealDto;
import com.alttd.altitudeweb.model.MinecraftAppealDto;
import com.alttd.altitudeweb.model.UpdateMailDto;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.util.concurrent.TimeUnit;
@RestController
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.HOURS)
public class AppealController implements AppealsApi {
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
@Override
public ResponseEntity<MinecraftAppealDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported");
}
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal")
@Override
public ResponseEntity<AppealResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Minecraft appeals are not yet supported");
}
@Override
public ResponseEntity<AppealResponseDto> updateMail(UpdateMailDto updateMailDto) {
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Updating mail is not yet supported");
}
}
@@ -1,7 +1,7 @@
package com.alttd.altitudeweb.controllers.history;
import com.alttd.altitudeweb.api.HistoryApi;
import com.alttd.altitudeweb.controllers.limits.RateLimit;
import com.alttd.altitudeweb.services.limits.RateLimit;
import com.alttd.altitudeweb.model.HistoryCountDto;
import com.alttd.altitudeweb.model.PunishmentHistoryListDto;
import com.alttd.altitudeweb.setup.Connection;
@@ -153,6 +153,24 @@ public class HistoryApiController implements HistoryApi {
return ResponseEntity.ok().body(searchResultCountCompletableFuture.join());
}
@Override
public ResponseEntity<PunishmentHistoryListDto> getAllHistoryForUUID(String uuid) {
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
CompletableFuture<List<HistoryRecord>> historyRecordsCompletableFuture = new CompletableFuture<>();
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
log.debug("Loading all history for uuid {}", uuid);
try {
List<HistoryRecord> punishments = sqlSession.getMapper(UUIDHistoryMapper.class)
.getAllHistoryForUUID(UUID.fromString(uuid));
historyRecordsCompletableFuture.complete(punishments);
} catch (Exception e) {
log.error("Failed to load all history for uuid {}", uuid, e);
historyRecordsCompletableFuture.completeExceptionally(e);
}
});
return mapPunishmentHistory(punishmentHistoryList, historyRecordsCompletableFuture);
}
@Override
public ResponseEntity<PunishmentHistoryDto> getHistoryById(String type, Integer id) {
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
@@ -0,0 +1,102 @@
package com.alttd.altitudeweb.controllers.login;
import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.web_db.KeyPairEntity;
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
import com.alttd.altitudeweb.setup.Connection;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.concurrent.CompletableFuture;
@Service
@RequiredArgsConstructor
@Slf4j
public class KeyPairService {
private KeyPair cachedKeyPair = null;
private static final String RSA_ALGORITHM = "RSA";
private static final int RSA_KEY_SIZE = 2048;
public KeyPair getJwtSigningKeyPair() {
if (cachedKeyPair != null) {
return cachedKeyPair;
}
KeyPair keyPair = getOrCreateKeyPair();
if (keyPair != null) {
cachedKeyPair = keyPair;
return cachedKeyPair;
}
throw new IllegalStateException("Failed to generate or load key pair");
}
public KeyPair getOrCreateKeyPair() {
CompletableFuture<KeyPairEntity> keyPairFuture = new CompletableFuture<>();
Connection.getConnection(Databases.DEFAULT)
.runQuery(sqlSession -> {
log.debug("Loading key pair");
try {
KeyPairEntity entity = sqlSession.getMapper(KeyPairMapper.class).getKeyPair();
keyPairFuture.complete(entity);
} catch (Exception e) {
log.error("Failed to key pair", e);
keyPairFuture.completeExceptionally(e);
}
});
KeyPairEntity keyPairEntity = keyPairFuture.join();
if (keyPairEntity != null) {
try {
byte[] privateKeyBytes = Base64.getDecoder().decode(keyPairEntity.getPrivateKey());
byte[] publicKeyBytes = Base64.getDecoder().decode(keyPairEntity.getPublicKey());
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
return new KeyPair(publicKey, privateKey);
} catch (Exception e) {
log.error("Failed to load key pair from database", e);
}
}
KeyPair keyPair = generateKeyPair();
try {
KeyPairEntity entity = new KeyPairEntity();
entity.setPrivateKey(Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
entity.setPublicKey(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
entity.setCreatedAt(Instant.now());
Connection.getConnection(Databases.DEFAULT)
.runQuery(sqlSession -> {
log.debug("Saving key pair");
try {
sqlSession.getMapper(KeyPairMapper.class).save(entity);
log.info("Generated and saved new key pair");
} catch (Exception e) {
log.error("Failed to key pair", e);
}
});
} catch (Exception e) {
log.error("Failed to save key pair to database", e);
}
return keyPair;
}
private KeyPair generateKeyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
keyPairGenerator.initialize(RSA_KEY_SIZE);
return keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Error generating key pair", e);
}
}
}
@@ -0,0 +1,172 @@
package com.alttd.altitudeweb.controllers.login;
import com.alttd.altitudeweb.api.LoginApi;
import com.alttd.altitudeweb.model.PermissionClaimDto;
import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.web_db.PrivilegedUser;
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
import com.alttd.altitudeweb.setup.Connection;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import com.alttd.altitudeweb.services.limits.RateLimit;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
@Slf4j
@RequiredArgsConstructor
@RestController
public class LoginController implements LoginApi {
private final JwtEncoder jwtEncoder;
@Value("${login.secret:#{null}}")
private String loginSecret;
private record CacheEntry(UUID uuid, Instant expiry) {}
private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 300000) // 5 minutes in milliseconds
private void clearExpiredCacheEntries() {
Instant now = Instant.now();
int initialCacheSize = cache.size();
cache.entrySet().removeIf(entry -> entry.getValue().expiry().isBefore(now));
log.info("Cleared {} expired cache entries", initialCacheSize - cache.size());
}
@RateLimit(limit = 100, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "addLogin")
@Override
public ResponseEntity<String> requestLogin(String authorization, String uuid) {
UUID uuidFromString;
try {
uuidFromString = UUID.fromString(uuid);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
if (authorization == null || !authorization.startsWith("SECRET ")) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
String secret = authorization.substring("SECRET ".length());
if (!isValidSecret(secret)) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid secret");
}
Optional<String> key = cache.entrySet().stream()
.filter(entry -> entry.getValue().uuid.equals(uuidFromString))
.map(Map.Entry::getKey)
.findFirst();
if (key.isPresent()) {
return ResponseEntity.ok(key.get());
}
String loginCode = generateLoginCode(uuidFromString);
return ResponseEntity.ok(loginCode);
}
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
@Override
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) {
return ResponseEntity.badRequest().build();
}
CacheEntry cacheEntry = cache.get(code);
if (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
String token = generateToken(cacheEntry.uuid);
cache.remove(code);
return ResponseEntity.ok(token);
}
private String generateLoginCode(UUID uuid) {
String characters = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
StringBuilder loginCode = new StringBuilder();
for (int i = 0; i < 8; i++) {
int index = (int) (Math.random() * characters.length());
loginCode.append(characters.charAt(index));
}
CacheEntry cacheEntry = new CacheEntry(uuid,
Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(15)));
cache.put(loginCode.toString(), cacheEntry);
return loginCode.toString();
}
private boolean isValidSecret(String secret) {
if (loginSecret == null) {
log.warn("No login secret set, skipping secret validation");
return false;
}
if (loginSecret.length() < 16) {
log.warn("Login secret is too short, skipping secret validation");
return false;
}
if (!loginSecret.equals(secret)) {
log.info("Received invalid secret attempt");
return false;
}
return true;
}
private String generateToken(UUID uuid) {
Instant now = Instant.now();
//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));
CompletableFuture<PrivilegedUser> privilegedUserCompletableFuture = new CompletableFuture<>();
List<PermissionClaimDto> claimList = new ArrayList<>();
Connection.getConnection(Databases.DEFAULT)
.runQuery(sqlSession -> {
try {
PrivilegedUser privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class)
.getUserByUuid(uuid.toString());
privilegedUserCompletableFuture.complete(privilegedUser);
} catch (Exception e) {
log.error("Failed to load user by uuid", e);
privilegedUserCompletableFuture.completeExceptionally(e);
}
});
PrivilegedUser privilegedUser = privilegedUserCompletableFuture.join();
claimList.add(PermissionClaimDto.USER);
if (privilegedUser != null) {
privilegedUser.getPermissions().forEach(permission -> {
try {
claimList.add(PermissionClaimDto.valueOf(permission));
} catch (IllegalArgumentException e) {
log.warn("Received invalid permission claim: {}", permission);
}
});
}
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("altitudeweb")
.claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList())
.issuedAt(now)
.expiresAt(expiryTime)
.subject(uuid.toString())
.build();
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
@@ -1,7 +1,7 @@
package com.alttd.altitudeweb.controllers.team;
import com.alttd.altitudeweb.api.TeamApi;
import com.alttd.altitudeweb.controllers.limits.RateLimit;
import com.alttd.altitudeweb.services.limits.RateLimit;
import com.alttd.altitudeweb.setup.Connection;
import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.luckperms.Player;
@@ -1,4 +1,4 @@
package com.alttd.altitudeweb.controllers.limits;
package com.alttd.altitudeweb.services.limits;
import org.springframework.stereotype.Service;
@@ -1,4 +1,4 @@
package com.alttd.altitudeweb.controllers.limits;
package com.alttd.altitudeweb.services.limits;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1,4 +1,4 @@
package com.alttd.altitudeweb.controllers.limits;
package com.alttd.altitudeweb.services.limits;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -26,8 +26,8 @@ public class RateLimitAspect {
private final InMemoryRateLimiterService rateLimiterService;
@Around("""
@annotation(com.alttd.altitudeweb.controllers.limits.RateLimit)
|| @within(com.alttd.altitudeweb.controllers.limits.RateLimit)""")
@annotation(com.alttd.altitudeweb.services.limits.RateLimit)
|| @within(com.alttd.altitudeweb.services.limits.RateLimit)""")
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
@@ -1,4 +1,4 @@
package com.alttd.altitudeweb.controllers.limits;
package com.alttd.altitudeweb.services.limits;
import java.time.Duration;
import java.time.Instant;
@@ -0,0 +1,27 @@
package com.alttd.altitudeweb.services.user;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String uuid) throws UsernameNotFoundException {
try {
//Validate uuid
UUID.fromString(uuid);
return new User(uuid, "", Collections.emptyList());
} catch (IllegalArgumentException e) {
throw new UsernameNotFoundException("Invalid UUID format: " + uuid);
}
}
}
@@ -5,4 +5,5 @@ database.host=${DB_HOST:localhost}
database.user=${DB_USER:root}
database.password=${DB_PASSWORD:root}
cors.allowed-origins=${CORS:https://alttd.com}
login.secret=${LOGIN_SECRET:SET_TOKEN}
logging.level.com.alttd.altitudeweb=INFO
@@ -80,6 +80,10 @@ public interface UUIDHistoryMapper {
};
}
default List<HistoryRecord> getAllHistoryForUUID(@NotNull UUID uuid) {
return getRecentAllHistory(uuid.toString(), "uuid", 100, 0);
}
private List<HistoryRecord> getRecent(@NotNull String tableName, @NotNull UserType userType,
@NotNull UUID uuid, int page) {
int offset = page * PAGE_SIZE;
@@ -16,6 +16,7 @@ public interface TeamMemberMapper {
FROM luckperms_user_permissions AS permissions
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
WHERE permission = #{groupPermission}
AND world = 'global'
""")
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
}
@@ -0,0 +1,16 @@
package com.alttd.altitudeweb.database.web_db;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.Instant;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class KeyPairEntity {
private int id;
private String privateKey;
private String publicKey;
private Instant createdAt;
}
@@ -0,0 +1,15 @@
package com.alttd.altitudeweb.database.web_db;
import org.apache.ibatis.annotations.*;
public interface KeyPairMapper {
@Select("SELECT * FROM key_pair ORDER BY id DESC LIMIT 1")
KeyPairEntity getKeyPair();
@Insert("""
INSERT INTO key_pair (id, private_key, public_key, created_at)
VALUES (#{id}, #{privateKey}, #{publicKey}, #{createdAt})
""")
void save(KeyPairEntity keyPair);
}
@@ -0,0 +1,16 @@
package com.alttd.altitudeweb.database.web_db;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PrivilegedUser {
private int id;
private String uuid;
private List<String> permissions;
}
@@ -0,0 +1,102 @@
package com.alttd.altitudeweb.database.web_db;
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface PrivilegedUserMapper {
/**
* Retrieves a user by their UUID along with their permissions
* @param uuid The UUID of the user to retrieve
* @return The PrivilegedUser with their permissions, or null if not found
*/
@Select("""
SELECT privileged_users.id, privileged_users.uuid, privileges.privileges as permission
FROM privileged_users
LEFT JOIN privileges ON privileged_users.id = privileges.user_id
WHERE privileged_users.uuid = #{uuid}
""")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "permissions", column = "id", javaType = List.class,
many = @Many(select = "getPermissionsForUser"))
})
PrivilegedUser getUserByUuid(@Param("uuid") String uuid);
/**
* Retrieves all privileged users with their permissions
* @return List of all privileged users with their permissions
*/
@Select("""
SELECT id, uuid
FROM privileged_users
""")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "uuid", column = "uuid"),
@Result(property = "permissions", column = "id", javaType = List.class,
many = @Many(select = "getPermissionsForUser"))
})
List<PrivilegedUser> getAllUsers();
/**
* Gets all permissions for a specific user
* @param userId The ID of the user
* @return List of permission strings
*/
@Select("""
SELECT privileges
FROM privileges
WHERE user_id = #{userId}
""")
List<String> getPermissionsForUser(@Param("userId") int userId);
/**
* Adds a new privileged user
* @param user The PrivilegedUser object to add
* @return The number of rows affected
*/
@Insert("""
INSERT INTO privileged_users (uuid)
VALUES (#{user.uuid})
""")
@Options(useGeneratedKeys = true, keyProperty = "user.id", keyColumn = "id")
int addUser(@Param("user") PrivilegedUser user);
/**
* Deletes a privileged user by their UUID
* @param uuid The UUID of the user to delete
* @return The number of rows affected
*/
@Delete("""
DELETE FROM privileged_users
WHERE uuid = #{uuid}
""")
int deleteUserByUuid(@Param("uuid") String uuid);
/**
* Adds a permission to a user
* @param userId The ID of the user
* @param permission The permission to add
* @return The number of rows affected
*/
@Insert("""
INSERT INTO privileges (user_id, privileges)
VALUES (#{userId}, #{permission})
""")
int addPermissionToUser(@Param("userId") int userId, @Param("permission") String permission);
/**
* Removes a permission from a user
* @param userId The ID of the user
* @param permission The permission to remove
* @return The number of rows affected
*/
@Delete("""
DELETE FROM privileges
WHERE user_id = #{userId} AND privileges = #{permission}
""")
int removePermissionFromUser(@Param("userId") int userId, @Param("permission") String permission);
}
@@ -87,6 +87,7 @@ public class Connection {
log.debug("Loaded default database settings {}", databaseSettings);
Connection connection = new Connection(databaseSettings, addMappers);
log.debug("Created default database connection {}", connection);
connections.put(Databases.DEFAULT, connection);
return CompletableFuture.completedFuture(connection);
}
@@ -1,9 +1,11 @@
package com.alttd.altitudeweb.setup;
import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession;
import org.jetbrains.annotations.NotNull;
import java.sql.SQLException;
import java.sql.Statement;
@@ -12,15 +14,21 @@ import java.sql.Statement;
public class InitializeWebDb {
protected static void init() {
log.info("Initializing LiteBans");
log.info("Initializing WebDb");
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
configuration.addMapper(SettingsMapper.class);
configuration.addMapper(KeyPairMapper.class);
}).join()
.runQuery(InitializeWebDb::createSettingsTable);
log.debug("Initialized LuckPerms");
.runQuery(SqlSession -> {
createSettingsTable(SqlSession);
createKeyTable(SqlSession);
createPrivilegedUsersTable(SqlSession);
createPrivilegesTable(SqlSession);
});
log.debug("Initialized WebDb");
}
private static void createSettingsTable(SqlSession sqlSession) {
private static void createSettingsTable(@NotNull SqlSession sqlSession) {
String query = """
CREATE TABLE IF NOT EXISTS db_connection_settings
(
@@ -40,4 +48,53 @@ public class InitializeWebDb {
}
}
private static void createKeyTable(@NotNull SqlSession sqlSession) {
String query = """
CREATE TABLE IF NOT EXISTS key_pair (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
private_key TEXT NOT NULL,
public_key TEXT NOT NULL,
created_at TIMESTAMP NOT NULL
);
""";
try (Statement statement = sqlSession.getConnection().createStatement()) {
statement.execute(query);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private static void createPrivilegedUsersTable(@NotNull SqlSession sqlSession) {
String query = """
CREATE TABLE IF NOT EXISTS privileged_users (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
uuid VARCHAR(36) NOT NULL
);
""";
try (Statement statement = sqlSession.getConnection().createStatement()) {
statement.execute(query);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private static void createPrivilegesTable(@NotNull SqlSession sqlSession) {
String query = """
CREATE TABLE IF NOT EXISTS privileges (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id int NOT NULL,
privileges VARCHAR(36) NOT NULL,
CONSTRAINT fk_privileges_user FOREIGN KEY (user_id)
REFERENCES privileged_users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
""";
try (Statement statement = sqlSession.getConnection().createStatement()) {
statement.execute(query);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
+4
View File
@@ -13,15 +13,19 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^19.2.18",
"@angular/common": "^19.2.0",
"@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0",
"@angular/material": "^19.2.18",
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0",
"@types/three": "^0.177.0",
"ngx-cookie-service": "^19.1.2",
"rxjs": "~7.8.0",
"three": "^0.177.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
+3 -1
View File
@@ -2,13 +2,15 @@ import {Component} from '@angular/core';
import {ScrollService} from '../scroll/scroll.service';
import {CommonModule} from '@angular/common';
import {HeaderComponent} from '../header/header.component';
import {RemoveTrailingPeriodPipe} from "../util/RemoveTrailingPeriodPipe";
@Component({
selector: 'app-about',
standalone: true,
imports: [
CommonModule,
HeaderComponent
HeaderComponent,
RemoveTrailingPeriodPipe
],
templateUrl: './about.component.html',
styleUrl: './about.component.scss'
+49 -3
View File
@@ -5,6 +5,10 @@ export const routes: Routes = [
path: '',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
},
{
path: 'particles',
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
},
{
path: 'map',
loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
@@ -64,7 +68,49 @@ export const routes: Routes = [
{
path: 'warps',
loadComponent: () => import('./warps/warps.component').then(m => m.WarpsComponent)
}
},
{
path: 'skyblock',
loadComponent: () => import('./skyblock/skyblock.component').then(m => m.SkyblockComponent)
},
{
path: 'customfeatures',
loadComponent: () => import('./customfeatures/customfeatures.component').then(m => m.CustomfeaturesComponent)
},
{
path: 'guide',
loadComponent: () => import('./guide/guide.component').then(m => m.GuideComponent)
},
{
path: 'ranks',
loadComponent: () => import('./ranks/ranks.component').then(m => m.RanksComponent)
},
{
path: 'commandlist',
loadComponent: () => import('./commandlist/commandlist.component').then(m => m.CommandlistComponent)
},
{
path: 'mapart',
loadComponent: () => import('./mapart/mapart.component').then(m => m.MapartComponent)
},
{
path: 'lag',
loadComponent: () => import('./lag/lag.component').then(m => m.LagComponent)
},
{
path: 'staffpowers',
loadComponent: () => import('./staffpowers/staffpowers.component').then(m => m.StaffpowersComponent)
},
{
path: 'forms/:form',
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
},
{
path: 'forms',
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
},
{
path: 'particles',
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
},
];
@@ -6,57 +6,104 @@
</div>
</app-header>
<ng-container *ngIf="punishment === undefined">
<p>Loading...</p>
</ng-container>
<main>
<section class="darkmodeSection">
<section class="columnSection">
<div class="detailsBackButton">
<ng-container *ngIf="punishment === undefined">
<p>Loading...</p>
</ng-container>
<ng-container *ngIf="punishment">
<table [cellSpacing]="0">
<div>
<p>type: {{ this.historyFormat.getType(punishment) }}</p>
<p>is active: {{ this.historyFormat.isActive(punishment) }}</p>
<tbody>
<tr>
<td>Player</td>
<td>
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid)" width="25" height="25"
alt="{{punishment.username}}'s Minecraft skin">
<span class="username">{{ punishment.username }}</span>
</div>
</td>
</tr>
<tr>
<td>Moderator</td>
<td>
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid)" width="25"
height="25"
alt="{{punishment.punishedBy}}'s Minecraft skin">
<span class="username">{{ punishment.punishedBy }}</span>
</div>
</td>
</tr>
<tr>
<td>Reason</td>
<td>{{ punishment.reason | removeTrailingPeriod }}</td>
</tr>
<tr>
<td>Date</td>
<td>{{ this.historyFormat.getPunishmentTime(punishment) }}</td>
</tr>
<tr>
<td>Expires</td>
<td>{{ this.historyFormat.getExpiredTime(punishment) }}</td>
</tr>
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
<tr>
<td>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</td>
<td>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</td>
</tr>
<a [routerLink]="['/bans']">< Back</a>
</div>
</section>
<section class="columnSection center">
<ng-container *ngIf="punishment">
<div>
<span class="tag tagInfo"
[ngClass]="{
'tagPermanent': this.historyFormat.isPermanent(punishment),
'tagExpired': !this.historyFormat.isPermanent(punishment)
}">
{{ this.historyFormat.getType(punishment) }}
</span>
</div>
<div>
<span
class="tag tagInfo"
[ngClass]="{
'tagActive': this.historyFormat.isActive(punishment),
'tagInactive': !this.historyFormat.isActive(punishment)
}">
{{ this.historyFormat.isActive(punishment) ? 'Active' : 'Inactive' }}
</span>
</div>
</ng-container>
</tbody>
</div>
</table>
</ng-container>
</section>
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<ng-container *ngIf="punishment">
<div class="playerContainer">
<h2>Player</h2>
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid, '150')"
width="150"
height="150"
alt="{{punishment.username}}'s Minecraft skin"
>
<h3 class="detailsUsername">{{ punishment.username }}</h3>
</div>
</ng-container>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<ng-container *ngIf="punishment">
<div class="playerContainer">
<h2>Moderator</h2>
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid, '150')"
width="150"
height="150"
alt="{{punishment.punishedBy}}'s Minecraft skin"
>
<h3 class="detailsUsername">{{ punishment.punishedBy }}</h3>
</div>
</ng-container>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<ng-container *ngIf="punishment">
<div class="detailsInfo">
<h2>Reason</h2>
<p>{{ punishment.reason | removeTrailingPeriod }}</p>
</div>
</ng-container>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<ng-container *ngIf="punishment">
<div class="detailsInfo">
<h2>Date</h2>
<p>{{ this.historyFormat.getPunishmentTime(punishment) }}</p>
</div>
</ng-container>
</div>
</div>
</section>
</section>
</main>
</ng-container>
<section class="columnSection">
<ng-container *ngIf="punishment">
<span>Expires</span>
<span>{{ this.historyFormat.getExpiredTime(punishment) }}</span>
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
<span>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</span>
<span>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</span>
</ng-container>
</ng-container>
</section>
@@ -0,0 +1,68 @@
.detailsBackButton {
font-family: open-sans, sans-serif;
}
.columnSection {
padding-top: 30px;
}
h2 {
text-align: center;
}
h3 {
text-align: center;
}
.avatar {
padding: 10px 0;
}
.detailsUsername {
font-size: 1.2em;
font-family: 'opensans-bold', sans-serif;
}
.detailsInfo {
padding-top: 50px;
}
.detailsInfo p {
text-align: center;
}
.tag {
display: inline-block;
padding: 5px 10px;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
font-family: 'opensans-bold', sans-serif;
}
.tagInfo {
margin: 0 20px;
font-size: 1.1em;
}
.tagActive {
color: #FFFFFF;
background-color: #EE5555;
}
.tagInactive {
color: #FFFFFF;
background-color: #F79720;
}
.tagPermanent {
color: #FFFFFF;
background-color: #EE5555;
}
.tagExpired {
color: #FFFFFF;
background-color: #777777
}
@@ -1,9 +1,9 @@
import {Component, OnInit} from '@angular/core';
import {HistoryService, PunishmentHistory} from '../../../api';
import {NgIf, NgOptimizedImage} from '@angular/common';
import {NgClass, NgIf, NgOptimizedImage} from '@angular/common';
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
import {HistoryFormatService} from '../history-format.service';
import {ActivatedRoute} from '@angular/router';
import {ActivatedRoute, RouterLink} from '@angular/router';
import {catchError, map} from 'rxjs';
import {HeaderComponent} from '../../header/header.component';
@@ -13,7 +13,9 @@ import {HeaderComponent} from '../../header/header.component';
NgIf,
NgOptimizedImage,
RemoveTrailingPeriodPipe,
HeaderComponent
HeaderComponent,
RouterLink,
NgClass
],
templateUrl: './details.component.html',
styleUrl: './details.component.scss'
@@ -22,9 +22,19 @@ export class HistoryFormatService {
}
public isActive(entry: PunishmentHistory): boolean {
if (entry.removedBy !== null) {
return false;
}
if (entry.expiryTime <= 0) {
return true;
}
return entry.expiryTime > Date.now();
}
public isPermanent(entry: PunishmentHistory): boolean {
return entry.expiryTime <= 0;
}
public getType(entry: PunishmentHistory): string {
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
}
@@ -32,7 +42,7 @@ export class HistoryFormatService {
public getExpiredTime(entry: PunishmentHistory): string {
let suffix: string = '';
if (entry.removedBy !== null && entry.removedBy !== undefined) {
suffix = '(' + entry.removedBy + ')';
suffix = '(Unbanned by ' + entry.removedBy + ')';
}
if (entry.expiryTime <= 0) {
return "Permanent " + this.getType(entry) + " " + suffix;
@@ -48,11 +58,11 @@ export class HistoryFormatService {
}) + " " + suffix;
}
public getAvatarUrl(entry: string): string {
public getAvatarUrl(entry: string, size: string = '25'): string {
let uuid = entry.replace('-', '');
if (uuid === 'C') {
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
}
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
return `https://crafatar.com/avatars/${uuid}?size=${size}&overlay`;
}
}
@@ -22,14 +22,14 @@
<td class="historyType" (click)="showDetailedPunishment(entry)">
{{ this.historyFormat.getType(entry) }}
</td>
<td class="historyPlayer" (click)="setSearch(entry.type, entry.username, 'player')">
<td class="historyPlayer" (click)="setSearch(entry.username, 'player')">
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.uuid)" width="25" height="25"
alt="{{entry.username}}'s Minecraft skin">
<span class="username">{{ entry.username }}</span>
</div>
</td>
<td class="historyPlayer" (click)="setSearch(entry.type, entry.punishedBy, 'staff')">
<td class="historyPlayer" (click)="setSearch(entry.punishedBy, 'staff')">
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
alt="{{entry.punishedBy}}'s Minecraft skin">
@@ -93,25 +93,10 @@ export class HistoryComponent implements OnInit, OnChanges {
).subscribe();
}
setSearch(type: PunishmentHistory.TypeEnum, name: string, userType: 'player' | 'staff') {
let punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
switch (type) {
case 'ban':
punishmentType = 'ban';
break;
case 'mute':
punishmentType = 'mute';
break;
case 'kick':
punishmentType = 'kick';
break;
case 'warn':
punishmentType = 'warn';
break;
}
setSearch(name: string, userType: 'player' | 'staff') {
let searchParams: SearchParams = {
userType: userType,
punishmentType: punishmentType,
punishmentType: 'all',
searchTerm: name
}
this.selectItem.emit(searchParams);
@@ -0,0 +1,496 @@
<ng-container>
<app-header [current_page]="'commandlist'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Command list</h1>
<h2>Looking for a specific command, or the description of one? This is where you find it.</h2>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<p style="font-family: 'opensans-bold', sans-serif; padding-bottom: 5px !important;">Claiming</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/claim -</span> Gives you the claim kit</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/buyclaimblocks &lt;amount&gt; -</span> Allows
you to buy claimblocks (0.5 in game per block)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/sellclaimblocks &lt;amount&gt; -</span>
Allows you to sell claimblocks (0.5 in game per block)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/abandonallclaims -</span> Deletes all your
claims
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/abandonclaim -</span> Deletes the
claim/subdivision you're currently in
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/delclaim -</span> Deletes the
claim/subdivision you're currently in
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/unclaim -</span> Deletes the
claim/subdivision you're currently in
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/claimlist -</span> Shows you all your claims
and claimblocks
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/accesstrust &lt;username/public&gt; -</span>
Gives accesstrust to a user/everyone
</li>
<li><span
style="font-family: 'opensans-bold', sans-serif;">/containertrust &lt;username/public&gt; -</span> Gives
containertrust to a user/everyone
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/expandclaim &lt;amount&gt; -</span> Expands
claim by the specified amount in the direction you're facing
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/restrictsubclaim -</span> Seperates
permissions for the subclaim you are standing in from the main claim
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/subdivide -</span> Turns on subdivide mode
(hold golden shovel) to allow you to create subdivisions
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/trustlist -</span> Shows users trusted in the
claim/subclaim you're standing it
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/claimnearbytrust -</span> Allows other
players to claim within 100 blocks to your claim
</li>
</ul>
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Creative</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot help -</span> Display all creative
commands
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot info -</span> Display plot info</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot confirm -</span> Confirm an action</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot claim -</span> Claim the current plot
you're standing on
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot auto -</span> Claim a random available
plot nearby
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot delete -</span> Delete your plot</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot trust -</span> Allow a player to build
in your plot
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot add -</span> Allow a user to build while
you are online
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot setowner -</span> Set the plot owner
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot setbiome -</span> Set the plot biome
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot visit -</span> Visit someone (or your
own) plot
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot kick -</span> Kick a player from your
plot
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot middle -</span> Teleports you to the
center of the current plot
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot untrust -</span> Remove a player from
your plot
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot merge [auto] -</span> Merge a plot (or
multiple)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot unmerge -</span> Unmerge plots</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot sethome -</span> Set the plot home</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot clear -</span> Clear a plot</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/nightvision -</span> Enables nightvision</li>
</ul>
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Dynamic Map</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/dynmap -</span> Sends you a link to the
Altitude map page
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/dynmap hide -</span> Hides you on the map
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/dynmap show -</span> Shows you on the map
</li>
</ul>
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">McMMO</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/mctop &lt;ability&gt; -</span> Shows the top
10 players for Power Level or the specified ability
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/mcrank -</span> Shows where you are on the
mctop per skill
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/mcstats -</span> Shows your mcmmo levels in a
scoreboard
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/mcability -</span> Toggles ability use on or
off
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/acrobatics [?] [page] -</span> Shows your
stats for Acrobatics. If you add ? it will show the guide for Acrobatics
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/archery [?] [page] -</span> Shows your stats
for Archery. If you add ? it will show the guide for Archery
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/axes [?] [page] -</span> Shows your stats for
Axes. If you add ? it will show the guide for Axes
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/excavation [?] [page] -</span> Shows your
stats for Excavation. If you add ? it will show the guide for Excavation
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/fishing [?] [page] -</span> Shows your stats
for Fishing. If you add ? it will show the guide for Fishing
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/herbalism [?] [page] -</span> Shows your
stats for Herbalism. If you add ? it will show the guide for Herbalism
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/mining [?] [page] -</span> Shows your stats
for Mining. If you add ? it will show the guide for Mining
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/repair [?] [page] -</span> Shows your stats
for Repair. If you add ? it will show the guide for Repair
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/swords [?] [page] -</span> Shows your stats
for Swords. If you add ? it will show the guide for Swords
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/taming [?] [page] -</span> Shows your stats
for Taming. If you add ? it will show the guide for Taming
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/unarmed [?] [page] -</span> Shows your stats
for Unarmed. If you add ? it will show the guide for Unarmed
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/woodcutting [?] [page] -</span> Shows your
stats for Woodcutting. If you add ? it will show the guide for Woodcutting
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/inspect &lt;username&gt; -</span> Shows mcmmo
levels for specified user in a scoreboard if they are near you
</li>
</ul>
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">MyPet</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinfo -</span> Shows information about your
MyPet
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petname &lt;name&gt; -</span> Gives your
current MyPet the specified name
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petrelease &lt;name&gt; -</span> Release the
specified MyPet (this will despawn it)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petcall (/petc) -</span> Teleports your
current MyPet to you
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/psa -</span> Sends your MyPet away, it can
still be called using /petc (does not store your pet)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petswitch (/psw) -</span> Switches between
current and stored MyPet
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petstore (/pst) -</span> Stores your MyPet
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/pettrade &lt;username&gt; [price] -</span>
Trades current MyPet with a player
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petshop -</span> Opens the MyPet shop</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petskill -</span> Shows information about the
skills of your MyPet
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petstop -</span> Stops your MyPet from
attacking a target if it's not in farm or aggressive behavior modes
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petchooseskilltree (/pcst) -</span> Allows
you to select a skilltree for your MyPet
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petcapturehelper (/pch) -</span>
Enables/disables the CaptureHelper for MyPet
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petoptions healthbar -</span> Toggles
actionbar healthbar on/off
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petoptions idle-volume &lt;%&gt; -</span>
Sets volume percentage for the MyPet
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinventory (/peti) -</span> Opens the
inventory for your current MyPet if it has one
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petpickup (/pp) -</span> Toggles pet pickup
on/off if your pet has an inventory
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbehavior [mode] (/pb) -</span> Switches
between pet behaviors or to the specified one
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbeacon -</span> Opens the beacon editor
for your MyPet if it has one
</li>
</ul>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<p style="font-family: 'opensans-bold', sans-serif; padding-bottom: 5px !important;">Economy</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/bal -</span> Shows you your balance</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/cheque &lt;amount&gt; -</span> Creates a
cheque with the specified amount of money if you're holding a piece of paper
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/pay &lt;username&gt; &lt;amount&gt; [-confirmed] -</span>
Pays the specified amount to the specified user
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/shop -</span> Opens the shop GUI</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/points -</span> Displays your current amount
of sell/buy points
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/sell &lt;item&gt; -</span> Tells you if the
entered item is sellable at spawn, and at what price
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/buy &lt;item&gt; -</span> Tells you if the
entered item is buyable at spawn, and at what price
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/playershop checkstock &lt;radius&gt; [max. stock] -</span>
highlights all of your shops which do not meet the minimum amount specified
</li>
</ul>
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Party Chat</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/party help -</span> Display the help menu
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/party create &lt;partyname&gt; &lt;password&gt; -</span>
Creates a party
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/party info -</span> Displays information
about your current party
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/party invite &lt;username&gt; -</span> Invite
a user to your party
</li>
<li><span
style="font-family: 'opensans-bold', sans-serif;">/party join &lt;partyname&gt; [password] -</span> Join
a users party (password is required only if the party has one)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/p &lt;message&gt; -</span> Changes chat to
party chat. If a message is specified it is sent to party chat
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/party leave -</span> Leave a party</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/party name &lt;newname&gt; -</span> Leave a
party
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/party owner &lt;username&gt; -</span> Makes
the specified user owner of the party
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/party password &lt;password&gt; -</span> Sets
or changes a password for the party
</li>
<li><span
style="font-family: 'opensans-bold', sans-serif;">/party remove (/party kick) &lt;username&gt; -</span>
Kicks the specified user from the party
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/party disband -</span> Disband your party
</li>
</ul>
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Voting</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/vote -</span> Sends a link to our vote page
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/votebest -</span> Shows highest total votes
you reached per day/week/month
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/votelast -</span> Shows when you last voted
on each of the websites
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/votenext -</span> Shows when you can vote
again per website
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/votestreak -</span> Shows your current and
best vote streak
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/votetop [page] -</span> Shows the vote top
page
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/votetotal -</span> Shows you your total votes
</li>
</ul>
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Warps</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/warps [warp] -</span> Opens the warp GUI or
teleports you to the specified warp
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/warp apply -</span> Starts application
process for a new warp (costs 25k)
</li>
</ul>
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Altitude</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/queue -</span> Shows what server you are
queued for
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/find &lt;username&gt; -</span> Shows if and
where this user is online
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/back -</span> Teleports you back to where you
last teleported from
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/dback -</span> Brings you back to the last
location you died at
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/colors -</span> Shows all color and format
codes
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/home [name] -</span> Shows your homes (click
to teleport), if a home is specified it will teleport you there
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/list -</span> Lists all online players on the
server
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/stafflist -</span> Lists all online staff
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/seen &lt;username&gt; -</span> Shows when a
player is/was online
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/playtime -</span> Shows your playtime</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/sit -</span> Allows you to sit down</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/cooldown -</span> Shows the cooldown for the
rtp portal
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/hat -</span> Puts the current item on your
head
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/discord -</span> Sends you a link to the
Altitude Discord server
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/ranks -</span> Shows the ranks on the
Altitude server
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/contact -</span> Sends a link to our Contact
page
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/report -</span> Sends a link to the report
form
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/rules -</span> Sends a link to the rules page
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/store -</span> Sends a link to the Altitude
store
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/guide -</span> Sends a link to the guide page
for getting started
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/info -</span> Sends some links to our website
and Discord
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/se &lt;signline&gt; &lt;text&gt; -</span>
While looking at a sign changes text on specified line to specified text.
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/editsign -</span> Toggles shift click sign
editing
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/kickfromclaim -</span> Kicks specified user
from claim
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/kickpet -</span> Toggles kickpet mode, kicks
vanilla pets from claims after right clicking it
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/warnings -</span> Shows all active warnings
for yourself
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/realname &lt;username/nickname&gt; -</span>
Shows the specified users username and nickname
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart remove -</span> Deletes map from the
database if you own it and are holding it
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart save -</span> Saves map on the
database if you're holding it
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/sneakclickmending -</span> Allows you to mend
your items by right clicking while sneaking
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/pu rotateblock -</span> Rotate already
placed, direction-specific, blocks
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/pu xpcalc &lt;current xp&gt; &lt;goal xp&gt; -</span>
Calculates the amount of XP needed to get from level X to level Y
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/pu xpcheque &lt;xp&gt; -</span> Creates a
'cheque' in the form of an XP bottle with the specified amount of XP
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/pt top [daily/weekly/monthly/total]-</span>
Displays the players with the highest amount of playtime for the specified time
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/finditem &lt;item&gt; -</span> Enables you to
search your chest monster for a specific item
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/local -</span> Toggles local chat, where
messages will only be sent to players within a 200 block distance
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/blockitempickup -</span> Prevents picking up
items
</li>
</ul>
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Miscellaneous</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/apart -</span> Opens particles GUI</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/cmi armorstand [last/near] -</span> Opens
Armorstand editor
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/ping -</span> Shows your ping</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/time -</span> Shows in-game time</li>
<li><span
style="font-family: 'opensans-bold', sans-serif;">/mail send &lt;username&gt; &lt;message&gt; -</span>
Sends a mail to the specified user
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/ignore &lt;username&gt; -</span> Ignores a
users messages to you and hides their public chat messages from you
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/r &lt;message&gt; -</span> Replies to the
last user to send you a message
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/c &lt;message&gt; -</span> Replies to the
last user you sent a message to
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/msg &lt;username&gt; &lt;message&gt; -</span>
Sends a message to the specified user
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/pvp [on/off] -</span> Toggles your pvp on/off
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/sethome &lt;homename&gt; -</span> Sets a home
with the specified name
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/delhome &lt;homename&gt; -</span> Shows your
homes (click to remove), or removes the specified home
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/spawn -</span> Teleports you to spawn</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpa &lt;username&gt; -</span> Sends a
teleport request to the specified user
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpaaccept [username] -</span> Accepts last
teleport request, or the teleport request of the specified user
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpahere &lt;username&gt; -</span> Sends a
teleport here request to the specified user
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpbypass -</span> Used to teleport to a
location the plugin marks as unsafe (such as void or lava)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpdeny [username] -</span> Denies last
teleport request, or the teleport request of the specified user
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/givepet &lt;username&gt; -</span> Gives
vanilla pet to specified user after you right click it
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/trapped -</span> Teleports you out of the
claim you are in if you do not have build permission there
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/colorsextra -</span> Displays useable RGB
colors (shift+lmb to paste in chat)
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/dispose (/trash) -</span> Opens a GUI where
you can dispose of items
</li>
</ul>
</div>
</div>
</section>
</section>
</main>
</ng-container>
@@ -0,0 +1,14 @@
main ul {
font-family: 'opensans', sans-serif;
text-align: left;
}
main li {
margin-left: 30px;
padding-bottom: 10px;
}
li {
color: var(--font-color);
transition: 0.5s ease;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CommandlistComponent } from './commandlist.component';
describe('CommandlistComponent', () => {
let component: CommandlistComponent;
let fixture: ComponentFixture<CommandlistComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CommandlistComponent]
})
.compileComponents();
fixture = TestBed.createComponent(CommandlistComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,14 @@
import {Component} from '@angular/core';
import {HeaderComponent} from "../header/header.component";
@Component({
selector: 'app-commandlist',
imports: [
HeaderComponent
],
templateUrl: './commandlist.component.html',
styleUrl: './commandlist.component.scss'
})
export class CommandlistComponent {
}
@@ -0,0 +1,179 @@
<ng-container>
<app-header [current_page]="'skyblock'" height="460px" background_image="/public/img/backgrounds/path.jpg"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Custom Features</h1>
<h2>A list of custom features created specifically to enhance the survival experience on Altitude.</h2>
<h3
style="font-family: 'minecraft-text', sans-serif; font-size: 0.8rem; margin-top: 10px; color: #FFFFFF !important;">
Commands for the features are listed <a [routerLink]="['/commandlist']">here</a></h3>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Blocks</h2>
<p style="padding-bottom: 0 !important;">Beacons can be activated by redstone</p>
<ul class="full-page-list">
<li>Powering the block above a beacon disables the beam</li>
</ul>
<p>Glow lichen can be silk touched</p>
<p>Vines can be silk touched and bonemealed</p>
<p>Coral and coral fans can be bonemealed to grow a new coral tree</p>
<p>Mushroom blocks can be stripped with an axe</p>
<p>Candles can be placed without a block beneath them</p>
<p>Crying obsidian can be used to create nether portals</p>
<p style="padding-bottom: 0 !important;">Gravel and sand are craftable in stonecutters</p>
<ul class="full-page-list">
<li>Cobblestone -> Gravel</li>
<li>Gravel -> Sand</li>
</ul>
<p>Sponges can remove lava - wet sponges remove more (Note: the sponge will be consumed)</p>
<p>Item frames can be made invisible using a diamond</p>
<p>Rotation of item frames can be locked using a slime ball</p>
<p>Copper can be oxidized using a cauldron of water</p>
<p>Pink petals can be placed on any surface</p>
<p>Wither roses can be bonemealed</p>
</div>
</div>
</section>
</section>
<section class="darkmodeSectionThree">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Containers</h2>
<p>Dispensers can interact with campfires</p>
<p>Dispensers can interact with cauldrons</p>
<p>Dispensers can place pumpkins</p>
<p>Dispensers can carve pumpkins</p>
<p>Barrels have 54 inventory slots</p>
<p>Furnaces have a built in item filter, this moves any item they can't process to the output</p>
<p>Naming a container or villager "public" makes them publically available</p>
<p>Natual loot chests (in dungeons, etc.) are not protected by claims (but can only be broken by the claim
owner)</p>
</div>
</div>
</section>
</section>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Compostable items</h2>
<ul class="full-page-list">
<li>leather: 0.10</li>
<li>wooden_shovel: 0.10</li>
<li>rotten_flesh: 0.15</li>
<li>salmon: 0.15</li>
<li>cod: 0.15</li>
<li>porkchop: 0.15</li>
<li>beef: 0.15</li>
<li>chicken: 0.15</li>
<li>rabbit: 0.15</li>
<li>mutton: 0.15</li>
<li>bamboo: 0.15</li>
<li>wooden_sword: 0.20</li>
<li>wooden_hoe: 0.20</li>
<li>wooden_pickaxe: 0.30</li>
<li>wooden_axe: 0.30</li>
<li>poisonous_potato: 0.35</li>
<li>leather_boots: 0.40</li>
<li>flowering_azalea_leaves: 0.50</li>
<li>leather_helmet: 0.50</li>
<li>leather_leggings: 0.70</li>
<li>leather_chestplate: 0.80</li>
</ul>
</div>
</div>
</section>
</section>
<section class="darkmodeSectionThree">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Ender Dragon</h2>
<p>First kill of the month awards a dragon egg</p>
<p style="padding-bottom: 0 !important;">Can drop items when killed</p>
<ul class="full-page-list">
<li>elytra: 30%</li>
<li>dragon_head: 30%</li>
<li>dragon_breath: 40%</li>
<li>and seperately from the rest, a 0.1% chance for a dragon_egg</li>
</ul>
</div>
</div>
</section>
</section>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Mobs</h2>
<p>Bats can spawn above sea level (bat spawning only enabled during halloween)</p>
<p style="padding-bottom: 0 !important;">Mobs can respawn in structures</p>
<ul class="full-page-list">
<li>Shulker -> End City</li>
<li>Piglin Brute, Piglin -> Bastion</li>
<li>Evoker, Vindicator, Allay -> Woodland Mansion</li>
<li>Allay -> Pillager Outpost</li>
<li>Camel -> Desert Village</li>
</ul>
<p>Naming a mob "muted", "silenced", "silent" or "silence me" silences a mob</p>
<p>Naming a mob "muted", "silenced", "silent", "silence me", "protected" or "protect me" prevents it from
being damaged by non-trusted players in a claim</p>
<p>Naming a mob "baby" prevents it from growing up</p>
<p>Wither health bar is removed when silenced</p>
<p>Turtles can hatch during daytime</p>
<p>Unclaim vanilla player pets (dogs, cats, parrots, horses) if the owner has been offline for 60+ days by
right clicking with a lead</p>
<p>Fisherman villagers can sell Bucket of Salmon</p>
</div>
</div>
</section>
</section>
<section class="darkmodeSectionThree">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Restrictions & disabled vanilla features</h2>
<p>Restricted iron golem spawning (10 golems spawning in a radius of 112 blocks in a time span of 1
minute)</p>
<p>Disabled stacking raids (having multiple raids going on at once)</p>
<p>Disabled TNT duping</p>
<p>Disabled bat spawning</p>
<p>Disabled patrol spawning</p>
<p>Zombies don't attack villagers</p>
<p>Disabled processing of most things around AFK players</p>
<p>Disabled AI for villagers in a 1x1 area</p>
<p>Shulkers shooting each other does not spawn a new shulker since they respawn naturally in End Cities</p>
<p>Pigmen cannot spawn from portals</p>
<p>Allay duplication is disabled since they respawn natually in Mansions and Outposts</p>
<p>Players cannot claim land within 100 blocks of another claim without the owner's permission</p>
</div>
</div>
</section>
</section>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Miscellaneous</h2>
<p>Grindstones can strip shulker box & beehive NBT data</p>
<p>Sneak click mending</p>
<p>/iwanttobreakthisblock, required to break some natural generated blocks</p>
<p>/sneakclickmending, allows you to mend your items by right clicking while sneaking</p>
<p>Swift Sneak enchantment can be found in villager trades</p>
<p>Nether portals can be as small as 1x2 (1x2 portal, 3x4 base)</p>
<p>Lootchests refill once for every player</p>
<p>Empty maps can be duplicated using paper in a cartography table</p>
<p>Eating glowberries gives you a glowing effect</p>
</div>
</div>
</section>
</section>
</main>
</ng-container>
@@ -0,0 +1,9 @@
main ul {
font-family: opensans, sans-serif;
text-align: left;
}
main li {
margin-left: 30px;
padding-bottom: 10px;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CustomfeaturesComponent } from './customfeatures.component';
describe('CustomfeaturesComponent', () => {
let component: CustomfeaturesComponent;
let fixture: ComponentFixture<CustomfeaturesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CustomfeaturesComponent]
})
.compileComponents();
fixture = TestBed.createComponent(CustomfeaturesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,16 @@
import {Component} from '@angular/core';
import {HeaderComponent} from "../header/header.component";
import {RouterLink} from '@angular/router';
@Component({
selector: 'app-customfeatures',
imports: [
HeaderComponent,
RouterLink
],
templateUrl: './customfeatures.component.html',
styleUrl: './customfeatures.component.scss'
})
export class CustomfeaturesComponent {
}
@@ -0,0 +1,5 @@
<app-forms [currentPage]="'appeal'" [formTitle]="'Minecraft Appeal'">
<div form-content>
</div>
</app-forms>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppealComponent } from './appeal.component';
describe('AppealComponent', () => {
let component: AppealComponent;
let fixture: ComponentFixture<AppealComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppealComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AppealComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,69 @@
import {Component, OnInit} from '@angular/core';
import {FormsComponent} from '../forms.component';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {AppealsService, MinecraftAppeal} from '../../../api';
@Component({
selector: 'app-appeal',
imports: [
FormsComponent
],
templateUrl: './appeal.component.html',
styleUrl: './appeal.component.scss'
})
export class AppealComponent implements OnInit {
public form: FormGroup<Appeal>;
constructor(private appealApi: AppealsService) {
this.form = new FormGroup({
username: new FormControl('', {nonNullable: true, validators: [Validators.required]}),
punishmentId: new FormControl('', {nonNullable: true, validators: [Validators.required]}),
email: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.email]}),
appeal: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.minLength(10)]})
});
}
ngOnInit() {
}
public onSubmit() {
if (this.form === undefined) {
console.error('Form is undefined');
return
}
if (this.form.valid) {
this.sendForm()
} else {
// Mark all fields as touched to trigger validation display
Object.keys(this.form.controls).forEach(field => {
const control = this.form!.get(field);
if (!(control instanceof FormGroup)) {
console.error('Control [' + control + '] is not a FormGroup');
return;
}
control.markAsTouched({onlySelf: true});
});
}
}
private sendForm() {
const rawValue = this.form.getRawValue();
const appeal: MinecraftAppeal = {
appeal: rawValue.appeal,
email: rawValue.email,
punishmentId: parseInt(rawValue.punishmentId),
username: rawValue.username,
uuid: ''//TODO
}
this.appealApi.submitMinecraftAppeal(appeal).subscribe()
}
}
interface Appeal {
username: FormControl<string>;
punishmentId: FormControl<string>;
email: FormControl<string>;
appeal: FormControl<string>;
}
+5
View File
@@ -0,0 +1,5 @@
export enum FormType {
APPEAL = 'appeal',
STAFF_APPLICATION = 'staff_application',
CONTACT = 'contact'
}
@@ -0,0 +1,18 @@
<ng-container>
<app-header [current_page]="currentPage" height="200px" background_image="/public/img/backgrounds/staff.png"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>{{ formTitle }}</h1>
</div>
</app-header>
<ng-container *ngIf="!type">
<ng-container *ngFor="let formType of FormType | keyvalue">
<button mat-raised-button (click)="setFormType(formType.value)">
{{ formType }}
</button>
</ng-container>
</ng-container>
<div>
<ng-content select="[form-content]"></ng-content>
</div>
</ng-container>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsComponent } from './forms.component';
describe('FormsComponent', () => {
let component: FormsComponent;
let fixture: ComponentFixture<FormsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(FormsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+68
View File
@@ -0,0 +1,68 @@
import {Component, Input, OnInit} from '@angular/core';
import {HeaderComponent} from '../header/header.component';
import {MatDialog} from '@angular/material/dialog';
import {ActivatedRoute} from '@angular/router';
import {LoginDialogComponent} from '../login/login.component';
import {KeyValuePipe, NgForOf, NgIf} from '@angular/common';
import {FormType} from './form_type';
import {MatButton} from '@angular/material/button';
import {AuthService} from '../services/auth.service';
@Component({
selector: 'app-forms',
imports: [
HeaderComponent,
NgIf,
NgForOf,
MatButton,
KeyValuePipe
],
templateUrl: './forms.component.html',
styleUrl: './forms.component.scss'
})
export class FormsComponent implements OnInit {
@Input() formTitle: string = 'Form';
@Input() currentPage: string = 'forms';
public type: FormType | undefined;
constructor(private authService: AuthService,
private dialog: MatDialog,
private route: ActivatedRoute,
) {
this.route.paramMap.subscribe(async params => {
const code = params.get('code');
if (code) {
this.authService.login(code).subscribe();
} else if (!this.authService.checkAuthStatus()) {
const dialogRef = this.dialog.open(LoginDialogComponent, {
width: '400px',
disableClose: true
});
dialogRef.afterClosed().subscribe();
}
});
}
ngOnInit() {
this.route.paramMap.subscribe(params => {
switch (params.get('form')) {
case FormType.APPEAL:
this.type = FormType.APPEAL;
this.currentPage = 'appeal';
break;
default:
throw new Error("Invalid type");
}
});
}
protected readonly FormType = FormType;
protected readonly Object = Object;
public setFormType(formType: FormType) {
this.type = formType;
}
}
+110
View File
@@ -0,0 +1,110 @@
<ng-container>
<app-header [current_page]="'guide'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Guide Book</h1>
<h2>We aim to be an inclusive community server where players of all ages can find something to enjoy.</h2>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Backstory</h2>
<p>Altitude is a community-centered survival Minecraft server for {{ ALTITUDE_VERSION }}. Altitude opened
in January of 2015 and was originally a private server for a small group of friends. Weve since opened
our doors to welcome anyone and everyone to enjoy the custom survival experience weve crafted!</p>
<p>The community grew rapidly in the beginning of 1.13 and therefore grew beyond the capacity of a single
server, so we opened two survival servers that each offered the same features and plugins, but a different
seed so that there is lots of land to explore!. The two servers later became four servers.</p>
<p>The Minecraft hype slowly died down and therefore we had to downscale first to two servers for 1.18 and
then for 1.20 down to one server again.</p>
<p>If you would like to learn more about the history of Altitude, you can visit our <a
[routerLink]="['/about']">about page</a>.</p>
</div>
<div class="columnParagraph">
<h2>Claiming Land</h2>
<p>You should protect your home by “claiming” it to prevent others stealing your items or damaging your
build. This is important because we allow players to loot unclaimed buildings, claim them as their own,
and even completely remove them because we otherwise would have a world full of abandoned buildings. This
means that if your home is damaged in any way because it wasnt protected, staff will not be able to
restore your items!</p>
<p>Claiming land on Altitude is easy. We use the very popular plugin “GriefPrevention”, which allows players
to use a golden shovel to make their claim.To get started enter the command <span
style="font-family: 'opensans-bold', sans-serif;">/claim</span> which will give you the necessary tools.
After that you simply click two opposite corners with the golden shovel (can be any golden shovel, not
only the one given through /claim) to claim the area between the points.</p>
<p>If you need more help with claiming, including trusting others and expanding your claim, visit our <a
[routerLink]="['/claiming']">claiming page</a>.</p>
</div>
<div class="columnParagraph">
<h2>Economy</h2>
<p>We offer a collection of economy features to provide a player-focused and player-operated economy.
Players can create their own shops with the ChestShop plugin to buy and sell goods with others, apply for
a warp so that other players can travel directly to their shop, and pay other players directly with the
<b>/pay</b> command.</p>
<p>There are also shops in the spawn of each world that sell basic items like raw ores, food, and building
blocks. The prices in the spawn shops are designed to be much higher than you would likely find at player
shops to encourage trading between players and only using spawn as a last resort.</p>
<p>If you would like to learn more about the economy and how to create your own chest shops, see our <a
[routerLink]="['/economy']">economy page</a>.</p>
<p>If you would like to learn about applying for your own warp, including the criteria you must meet, see
our <a [routerLink]="['/warps']">warps page</a>.</p>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<h2>Survival Features</h2>
<p>On Altitude, we want to provide an authentic survival experience for every player. In a multiplayer
environment, this can be challenging! To provide the best experience we possibly can, weve kept some
vanilla features on that other servers disable:</p>
<ul>
<li>TNT and creepers do damage! (Claimed land is safe)</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/back</span> to return to your last position
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/dback</span> to return to your death
position. (This even works if you have used <b>/back</b> earlier)
</li>
<li>Loot chests by default cannot be broken. You can enable this by doing <b>/iwanttobreakthisblock &lt;block&gt;</b>
</li>
</ul>
</div>
<div class="columnParagraph">
<h2>McMMO &amp; MyPet</h2>
<p>Our server feature the very popular community-requested plugins: McMMO & MyPet. McMMO introduces an
RPG-like experience to the survival game with skill leveling, special abilities, rare loot, and new
mechanics. The primary feature of McMMO is the detailed leveling system that breaks down Minecraft into
fifteen skills including mining, woodcutting, and herbalism. Each skill will level up the more you do it
unlocking powerful abilities related to that skill.</p>
<p>If youd like to learn more about McMMO, <a href="https://mcmmo.org/wiki/Main_Page">visit their wiki
here</a>!</p>
<p>MyPet allows you to claim nearly any mob in-game as your “pet”. Each pet can be assigned a skilltree that
gives it unique abilities. Pets can learn to fight alongside you against mobs, pick up items for you as
you mine, and even provide faster travel around the world. Pets will level up in their skilltree the more
you play with them to unlock more abilities.</p>
<p>If you would like to learn more about MyPet and their abiltiies, visit our <a [routerLink]="['/mypet']">MyPet
page</a></p>
</div>
<div class="columnParagraph">
<h2>Creative</h2>
<p>Occasionally, you may find yourself with a creative itch, or need a way to experiment and collaborate
with other players on the server! We offer a creative server for just that purpose. Players can use as
many plots in creative as they need (just ask a staff for more if you reach the default limit) and each
plot is 100 by 100 blocks. Players frequently use these plots for testing our survival builds before
taking the time to collect the resources in survival to build it.</p>
<p>Due to the low traffic that the creative server gets, we do not moderate this server as closely. If you
see anything wrong with the server, please inform a staff member so that we can get it resolved!</p>
</div>
<div class="columnParagraph">
<h2>More Help</h2>
<p>You are now on your way to becoming a member of the Altitude community! We hope weve provided you with
the tools necessary to be successful on our servers. If you have any other questions, please reach out to
a staff member in-game, or on <a href="https://discordapp.com/invite/TGqpzCJ">our Discord</a>!</p>
</div>
</div>
</section>
</section>
</main>
</ng-container>
@@ -0,0 +1,9 @@
main ul {
font-family: opensans, sans-serif;
text-align: left;
}
main li {
margin-left: 30px;
padding-bottom: 10px;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GuideComponent } from './guide.component';
describe('GuideComponent', () => {
let component: GuideComponent;
let fixture: ComponentFixture<GuideComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GuideComponent]
})
.compileComponents();
fixture = TestBed.createComponent(GuideComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+18
View File
@@ -0,0 +1,18 @@
import {Component} from '@angular/core';
import {HeaderComponent} from "../header/header.component";
import {RouterLink} from '@angular/router';
import {ALTITUDE_VERSION} from '../constant';
@Component({
selector: 'app-guide',
imports: [
HeaderComponent,
RouterLink
],
templateUrl: './guide.component.html',
styleUrl: './guide.component.scss'
})
export class GuideComponent {
protected readonly ALTITUDE_VERSION = ALTITUDE_VERSION;
}
@@ -89,7 +89,7 @@
</li>
<li class="nav_li">
<span
[id]="getCurrentPageId(['guide', 'faq', 'ranks', 'rules', 'commandlist', 'art', 'lag', 'staffpowers',
[id]="getCurrentPageId(['guide', 'faq', 'ranks', 'rules', 'commandlist', 'mapart', 'lag', 'staffpowers',
'nicknames', 'bans', 'discord-bans'])" class="nav_link fake_link" [ngClass]="active">Reference</span>
<ul class="dropdown">
<li class="nav_li"><a class="nav_link2" [routerLink]="['/guide']">Guide Book</a></li>
+190
View File
@@ -0,0 +1,190 @@
<ng-container>
<app-header [current_page]="'lag'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Reducing Server Lag</h1>
<h2>How can we minimize lag on the server? Together we can create a lag-free survival server, but it takes
everyone's help!</h2>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<div class="container">
<div class="paragraph">
<h2>Introduction</h2>
<p>Lag is not caused by one person, it is caused by all of us. Some of us cause more than others, but we can
all do small things to reduce the amount of lag we cause. Remember: do not ever accuse anyone else of being
the problem. If you believe someones build or behavior is a large contributor to the lag, please privately
tell a staff member so we can look into it. If you have any specific questions, staff are always here to
help. This is especially true if you want to make a redstone contraption, but are unsure how to have it
automatically shut off when not needed or to make it less taxing on the server.</p>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h2>Mobs</h2>
<p style="margin-bottom: 0;">Mobs are the #1 cause of lag on a survival Minecraft server. They can be
surprisingly hard on the server and are a cause of lag that everyone contributes to. Because of this, never
keep more than you need of any given mob.</p>
<p>FarmLimiter is a plugin that introduces hard limits to mobs. The plugin groups mobs by chaining mobs
together that are within a minimum radius of each other. Please refer to the example image for a better idea
of how mob chaining works. We've provided the settings of this plugin below for your reference, but please
do not try to circumvent this system - it is in place for a reason!</p>
<ul class="full-page-list" style="margin-bottom: 0;">
<li>Villagers should not be allowed to build up from a breeder, and you should cull undesirables as needed
to keep your numbers as low as possible. Also think carefully about which villagers you really need and if
you can make dual use of some. Only use “infinite” villager breeder designs if you limit their breeding by
limiting their food.
</li>
<li>Villagers used for trading should be put on emerald blocks, emerald ore or smooth stone. This will
disable their AI for anything other than trading. This helps server performance a lot and any villagers
used for trading found without their AI disabled could be removed by staff if they are causing lag.
Villagers placed on these blocks will refresh their trades faster than normal. The following example
visualizes the 20 passive mobs within 5 blocks FarmLimiter restriction. <a style="cursor: pointer;"
(click)="toggleExceptions()">Show
example...</a></li>
</ul>
<div id="exceptions" [ngClass]="{'hide': !showExceptions}"
style="display: flex; justify-content: center; padding-bottom: 30px;">
<img ngSrc="/public/img/random/farm_limiter_darker.png" alt="Visualization of the farm limiter"
style="width: 70%; padding-top: 7px;" height="375" width="715">
</div>
<ul class="full-page-list">
<li>Turtles only need one breeding pair. The breeding mechanic of turtles allows you to breed one pair over
and over, as they become fertile immediately upon laying their eggs. Consider keeping only two adult
turtles in your farm.
</li>
<li>Consider not raising pigs at all, as their meat is exactly the same as beef and they have no secondary
drops
</li>
<li>Do not fill your base with more vanilla pets than you need</li>
<li>Never circumvent entity cramming. It is there because a lot of entities crammed into a small space is
hard on the server.
</li>
<li>FarmLimiter is a plugin that introduces hard limits to mobs. We've provided the settings of this plugin
below for your reference, but please do not try to circumvent this system - it is in place for a reason!
</li>
</ul>
<div class="highlightedContent">
<h3>Farm Limiter Plugin</h3>
<p>No more than 25 mobs (of all types) within a radius of 3 blocks (this gets the chickens).</p>
<p>No more than 20 passive mobs within a radius of 5 blocks (limits farms - including villagers and
excluding chickens).</p>
<p>No more than 10 hostile mobs within a single block (limits grinders).</p>
<p>No more than 10 turtles within a radius of 5 blocks (turtles cause a LOT of lag).</p>
<p>No more than 70 villagers within a radius of 50 blocks.</p>
</div>
</div>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<div class="paragraph">
<h2>Minecarts</h2>
<p>Minecarts are another type of entity that cause a lot of lag. They are harder on the server than you might
think.</p>
<ul class="full-page-list">
<li>Do not store villagers or other mobs long-term in minecarts. Use them for transportation and drop them
off in an enclosed location on the other end.
</li>
<li>Do not use farm designs that rely on many hopper minecarts. They are convenient because they pick up
items through blocks and have a faster throughput than hoppers, but they are unnecessarily hard on the
server.
</li>
<li>Avoid using minecarts for entity cramming purposes when possible and try to design your farm to only
have one such killing chamber if it is necessary.
</li>
</ul>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h2>Hoppers</h2>
<p>Hoppers cause lag because they are constantly checking for items, even when nothing is in them, so you
should minimize your use of them.</p>
<ul class="full-page-list">
<li>When bringing items down through a vertical line of hoppers, try alternating them with single chests.
This saves you iron and saves the server a bit of lag.
</li>
<li>Bulk storage systems where random items are dropped in and sent into chests should be avoided because
they are likely to cause a situation that is especially taxing. More specifically: A hopper pushing items
into a full chest that are not in the chest OR A hopper trying to pull items from a chest to complete
stacks but none of the items in the chest match.
</li>
<li>Myth: Droppers/Furnaces on top of hoppers reduces lag. Droppers on top of hoppers do not reduce lag, and
furnaces are a slightly laggy object in their own right, so do not use them unnecessarily.
</li>
<li>When moving items long distances use water streams. Also try to use water to gather newly farmed items
rather than an array of hoppers. However, be aware that water streams have their own downfalls: First, the
item entity is at risk to be picked up by a pet with /petpickup turned on. So, do not use them near public
areas of your base or for high value items. Second, do not make water streams so long that they flow into
unloaded chunks, as the items can back up where chunks unload and create a ticking lag bomb of item
entities. Finally, set things up so that, if the storage system at the other end fills up, the extra items
get automatically destroyed rather than sitting on full hoppers to despawn.
</li>
</ul>
</div>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<div class="paragraph">
<h2>Redstone Farms</h2>
<p>Redstone contraptions are, by their nature, going to contribute some lag. But you can do things to reduce
their impact.</p>
<ul class="full-page-list">
<li>Biggers is not better. Try to make farms a reasonable size for what you need and no larger. Think about
how much of a resource you actually need.
</li>
<li>Think about the design you use. If you have a farm with a lot of pistons to break crops automatically,
can you set it up so that the pistons fire in sequence instead of all at once? Can you use a slimeblock
push bar so that you only need one piston and one observer for every 12 plants instead of one per each?
Minimize how much redstone is running at once.
</li>
<li>Set up your machines to shut off when not in use.</li>
<li>If you use a minecart hopper/chest to transport items, make sure it automatically docks when there are
no items to move. You can set up the minecart pickup to only run after a harvest and an auto-smelter that
uses minecarts to fill the furnaces does not need to have the minecart running all the time, only when
its dropping off items.
</li>
<li>Redstone clocks rarely need to run all the time. For example, an item disposal dropper can be set up to
automatically trigger the clock when there are items in the dropper and to shut off the clock when the
dropper is empty.
</li>
</ul>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h2>Other Entities</h2>
<p>You may have noticed a theme in this list. Everything we've listed is an <span
style="font-family: 'opensans-bold', sans-serif;">entity</span>. Entities are the biggest cause of lag on
Altitude, and anything we can do to reduce the number of entities on the server will help!</p>
<ul class="full-page-list">
<li>Block Entities. Any block entity causes more lag than a normal block, but some cause more than others.
<a href="https://minecraft.gamepedia.com/Block_entity">minecraft.gamepedia.com/Block_entity</a></li>
<li>Signs with text on them are laggier than empty signs, so consider if you need to write that sign. Also,
be sure to not write anything on signs used for technical reasons, like to hold back lava or water.
</li>
<li>Item frames also are a small contributor to lag, but can add up, so please do not use a lot of them for
no reason.
</li>
<li>Brewing Stands cause more lag than you might think, so please do not have more placed than you need.
</li>
<li>Item Entities. Please pick up item entities when mining, even if you dont actually want them. You can
dispose of them by throwing them in lava or on a cactus. But leaving mass amounts of items to despawn puts
an unnecessary strain on the server.
</li>
</ul>
</div>
</div>
</section>
</main>
</ng-container>
+55
View File
@@ -0,0 +1,55 @@
.full-page-list {
margin-left: 70px;
font-family: 'opensans', sans-serif;
margin-bottom: 30px;
color: var(--font-color);
transition: 0.5s ease;
}
.full-page-list li {
padding-bottom: 10px;
}
.highlightedContent {
width: 70%;
margin: auto;
background: var(--color-quinary);
padding: 20px;
margin-bottom: 30px;
text-align: center;
border-radius: 5px;
transition: 0.5s ease;
}
.highlightedContent p {
margin-bottom: 0px !important;
}
.paragraph {
width: 100%;
margin-bottom: -20px;
}
.paragraph p {
margin-bottom: 15px;
}
li {
padding-bottom: 10px;
}
.hide {
display: none !important;
}
@media (max-width: 1000px) {
main .container {
text-align: left;
}
}
@media (max-width: 670px) {
main .container {
text-align: center;
}
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LagComponent } from './lag.component';
describe('LagComponent', () => {
let component: LagComponent;
let fixture: ComponentFixture<LagComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LagComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LagComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+25
View File
@@ -0,0 +1,25 @@
import {Component} from '@angular/core';
import {HeaderComponent} from "../header/header.component";
import {NgClass, NgOptimizedImage} from '@angular/common';
import {ScrollService} from '../scroll/scroll.service';
@Component({
selector: 'app-lag',
imports: [
HeaderComponent,
NgOptimizedImage,
NgClass
],
templateUrl: './lag.component.html',
styleUrl: './lag.component.scss'
})
export class LagComponent {
showExceptions: boolean = false;
constructor(public scrollService: ScrollService) {
}
public toggleExceptions() {
this.showExceptions = !this.showExceptions;
}
}
@@ -0,0 +1,18 @@
<h2 mat-dialog-title>Login</h2>
<div mat-dialog-content>
<form [formGroup]="loginForm">
<mat-form-field appearance="fill" style="width: 100%">
<mat-label>Enter your code</mat-label>
<input matInput formControlName="code" type="text">
<mat-error *ngIf="formHasError()">
Code is required
</mat-error>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-flat-button color="primary" (click)="onSubmit()" [disabled]="!loginForm.valid">
Submit
</button>
</div>
@@ -0,0 +1,13 @@
:host {
display: block;
width: 100%;
max-width: 400px;
}
.mat-dialog-content {
padding-top: 10px;
}
.mat-dialog-actions {
padding: 16px 0;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LoginComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+71
View File
@@ -0,0 +1,71 @@
import {Component} from '@angular/core';
import {MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle} from '@angular/material/dialog';
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';
import {NgIf} from '@angular/common';
import {MatSnackBar} from '@angular/material/snack-bar';
import {AuthService} from '../services/auth.service';
@Component({
selector: 'app-login',
standalone: true,
imports: [
ReactiveFormsModule,
MatButtonModule,
MatInputModule,
MatFormFieldModule,
MatDialogTitle,
MatDialogContent,
MatDialogActions,
NgIf
],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
})
export class LoginDialogComponent {
public loginForm: FormGroup;
constructor(
public dialogRef: MatDialogRef<LoginDialogComponent>,
private fb: FormBuilder,
private authService: AuthService,
private snackBar: MatSnackBar
) {
this.loginForm = this.fb.group({
code: ['', [
Validators.required,
Validators.minLength(8),
Validators.maxLength(8),
Validators.pattern('^[ABCDEFGHJKLMNPQRSTUVWXYZ23456789]+$')
]]
});
}
onCancel(): void {
this.dialogRef.close();
}
onSubmit(): void {
if (!this.loginForm.valid) {
this.snackBar.open('Invalid code', '', {duration: 2000});
return;
}
this.snackBar.open('Logging in...', '', {duration: 2000});
this.authService.login(this.loginForm.value.code).subscribe({
next: (jwt) => {
this.dialogRef.close(jwt);
},
error: () => {
this.loginForm.get('code')?.setErrors({
invalid: true
});
}
});
}
public formHasError() {
return this.loginForm.get('code')?.hasError('required');
}
}
@@ -0,0 +1,75 @@
<ng-container>
<app-header [current_page]="'mapart'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Mapart</h1>
<h2>Altitude features a way to save maparts.</h2>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>What is mapart?</h2>
<p>Any map is a piece of art, but the common meaning of this is an actual drawing onto a map. By placing
blocks on a plot to color in a map one pixel at a time, you can create an image.</p>
<p>Creating these maps is time consuming, and Altitude wants to support these mapartists by offering an
easier way to share their mapart..</p>
</div>
<div class="columnParagraph">
<h2>How to save mapart</h2>
<p>After you have placed all blocks on your plot and have the image recorded on your map, hold the map in
your main hand and do <span style="font-family: 'opensans-bold', sans-serif;">/mapart save</span>. This
will cost you $1000 in-game currency. The saving process can take several minutes.</p>
</div>
<div class="columnParagraph">
<h2>Useful resources for creating mapart</h2>
<p>Mapart may be created simply by zooming in on an image you wish to create and placing pixels by eye.
First, however, you must know what blocks to use for each color. This <a
href="https://minecraft.gamepedia.com/Map_item_format">Minecraft wiki article</a> explains map item
formatting such as colors available on maps and the blocks that correspond.</p>
<p>Pick whichever blocks are the most convenient to collect and to break later if you plan on reusing blocks
for multiple maps.</p>
<p>Additionally, there is an incredibly useful online tool known as Mapartcraft by Rebane that can take an
image and automatically convert it to a schematic that can be used in conjunction with
litematica/schematica as a guide to create much more detailed mapart! Be sure to select the correct
version of Minecraft in the settings and pick your preferred blocks. This tool can be found <a
href="https://rebane2001.com/mapartcraft/">here</a>.</p>
<p>Reminder: While schematica and litematica are permitted mods on Altitude, any features like auto-build
and easy-place they may include are not!</p>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<h2>Is my mapart protected?</h2>
<p>The only person that can remove or clone saved maps is the one who created them. Mapart can be copied by
using the vanilla method on a cartography table.</p>
</div>
<div class="columnParagraph">
<h2>What mapart can I make</h2>
<p>If you plan on making some kind of map art that some may consider inappropriate, explicit, or
controversial, you can submit a request for review through our AltitudeBot support bot on Discord. Simply
message the bot anything, and the bot will reply with commands that you can utilize. For mapart review
requests, you will be using the command <span style="font-family: 'opensans-bold', sans-serif;">!mapart &lt;description of map art&gt;</span>.
Please be sure to also attach an image of the mapart (it is not possible to submit the request without an
image)!</p>
<p>Creating inappropriate mapart without prior approval may result in deletion of the mapart itself and
punishment for the creator.</p>
</div>
<div class="columnParagraph">
<h2>Useful Commands</h2>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart save -</span> Save a mapart</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart remove -</span> Delete a mapart</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart find -</span> See who made a specific
mapart
</li>
</ul>
</div>
</div>
</section>
</section>
</main>
</ng-container>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MapartComponent } from './mapart.component';
describe('MapartComponent', () => {
let component: MapartComponent;
let fixture: ComponentFixture<MapartComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MapartComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MapartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,14 @@
import {Component} from '@angular/core';
import {HeaderComponent} from "../header/header.component";
@Component({
selector: 'app-mapart',
imports: [
HeaderComponent
],
templateUrl: './mapart.component.html',
styleUrl: './mapart.component.scss'
})
export class MapartComponent {
}
@@ -0,0 +1,189 @@
<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 row column">
<div #rendererContainer class="renderer-container column row"></div>
<div class="plane-controls">
<label>Plane Position (Z-axis):</label>
<mat-slider min="0" max="16" step="1" #planeSlider>
<input matSliderThumb [(ngModel)]="planePosition" (input)="updatePlanePosition($event)">
</mat-slider>
<span>{{ planePosition }}/16 blocks</span>
</div>
</div>
<div class="row">
<div class="column controls-section">
<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">Stationary</mat-checkbox>
</div>
</mat-card-content>
</mat-card>
</div>
<div class="column">
<mat-card class="color-picker-card">
<mat-card-header>
<mat-card-title>Particle Color</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="color-picker">
<input type="color" [(ngModel)]="selectedColor">
<span>Selected Color: {{ selectedColor }}</span>
</div>
</mat-card-content>
</mat-card>
</div>
<div class="column frames-section">
<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>Particle {{ i + 1 }}: ({{ particle.x.toFixed(2) }}, {{ particle.y.toFixed(2) }}
, {{ particle.z.toFixed(2) }})</span>
<button mat-icon-button color="warn" (click)="removeParticle(frameId, i)">
<mat-icon>delete</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>
<div class="column json-output">
<mat-card>
<mat-card-header>
<mat-card-title>JSON Output</mat-card-title>
</mat-card-header>
<mat-card-content>
<pre>{{ generateJson() }}</pre>
</mat-card-content>
</mat-card>
</div>
</div>
</section>
</section>
</main>
@@ -0,0 +1,128 @@
.renderer-section {
flex: 1;
min-width: 300px;
display: flex;
}
.renderer-container {
width: 100%;
height: 400px;
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
}
.plane-controls {
margin-top: 10px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
display: flex;
align-items: center;
gap: 10px;
}
.plane-controls mat-slider {
flex: 1;
}
.controls-section {
flex: 1;
min-width: 300px;
gap: 20px;
}
.form-row {
margin-bottom: 15px;
}
mat-form-field {
width: 100%;
}
.color-picker-card {
margin-top: 20px;
}
.color-picker {
display: flex;
align-items: center;
gap: 15px;
}
.color-picker input[type="color"] {
width: 50px;
height: 50px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.frames-section {
margin-bottom: 20px;
}
.frames-container {
margin-top: 10px;
}
.frame-content {
padding: 15px;
}
.particles-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #eee;
border-radius: 4px;
padding: 10px;
margin-bottom: 15px;
}
.particle-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-bottom: 1px solid #eee;
}
.particle-item:last-child {
border-bottom: none;
}
.no-particles {
padding: 20px;
text-align: center;
color: #888;
}
.frame-actions {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.add-frame {
margin-top: 15px;
display: flex;
justify-content: center;
}
.json-output {
margin-top: 20px;
}
.json-output pre {
background-color: #f5f5f5;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
max-height: 300px;
font-family: monospace;
white-space: pre-wrap;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ParticlesComponent } from './particles.component';
describe('ParticlesComponent', () => {
let component: ParticlesComponent;
let fixture: ComponentFixture<ParticlesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ParticlesComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ParticlesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,494 @@
import {AfterViewInit, Component, ElementRef, OnInit, 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';
import * as THREE from 'three';
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
// Define particle types
export enum ParticleType {
REDSTONE = 'REDSTONE',
// Other particle types can be added later
}
// Interface for particle information
interface ParticleInfo {
particle_type: string;
x: number;
y: number;
z: number;
color: string;
extra: number;
}
// Interface for the complete particle data
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[];
};
}
@Component({
selector: 'app-particles',
standalone: true,
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatButtonModule,
MatInputModule,
MatFormFieldModule,
MatSelectModule,
MatSliderModule,
MatCheckboxModule,
MatTabsModule,
MatCardModule,
MatIconModule,
HeaderComponent
],
templateUrl: './particles.component.html',
styleUrl: './particles.component.scss'
})
export class ParticlesComponent implements OnInit, AfterViewInit {
@ViewChild('rendererContainer') rendererContainer!: ElementRef;
@ViewChild('planeSlider') planeSlider!: ElementRef;
// Three.js objects
private scene!: THREE.Scene;
private camera!: THREE.PerspectiveCamera;
private renderer!: THREE.WebGLRenderer;
private controls!: OrbitControls;
private playerModel!: THREE.Group;
private intersectionPlane!: THREE.Mesh;
private particles: THREE.Mesh[] = [];
private raycaster = new THREE.Raycaster();
private mouse = new THREE.Vector2();
// Particle data
public particleData: ParticleData = {
particle_name: '',
display_name: '',
particle_type: ParticleType.REDSTONE,
lore: '',
display_item: 'REDSTONE',
permission: '',
package_permission: '',
frame_delay: 1,
repeat: 1,
repeat_delay: 0,
random_offset: 0,
stationary: true,
frames: {
'frame1': []
}
};
// UI state
public currentFrame: string = 'frame1';
public planePosition: number = 8; // Position in 1/16th of a block
public selectedColor: string = '#ff0000';
public particleTypes = Object.values(ParticleType);
public frames: string[] = ['frame1'];
constructor() {
}
ngOnInit(): void {
// Initialize component
}
ngAfterViewInit(): void {
this.initThreeJS();
this.animate();
}
// Initialize Three.js scene
private initThreeJS(): void {
// Create scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xf0f0f0);
// Get container dimensions
const containerWidth = this.rendererContainer.nativeElement.clientWidth;
const containerHeight = 400; // Fixed height as defined in CSS
// Create camera
this.camera = new THREE.PerspectiveCamera(75, containerWidth / containerHeight, 0.1, 1000);
this.camera.position.set(0, 1, 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';
this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);
// Initialize orbit controls
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true; // Add smooth damping effect
this.controls.dampingFactor = 0.05;
this.controls.minDistance = 2; // Minimum zoom distance
this.controls.maxDistance = 10; // Maximum zoom distance
this.controls.target.set(0, 1, 0); // Set target to player's center
this.controls.update();
// Add lights
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);
// Create player model (simple representation)
this.createPlayerModel();
// Create intersection plane
this.createIntersectionPlane();
// Add event listeners
this.renderer.domElement.addEventListener('mousedown', this.onMouseDown.bind(this));
this.renderer.domElement.addEventListener('mouseup', this.onMouseUp.bind(this));
this.renderer.domElement.addEventListener('mousemove', this.onMouseMove.bind(this));
window.addEventListener('resize', this.onWindowResize.bind(this));
}
// Create a simple player model
private createPlayerModel(): void {
this.playerModel = new THREE.Group();
// 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);
this.scene.add(this.playerModel);
}
// Create the intersection plane
private createIntersectionPlane(): void {
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.scene.add(this.intersectionPlane);
}
// Update plane position based on slider
public updatePlanePosition(event: Event): void {
// Access the value from the slider element
const slider = event.target as HTMLInputElement;
this.planePosition = Number(slider.value);
// Convert from 1/16th block to Three.js units
const position = (this.planePosition / 16) - 0.5; // Center at 0
// Apply position based on the plane's current rotation
// This ensures the plane always moves towards/away from the viewer
const rotation = this.intersectionPlane.rotation.y;
if (Math.abs(rotation) < 0.1 || Math.abs(rotation - Math.PI) < 0.1) {
// Camera in front (0) or behind (PI)
// For camera behind, we need to invert the direction
const direction = Math.abs(rotation) < 0.1 ? 1 : -1;
this.intersectionPlane.position.z = position * direction;
// Reset x position to avoid cumulative changes
this.intersectionPlane.position.x = 0;
} else {
// Camera on right (PI/2) or left (-PI/2)
// For camera on left, we need to invert the direction
const direction = rotation > 0 ? 1 : -1;
this.intersectionPlane.position.x = position * direction;
// Reset z position to avoid cumulative changes
this.intersectionPlane.position.z = 0;
}
}
// Track if mouse is being dragged
private isDragging = false;
private mouseDownTime = 0;
// Handle mouse down event
private onMouseDown(event: MouseEvent): void {
this.isDragging = false;
this.mouseDownTime = Date.now();
}
// Handle 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;
}
// Handle 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;
}
}
// Handle mouse click on the plane
private handlePlaneClick(event: MouseEvent): void {
// Calculate mouse position in normalized device coordinates
const rect = this.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.camera);
// Calculate objects intersecting the picking ray
const intersects = this.raycaster.intersectObject(this.intersectionPlane);
if (intersects.length > 0) {
const point = intersects[0].point;
this.addParticle(point.x, point.y, point.z);
}
}
// Add a particle at the specified position
private addParticle(x: number, y: number, z: number): void {
// Create a visual representation of the particle
const particleGeometry = new THREE.SphereGeometry(0.03, 16, 16);
const particleMaterial = new THREE.MeshBasicMaterial({color: this.selectedColor});
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
particleMesh.position.set(x, y, z);
this.scene.add(particleMesh);
this.particles.push(particleMesh);
// Add to particle data
const hexColor = this.selectedColor.replace('#', '');
const r = parseInt(hexColor.substring(0, 2), 16) / 255;
const g = parseInt(hexColor.substring(2, 4), 16) / 255;
const b = parseInt(hexColor.substring(4, 6), 16) / 255;
const particleInfo: ParticleInfo = {
particle_type: ParticleType.REDSTONE,
x: x,
y: y,
z: z,
color: `${r},${g},${b}`,
extra: 1
};
if (!this.particleData.frames[this.currentFrame]) {
this.particleData.frames[this.currentFrame] = [];
}
this.particleData.frames[this.currentFrame].push(particleInfo);
}
// Handle window resize
private onWindowResize(): void {
const containerWidth = this.rendererContainer.nativeElement.clientWidth;
const containerHeight = 400; // Fixed height as defined in CSS
this.camera.aspect = containerWidth / containerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(containerWidth, containerHeight);
}
// Animation loop
private animate(): void {
requestAnimationFrame(this.animate.bind(this));
// Update controls
if (this.controls) {
this.controls.update();
}
// Update plane orientation based on camera position
if (this.intersectionPlane && this.camera) {
// Calculate the angle between camera and player (in the XZ plane)
const cameraAngle = Math.atan2(
this.camera.position.x,
this.camera.position.z
);
// Determine which quadrant the camera is in with a 45-degree offset
// Adding Math.PI/4 (45 degrees) to the angle before determining the quadrant
// This shifts the quadrant boundaries by 45 degrees
const quadrant = Math.floor((cameraAngle + Math.PI + Math.PI / 4) / (Math.PI / 2)) % 4;
// Rotate the plane to face the camera
if (quadrant === 0) {
this.intersectionPlane.rotation.y = 0; // Camera in front
this.updateFrameMaterial(0x00AA00);
} else if (quadrant === 1) {
this.intersectionPlane.rotation.y = Math.PI / 2; // Camera on right
this.updateFrameMaterial(0x0000AA);
} else if (quadrant === 2) {
this.intersectionPlane.rotation.y = Math.PI; // Camera behind
this.updateFrameMaterial(0x00AA00);
} else {
this.intersectionPlane.rotation.y = -Math.PI / 2; // Camera on left
this.updateFrameMaterial(0x0000AA);
}
}
this.renderer.render(this.scene, this.camera);
}
public updateFrameMaterial(color: number) {
this.intersectionPlane.material = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.05,
side: THREE.DoubleSide
});
}
// Add a new frame
public addFrame(): void {
const frameId = `frame${this.frames.length + 1}`;
this.frames.push(frameId);
this.particleData.frames[frameId] = [];
this.currentFrame = frameId;
this.clearParticleVisuals();
}
// Switch to a different frame
public switchFrame(frameId: string): void {
this.currentFrame = frameId;
this.clearParticleVisuals();
this.renderFrameParticles(frameId);
}
// Clear particle visuals from the scene
private clearParticleVisuals(): void {
for (const particle of this.particles) {
this.scene.remove(particle);
}
this.particles = [];
}
// Render particles for a specific frame
private 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, 16, 16);
// Parse color
const colorParts = particleInfo.color.split(',');
const color = new THREE.Color(
parseFloat(colorParts[0]),
parseFloat(colorParts[1]),
parseFloat(colorParts[2])
);
const particleMaterial = new THREE.MeshBasicMaterial({color});
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z);
this.scene.add(particleMesh);
this.particles.push(particleMesh);
}
}
// Generate JSON output
public generateJson(): string {
return JSON.stringify(this.particleData, null, 2);
}
// Remove a particle
public 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);
}
}
}
// Remove a frame
public removeFrame(frameId: string): void {
const index = this.frames.indexOf(frameId);
if (index !== -1) {
this.frames.splice(index, 1);
delete this.particleData.frames[frameId];
// Switch to first frame if we removed the current one
if (frameId === this.currentFrame && this.frames.length > 0) {
this.switchFrame(this.frames[0]);
} else if (this.frames.length === 0) {
// If no frames left, add one
this.addFrame();
}
}
}
}
+228
View File
@@ -0,0 +1,228 @@
<ng-container>
<app-header [current_page]="'ranks'" height="460px" background_image="/public/img/backgrounds/118fjord.png"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Server Ranks</h1>
<h2>The standard ranks are unlocked by playing on the server. As you accumulate time you will automatically rank
up. Do /rank to check your playtime.</h2>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<div class="container">
<table>
<thead>
<tr>
<th scope="col" class="tableTitle">Rank</th>
<th scope="col" class="tableTitle">Requirements</th>
<th scope="col" class="tableTitle">Perks</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank1">Drifter</span>]</td>
<td data-label="Requirements">Join the server!</td>
<td data-label="Perks" class="perks">
All basic permissions<br>
Up to 3 homes in survival<br>
Up to 2 MyPet slots<br>
Up to 5 claims in survival
</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank1">Nomad</span>]</td>
<td data-label="Requirements">Play for 2 hours</td>
<td data-label="Perks" class="perks">
Up to 4 homes in survival<br>
Up to 3 MyPet slots<br>
Up to 10 claims in survival
</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank1">Peddler</span>]</td>
<td data-label="Requirements">Play for 1 day</td>
<td data-label="Perks" class="perks">
Up to 5 homes in survival<br>
Up to 4 MyPet slots<br>
Up to 15 claims in survival
</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank1">Settler</span>]</td>
<td data-label="Requirements">Play for 3 days</td>
<td data-label="Perks" class="perks">
Up to 6 homes in survival<br>
Up to 5 MyPet slots<br>
Up to 20 claims in survival
</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank1">Resident</span>]</td>
<td data-label="Requirements">Play for 7 days</td>
<td data-label="Perks" class="perks">
Up to 7 homes in survival<br>
Up to 6 MyPet slots<br>
Up to 25 claims in survival
</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank1">Esquire</span>]</td>
<td data-label="Requirements">Play for 14 days</td>
<td data-label="Perks" class="perks">
Up to 8 homes in survival<br>
Up to 7 MyPet slots<br>
Up to 30 claims in survival
</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank1">Knight</span>]</td>
<td data-label="Requirements">Play for 30 days</td>
<td data-label="Perks" class="perks">
Up to 9 homes in survival<br>
Up to 8 MyPet slots
</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank1">Baron</span>]</td>
<td data-label="Requirements">Play for 60 days</td>
<td data-label="Perks" class="perks">
Up to 10 homes in survival<br>
Up to 9 MyPet slots
</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank1">Viscount</span>]</td>
<td data-label="Requirements">Play for 180 days</td>
<td data-label="Perks" class="perks">
Up to 11 homes in survival<br>
Up to 9 MyPet slots
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="subheaderDonor">
<div class="title" style="margin-top: 0; height: 100%;">
<h1>Premium Ranks</h1>
<h2>Premium ranks are a symbol of those who have purchased a rank to support the server. Purchase a rank today
at: <a href="https://store.alttd.com">donate.alttd.com</a>. Thank you!</h2>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<table style="width:100%">
<thead>
<tr>
<th scope="col" class="tableTitle">Rank</th>
<th scope="col" class="tableTitle">Requirements</th>
<th scope="col" class="tableTitle">Perks</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank2">Count</span>]</td>
<td data-label="Requirements">Purchase for $15 on <a href="https://store.alttd.com">Buycraft</a></td>
<td data-label="Perks" class="perks">
Donor Discord channel access<br>
Up to 12 homes in survival<br>
Up to 10 MyPet slots<br>
More on the <a href="https://store.alttd.com/category/514458">store page</a></td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank2">Viceroy</span>]</td>
<td data-label="Requirements">Purchase for $30 on <a href="https://store.alttd.com">Buycraft</a></td>
<td data-label="Perks" class="perks">
Priority queue for full servers<br>
Up to 13 homes in survival<br>
Up to 18 MyPet slots<br>
More on the <a href="https://store.alttd.com/category/514458">store page</a></td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank2">Duke</span>]</td>
<td data-label="Requirements">Purchase for $50 on <a href="https://store.alttd.com">Buycraft</a></td>
<td data-label="Perks" class="perks">
Nickname in chat<br>
Up to 14 homes in survival<br>
Up to 27 MyPet slots<br>
More on the <a href="https://store.alttd.com/category/514458">store page</a></td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank2">Archduke</span>]</td>
<td data-label="Requirements">Purchase for $100 on <a href="https://store.alttd.com">Buycraft</a></td>
<td data-label="Perks" class="perks">
Custom prefix in chat<br>
Up to 16 homes in survival<br>
Up to 36 MyPet slots<br>
More on the <a href="https://store.alttd.com/category/514458">store page</a></td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank2">α</span>]</td>
<td data-label="Requirements">Purchase for $25 on <a href="https://store.alttd.com">Buycraft</a></td>
<td data-label="Perks" class="perks">
Time-limited rank<br>
Access to alpha testing<br>
This custom prefix<br>
More on the <a href="https://store.alttd.com/category/514458">store page</a></td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank4"></span>]</td>
<td data-label="Requirements">Boosting with <a href="https://discordapp.com/nitro">Discord Nitro</a></td>
<td data-label="Perks" class="perks">
Discord Nitro perks<br>
This custom prefix
</td>
</tr>
</tbody>
</table>
</div>
</section>
<section id="subheaderCommunity">
<div class="title" style="margin-top: 0; height: 100%;">
<h1>Community Ranks</h1>
<h2>Community ranks are for talented people who support Altitude in more than one way.</h2>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<table style="width:100%">
<thead>
<tr>
<th scope="col" class="tableTitle">Rank</th>
<th scope="col" class="tableTitle">Requirements</th>
<th scope="col" class="tableTitle">Perks</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank5">Social Media</span>]</td>
<td data-label="Requirements">See requirements on the <a href="/community.php">Community</a> page</td>
<td data-label="Perks" class="perks">/record<br>/ptime<br>/pweather</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank5">Streamer</span>]</td>
<td data-label="Requirements">See requirements on the <a href="/community.php">Community</a> page</td>
<td data-label="Perks" class="perks">/record<br>/ptime<br>/pweather</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank5">YouTube</span>]</td>
<td data-label="Requirements">See requirements on the <a href="/community.php">Community</a> page</td>
<td data-label="Perks" class="perks">/record<br>/ptime<br>/pweather</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank5">Event Leader</span>]</td>
<td data-label="Requirements">See requirements on the <a href="/community.php">Community</a> page</td>
<td data-label="Perks" class="perks">Build perms on event server</td>
</tr>
<tr>
<td data-label="Rank" class="rankTitle">[<span class="rank5">Event Team</span>]</td>
<td data-label="Requirements">See requirements on the <a href="/community.php">Community</a> page</td>
<td data-label="Perks" class="perks">Build perms on event server</td>
</tr>
</tbody>
</table>
</div>
</section>
</main>
</ng-container>
+151
View File
@@ -0,0 +1,151 @@
#subheaderDonor {
height: 350px;
background: -webkit-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/newtown.jpg");
background: -o-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/newtown.jpg");
background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/newtown.jpg");
background-attachment: fixed;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
text-align: center;
}
#subheaderCommunity {
height: 350px;
background: -webkit-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/caruselimage4.png");
background: -o-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/caruselimage4.png");
background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/caruselimage4.png");
background-attachment: fixed;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
text-align: center;
}
td,
th {
font-family: 'opensans', sans-serif;
}
table {
border-collapse: collapse;
margin: 0;
padding: 0;
width: 100%;
table-layout: fixed;
}
table tr {
padding: .35em;
}
table th,
table td {
padding: .625em;
text-align: center;
}
table th {
font-size: .85em;
}
.perks {
text-align: left;
padding-left: 105px;
}
.tableTitle {
font-family: 'opensans-bold', sans-serif;
font-size: 1.5em;
}
.rankTitle {
font-family: 'opensans-bold', sans-serif;
}
.rank1 {
color: #265ac9;
}
.rank2 {
color: #980399;
}
.rank3 {
color: #0c3999;
}
.rank4 {
color: #c949c9;
}
.rank5 {
color: #ffaa00;
}
@media (max-width: 1200px) {
.perks {
padding-left: 80px;
}
}
@media (max-width: 900px) {
.perks {
padding-left: 50px;
}
}
@media (max-width: 690px) {
main .container {
width: 80% !important;
}
}
@media screen and (max-width: 600px) {
table {
border: 0;
}
table th,
table td {
padding: .400em;
}
table thead {
border: none;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
table tr {
display: block;
margin-bottom: .625em;
}
table td {
display: block;
font-size: .8em;
text-align: right;
}
table td::before {
content: attr(data-label);
float: left;
font-weight: bold;
text-transform: uppercase;
}
table td:last-child {
border-bottom: 0;
}
.perks {
text-align: right;
padding-left: 6px;
}
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RanksComponent } from './ranks.component';
describe('RanksComponent', () => {
let component: RanksComponent;
let fixture: ComponentFixture<RanksComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RanksComponent]
})
.compileComponents();
fixture = TestBed.createComponent(RanksComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
+14
View File
@@ -0,0 +1,14 @@
import {Component} from '@angular/core';
import {HeaderComponent} from "../header/header.component";
@Component({
selector: 'app-ranks',
imports: [
HeaderComponent
],
templateUrl: './ranks.component.html',
styleUrl: './ranks.component.scss'
})
export class RanksComponent {
}
+1 -1
View File
@@ -40,7 +40,7 @@
<li>You can talk about current events and real-world topics. However, if staff recognizes the topic is
causing players to become uncomfortable, they may shut the conversation down.
<ul class="full-page-list-2">
<li>You are welcome to take conversations regarding sensitive topics to dm's and party chat with people
<li>You are welcome to take conversations regarding sensitive topics to DMs and party chat with people
who want to be involved, or off the server.
</li>
</ul>
+115
View File
@@ -0,0 +1,115 @@
import {Injectable} from '@angular/core';
import {LoginService} from '../../api';
import {CookieService} from 'ngx-cookie-service';
import {BehaviorSubject, Observable, throwError} from 'rxjs';
import {catchError, tap} from 'rxjs/operators';
import {MatSnackBar} from '@angular/material/snack-bar';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
private userClaimsSubject = new BehaviorSubject<any>(null);
public userClaims$ = this.userClaimsSubject.asObservable();
constructor(
private loginService: LoginService,
private cookieService: CookieService,
private snackBar: MatSnackBar
) {
// Check if user is already logged in on service initialization
this.checkAuthStatus();
}
/**
* Attempt to login with the provided code
*/
public login(code: string): Observable<any> {
return this.loginService.login(code).pipe(
tap(jwt => {
this.saveJwt(jwt as JsonWebKey);
this.isAuthenticatedSubject.next(true);
}),
catchError(error => {
this.snackBar.open('Login failed', '', {duration: 2000});
return throwError(() => error);
})
);
}
/**
* Log the user out by removing the JWT
*/
public logout(): void {
this.cookieService.delete('jwt', '/');
this.isAuthenticatedSubject.next(false);
this.userClaimsSubject.next(null);
}
/**
* Check if the user is authenticated
*/
public checkAuthStatus(): boolean {
const jwt = this.getJwt();
if (jwt) {
try {
const claims = this.extractJwtClaims(jwt as JsonWebKey);
// Check if token is expired
const currentTime = Math.floor(Date.now() / 1000);
if (claims.exp && claims.exp < currentTime) {
this.logout();
return false;
}
this.userClaimsSubject.next(claims);
this.isAuthenticatedSubject.next(true);
return true;
} catch (e) {
this.logout();
}
}
return false;
}
/**
* Get the JWT from cookies
*/
public getJwt(): string | null {
return this.cookieService.check('jwt') ? this.cookieService.get('jwt') : null;
}
/**
* Save the JWT to cookies
*/
private saveJwt(jwt: JsonWebKey): void {
this.cookieService.set('jwt', jwt.toString(), {
path: '/',
secure: true,
sameSite: 'Strict'
});
const claims = this.extractJwtClaims(jwt);
this.userClaimsSubject.next(claims);
}
/**
* Extract claims from JWT
*/
private extractJwtClaims(jwt: JsonWebKey): any {
const token = jwt.toString();
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
return JSON.parse(window.atob(base64));
}
/**
* Get user authorizations from claims
*/
public getUserAuthorizations(): string[] {
const claims = this.userClaimsSubject.getValue();
return claims?.authorizations || [];
}
}
@@ -0,0 +1,123 @@
<ng-container>
<app-header [current_page]="'skyblock'" height="460px" background_image="/public/img/backgrounds/babywither.png"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Skyblock</h1>
<h2>A skyblock server blending vanilla gameplay with unique enhancements for an elevated, community-driven sky
adventure.</h2>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Discover Comet</h2>
<p>Welcome to Comet, a skyblock server that offers players a unique blend of the features you know and love
with a dash of exiciting enhancements to elevate your sky-high adventure.</p>
<p>The heart of the skyblock challenge is the same as your traditional experience: start on a small island
in the sky with limited resources and aim to expand your domain and conquer the endless void. However,
Comet is not your ordinary skyblock experience. We've carefully introduced a selection of enhancements to
enrich the gameplay without overwhelming the core vanilla experience that players love and cherish.</p>
<p style="padding-bottom: 5px !important;">Some of the key features and enhancements includes:</p>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">Explorable Biomes:</span> All islands are on
the same public seed, you can expand your island and explore to find different biomes. Certain mobs only
spawn in certain biomes.
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Dynamic Economy:</span> Engage with our
vibrant server economy, where you can trade items with other players, visit the spawn shops or even set
up your own shops on your island.
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Custom Achievements:</span> Beyond the
traditional skyblock objectives, Comet introduces a series of custom achievements which will test your
skills and reward you with valuable resources and unique items.
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">Cooperative Play:</span> Team up with friends
or make new allies to build your sky empire together.
</li>
</ul>
<p style="padding-top: 10px;">Comet is the perfect experience for those who love skyblock but crave a bit
more excitement and community interaction. Whether you're a seasoned player or new to the sky life, Comet
provides a rich platform for exploration, creativity and fun.</p>
<p>Join Comet today and discover that in the world of skyblock, the sky is just the beginning.</p>
</div>
<div class="columnParagraph">
<h2>Useful commands</h2>
<ul>
<li><span style="font-family: 'opensans-bold', sans-serif;">/island -</span> Opens a GUI where you can see
information about your island, change settings and do upgrades
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/island go -</span> Start a new island or go
to your current island
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/island invite &lt;username&gt; -</span>
Invite a player to your island
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/island kick &lt;username&gt; -</span> Kick a
player from your island
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/island leave -</span> Leave your current
island
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/island restart -</span> Reset your island
without affecting removing your island members
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/island setowner &lt;username&gt; -</span> Set
a new owner of your current island
</li>
<li><span style="font-family: 'opensans-bold', sans-serif;">/island visit &lt;username&gt; -</span> Visit
the specificed player's island
</li>
</ul>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<h2>Cobble Generator Levels</h2>
<p><span style="font-family: 'opensans-bold', sans-serif;">Note:</span> Did you mess up your generator and
accidentally made an obsidian block? No fear, simply right click the obsidian with your bucket and it will
change lava in your bucket!</p>
<ul>
<p style="padding-bottom: 5px !important;">Level 1, coal</p>
<li>Island level 20</li>
<li>64 cobblestone</li>
<p style="padding-bottom: 5px !important;">Level 2, iron</p>
<li>Island level 35</li>
<li>128 cobblestone</li>
<li>35 coal</li>
<p style="padding-bottom: 5px !important;">Level 3, redstone</p>
<li>Island level 50</li>
<li>192 cobblestone</li>
<li>50 iron</li>
<p style="padding-bottom: 5px !important;">Level 4, lapis</p>
<li>Island level 60</li>
<li>256 cobblestone</li>
<li>60 redstone</li>
<p style="padding-bottom: 5px !important;">Level 5, copper</p>
<li>Island level 70</li>
<li>320 cobblestone</li>
<li>70 lapis</li>
<p style="padding-bottom: 5px !important;">Level 6, gold</p>
<li>Island level 80</li>
<li>384 cobblestone</li>
<li>80 copper</li>
<p style="padding-bottom: 5px !important;">Level 7, emerald</p>
<li>Island level 90</li>
<li>448 cobblestone</li>
<li>90 gold</li>
<p style="padding-bottom: 5px !important;">Level 8, diamond</p>
<li>Island level 100</li>
<li>512 cobblestone</li>
<li>100 emerald</li>
<p style="padding-bottom: 5px !important;">Level 9, ancient debris</p>
<li>Island level 150</li>
<li>576 cobblestone</li>
<li>150 diamond</li>
</ul>
</div>
</div>
</section>
</section>
</main>
</ng-container>
@@ -0,0 +1,9 @@
main ul {
font-family: opensans, sans-serif;
text-align: left;
}
main li {
margin-left: 30px;
padding-bottom: 10px;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SkyblockComponent } from './skyblock.component';
describe('SkyblockComponent', () => {
let component: SkyblockComponent;
let fixture: ComponentFixture<SkyblockComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SkyblockComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SkyblockComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,14 @@
import {Component} from '@angular/core';
import {HeaderComponent} from '../header/header.component';
@Component({
selector: 'app-skyblock',
imports: [
HeaderComponent
],
templateUrl: './skyblock.component.html',
styleUrl: './skyblock.component.scss'
})
export class SkyblockComponent {
}
@@ -0,0 +1,277 @@
<ng-container>
<app-header [current_page]="'staffpowers'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>Staff Powers</h1>
<h2>Our staff members have numerous powers to assist them in moderating effectively.</h2>
</div>
</app-header>
<main>
<section class="darkmodeSection">
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<h2>Staff Powers</h2>
<p>We believe it's important to be transparent with our community about what our staff can see and do. To do
this we've created this web page to explain all the things staff can see/do and why.</p>
<p>We store a lot of information, basically everything you do on Altitude is logged. We only use this data
to moderate, we will never sell it or give it to anyone who isn't an active staff member. Any misuse of
the data we store by staff members results in their immediate removal from the team.</p>
<p>This is not a complete list of all powers the staff team has, these are just a list of the commands that
could be misinterpreted as giving staff members an advantage or are related to your data.</p>
<p>If you have a concern about any of the powers talked about on the rest of this page, or you feel like
something is missing. Please get in contact with head staff through the AltitudeBot on Discord or by
submitting a contact form with a valid email.</p>
</div>
<div class="columnParagraph">
<h2>Staff Rooms</h2>
<p>Staff rooms are places where staff may need to go to handle a variety of staff-related tasks. We dont
want staff to seem unresponsive when working on a task if a player sees them unmoving on the server
somewhere when sorting through logs, coreprotect data, or sending mails to players. Often, secure
physical space is needed by staff to deal with an issue on the server (For example, storing stolen items
in one organized place, where all other mods can safely access them so they can be returned to the player
who owns them as quickly as possible). Staff meetings also often occur in these rooms so that the server
isnt left unattended during the meeting, while the team can still remain engaged in the meeting.</p>
<p>Previously, staff rooms were placed under server spawns for quick access by that servers staff team.
Now, staff rooms have been removed from the survival world and exist in a dimension similar to the nether
and the end.<br>
Concerns have been raised in the past that this space is not accessible to players. Due to the purpose of
the space (solely to handle staff-related tasks/player issues, and host meetings), this space will forever
remain inaccessible to players as that access would eliminate the purpose of the space.</p>
</div>
<div class="columnParagraph">
<h2>Spy (seeing DMs & PartyChat)</h2>
<p>Staff members are able to see all private messages and messages sent in party chats. This is vital for
protecting our players, we would not be able to moderate effectively without also being able to see
private messages.</p>
<p>We will never purposely spy on your private conversation for personal gain, the only time we will
interfere with these messages is if we see a severely concerning conversation happening; harassment,
bullying, suicide/self harm discussion, sexual discussion, etc. We will almost always step in through just
a simple DM to check in on things. We will take relevant private messages into account when collecting
evidence on a player.</p>
<p>Please keep in mind, this does not mean staff are constantly moderating your DMs in the way they do
chat. If you are experiencing an issue in DMs and need help, please reach out to a member of staff.<br>
Altitude staff are <span style="font-family: 'opensans-bold', sans-serif;">not</span> trained mental
health professionals, and we are not qualified to counsel through suicide and self harm, but we value our
community members deeply and will do our best to give you some official resources that you can use.</p>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<h2>Player Data (IPs and logs)</h2>
<p>All staff members have access to your IP through /whois and /iphist. They will never use this information
outside of moderation, and it will never be shared outside of our staff team. Everytime you visit a place
on the internet, your IP is used to share data back to you. If we did not have access to your IP, you
would not be able to play on Altitude. The only times we will use your IP for staff purposes is to compare
your IP to an account we may think is an alt (this is also why we have a history of your previous IPs
youve logged in with).</p>
<p>Almost every single action taken on Altitude is tracked through some sort of log. Weve created a very
detailed logging system to help ensure that we are always well informed when dealing with different staff
scenarios, and these logs help us tremendously. We do not share these logs outside of the staff team and
we will never use them for anything unrelated to staff. We will not list everything we log because its
not necessary and would be too long of a list, but just beware that basically anything you do on Altitude
is logged in some way. This is not only to protect Altitude, but you as well.</p>
</div>
<div class="columnParagraph">
<h2>CoreProtect</h2>
<p>Coreprotect is a log on the server that allows staff to see a variety of interactions on the server- From
block deletion to buttons pressed to commands issued. 90% of the time, staff use coreprotect when dealing
with issues related to blocks/entities, such as griefing and theft. Staff may use coreprotect in
conjunction with our database logs if a situation is bigger than coreprotect can support us with. As a
result, staff may take a bit of time when investigating a player issue due to the searching they must do
with coreprotect, especially if there is a lot of related data to search through!</p>
<p>Many servers use coreprotect as it is the most reliable source of this information, is well organized and
is extremely well documented. Coreprotect data is considered confidential, and staff will <span
style="font-family: 'opensans-bold', sans-serif;">not</span> release or share any coreprotect data with
players. The only exception to this rule is cases of theft under trust - where staff will share the name
of the thief with the claim owner for the purpose of them being removed from their claimlist and not being
re-trusted in the future.</p>
<p>Coreprotect also allows us to fix some serious problems using a tool called <span
style="font-family: 'opensans-bold', sans-serif;">Rollback</span>. Rollback allows staff to reverse the
interactions done with blocks/entities/containers within a selected period of time, and helps us fix
things quickly for you after griefing occurs!</p>
</div>
<div class="columnParagraph">
<h2>Head Staff</h2>
<p>Head staff (everyone above mod) have access to a lot of things depending on their exact job. The most
relevant things they have access to are: creative, worldedit, and invload. These powers can be used to
restore items we can't properly restore with coreprotect and remove/move/restore builds as needed.</p>
</div>
</div>
</section>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/fly</h3>
<p>Fly allows staff members to freely do their job without having to worry about being limited to just the
ground. There are a lot of instances where they need to be able to check out something high off the ground
or they just need to be able to travel more efficiently, and flying allows them to do this. We allow our
staff members to use fly for their own personal builds, sort of as a thank you and to help make up for the
time they lose through all of their moderation. We do not want any staff members to be openly flying around
in front of players, and they will never use it for anything other than building (looting, exploring, etc.
is not allowed).</p>
</div>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/god</h3>
<p>God mode allows staff members to work on something in game without being physically disturbed or harmed.
This is a tool that is only ever used for moderation purposes, to allow staff to focus on their job without
being interrupted by outside elements.</p>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/ignoreclaims</h3>
<p>The ignore claims command is extremely helpful when a staff member is tracking down stolen items, checking
trustlists, helping players with other miscellaneous issues, etc. This command is very powerful and all of
our staff members acknowledge this, it will never be used outside of a staff related incident.</p>
</div>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">CoreProtect</h3>
<p>Coreprotect is used to track and view information on block/item/etc placements and gives us the ability to
restore these actions. This is only used to fix and track damage done by stealing/griefing/etc.</p>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/vanish</h3>
<p>Vanish is used to check on what players are doing. This allows us to ensure all players are following the
rules. We hide ourselves while doing so to avoid disrupting the player experience.</p>
</div>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/patrol</h3>
<p>This command allows mods to teleport to each player one by one without missing anyone. This helps staff to
moderate all players equally and efficiently.</p>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/invsee - /ec</h3>
<p>These commands allow the inventory/enderchest of a player to be viewed and edited. This is only used in
situations where a player breaks a rule by taking items from someone else and refuses to return them.</p>
</div>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/whois - /whowas</h3>
<p>Whois is a command that shows a lot of player information, namely IP, UUID, player balance, number of
homes, etc. Your IP is kept private and will never be used maliciously, and your UUID is public information.
This command will never be used for anything outside of moderating purposes. Whowas is a command that lists
all previous usernames of a specific player, as well as your UUID again. This information is public
information that can easily be accessed through a number of different websites.</p>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/flag</h3>
<p>We use flags as an internal tool to inform other staff members of things a player has done, these are
usually in the form of short notes.</p>
</div>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/alts - /iphist</h3>
<p>These commands can be used to show if a player has other players on the same ip as them. This is necessary
to make it harder to bypass punishments. Your ip history is kept private and will never be shared outside of
staff related incidents.</p>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/spy</h3>
<p>All staff usually have /spy on, this allows them to see all conversations in DM's and party chat. This is
important to ensure people aren't using our server to harass players without staff being able to see and
respond to it.</p>
</div>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/cuff</h3>
<p>This command is used when a player is being uncooperative or not listening to staff after being instructed
to do something. When a player is cuffed, they cannot move, they cannot teleport, etc., but they can still
chat or message players. This is mainly used in cases where a player is attempting to interfere with a staff
situation (Ex: theft). This command will only be used for moderation purposes.</p>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/afklist</h3>
<p>This command lists everyone who has been marked as AFK by our plugin. It also notifies us if a player gets
marked as AFK. This is used to check if a player isn't going AFK near farms which might give them an
advantage over other players.</p>
</div>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/heal - /feed</h3>
<p>These commands are only used during moderation to deal with unexpected situations where a player might need
to be healed. They are not used by any staff member while playing on the server.</p>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/tp - /tphere - /spawn</h3>
<p>These commands are used to get a player away from a situation that requires staff intervention such as
attempted player killing or if they are in a place they aren't allowed to be in anymore. Tp can be used
while in vanish to check on a player or to easily and quickly get to a player who's asking for help.</p>
</div>
</div>
</section>
<section class="darkmodeSection">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px;">/cmi invlist</h3>
<p>This command is used to see all of a player's recent deaths. It allows us to see your death inventory, your
levels, the time of your death, location, cause, etc.</p>
</div>
</div>
</section>
<section class="darkmodeSectionThree">
<div class="container">
<div class="paragraph">
<h3 style="padding-top: 15px; padding-bottom: 0 !important;">Ability to see emails during appeals, contact
forms & staff applications</h3>
<p style="font-size: 0.75em;">(Head-Staff only)</p>
<p>When you send in a ban appeal, a contact form, or a staff application, we will be given your email (if you
send in an anonymous contact form we cannot see your email). We will never use your email for anything
besides replying. Head-staff are the only staff members who are able to see your email.</p>
</div>
</div>
</section>
</main>
</ng-container>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { StaffpowersComponent } from './staffpowers.component';
describe('StaffpowersComponent', () => {
let component: StaffpowersComponent;
let fixture: ComponentFixture<StaffpowersComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [StaffpowersComponent]
})
.compileComponents();
fixture = TestBed.createComponent(StaffpowersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,14 @@
import {Component} from '@angular/core';
import {HeaderComponent} from "../header/header.component";
@Component({
selector: 'app-staffpowers',
imports: [
HeaderComponent
],
templateUrl: './staffpowers.component.html',
styleUrl: './staffpowers.component.scss'
})
export class StaffpowersComponent {
}
+1 -1
View File
@@ -44,7 +44,7 @@
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
height="160" width="160" style="width: 160px;">
<h2>{{ member.name }}</h2>
<p>Admin</p>
<p>Head Mod</p>
</div>
</div>
</section>
+2
View File
@@ -1,3 +1,5 @@
@import '@angular/material/prebuilt-themes/azure-blue.css';
:root {
--white: #FFFFFF;
--black: #282828;
+10
View File
@@ -6,6 +6,10 @@ info:
version: 1.0.0
servers:
- url: https://alttd.com/api/v3
components:
schemas:
PermissionClaim:
$ref: './schemas/permissions/permissions.yml#/components/schemas/PermissionClaim'
tags:
- name: history
description: Retrieves punishment history
@@ -28,6 +32,8 @@ paths:
$ref: './schemas/bans/bans.yml#/getTotalResultsForUserSearch'
/history/single/{type}/{id}:
$ref: './schemas/bans/bans.yml#/getHistoryById'
/history/all/{uuid}:
$ref: './schemas/bans/bans.yml#/getAllHistoryForUUID'
/history/total:
$ref: './schemas/bans/bans.yml#/getTotalPunishments'
/appeal/update-mail:
@@ -36,3 +42,7 @@ paths:
$ref: './schemas/forms/appeal/appeal.yml#/MinecraftAppeal'
/appeal/discord-appeal:
$ref: './schemas/forms/appeal/appeal.yml#/DiscordAppeal'
/login/requestNewUserLogin/{uuid}:
$ref: './schemas/login/login.yml#/RequestNewUserLogin'
/login/userLogin/{code}:
$ref: './schemas/login/login.yml#/UserLogin'
@@ -89,7 +89,7 @@ getHistoryForUuid:
parameters:
- $ref: '#/components/parameters/UserType'
- $ref: '#/components/parameters/HistoryType'
- $ref: '#/components/parameters/Uuid'
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
- $ref: '#/components/parameters/Page'
responses:
'200':
@@ -158,7 +158,7 @@ getTotalResultsForUuidSearch:
parameters:
- $ref: '#/components/parameters/UserType'
- $ref: '#/components/parameters/HistoryType'
- $ref: '#/components/parameters/Uuid'
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
responses:
'200':
description: Successful operation
@@ -195,6 +195,22 @@ getHistoryById:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
getAllHistoryForUUID:
get:
tags:
- history
summary: Gets all history for specified UUID
description: Retrieves all history for specified UUID
operationId: getAllHistoryForUUID
parameters:
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
responses:
'200':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/PunishmentHistoryList'
components:
parameters:
HistoryType:
@@ -212,13 +228,6 @@ components:
schema:
type: string
description: The (partial) username to search for
Uuid:
name: uuid
in: path
required: true
schema:
type: string
description: The uuid of the desired user
UserType:
name: userType
in: path
@@ -0,0 +1,9 @@
components:
parameters:
Uuid:
name: uuid
in: path
required: true
schema:
type: string
description: The uuid of the desired user
@@ -0,0 +1,91 @@
UserLogin:
get:
tags:
- login
summary: Log in to the site
description: Log in to the site through a code from the server
operationId: login
parameters:
- $ref: '#/components/parameters/Code'
responses:
'200':
description: Logged in
content:
application/text:
schema:
type: string
description: A JWT token for this user
'401':
description: Login failed - Invalid credentials
content:
application/text:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
default:
description: Unexpected error
content:
application/text:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
RequestNewUserLogin:
get:
tags:
- login
summary: Request a login
description: Request a code, that can be used to log in
operationId: requestLogin
parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Secret
- $ref: '../generic/parameters.yml#/components/parameters/Uuid'
responses:
'200':
description: Success
content:
application/text:
schema:
type: string
description: code to log in with
'401':
description: Login failed - Invalid secret
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'
components:
parameters:
Code:
name: code
in: path
required: true
schema:
type: string
description: The code to log in with
schemas:
LoginData:
type: object
required:
- loginCode
properties:
loginCode:
type: string
description: The code to log in
AddLogin:
type: object
required:
- uuid
properties:
uuid:
type: string
format: uuid
description: UUID of the user that will get logged in
@@ -0,0 +1,8 @@
components:
schemas:
PermissionClaim:
type: string
enum: [ SCOPE_user, SCOPE_head_mod ]
description: Permission claims used for authorization
x-enum-varnames: [ USER, HEAD_MOD ]
x-enum-descriptions: [ "User permission", "Head moderator permission" ]