Compare commits
37
Commits
43b75b8e74
...
textures
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb57289f8 | ||
|
|
60c1329163 | ||
|
|
0e71c0f581 | ||
|
|
eb67a33331 | ||
|
|
39f20796ce | ||
|
|
e00165c56f | ||
|
|
02c6497700 | ||
|
|
0efd476676 | ||
|
|
237518638c | ||
|
|
fea1a98cea | ||
|
|
ecd9b3d824 | ||
|
|
9808b5d63d | ||
|
|
c13b7077a7 | ||
|
|
023ae809ef | ||
|
|
3a6f137c9a | ||
|
|
cb8447a096 | ||
|
|
3e98e1a498 | ||
|
|
4c31a91bb4 | ||
|
|
d6faaba01c | ||
|
|
56175e62d6 | ||
|
|
283838f444 | ||
|
|
49fe335c73 | ||
|
|
1875f050c6 | ||
|
|
52d8658be3 | ||
|
|
32a454c034 | ||
|
|
07646e8c42 | ||
|
|
20dcebbab9 | ||
|
|
c4c17b3adc | ||
|
|
cf758bfe60 | ||
|
|
8c7ec0a237 | ||
|
|
80462218a7 | ||
|
|
26b5f86983 | ||
|
|
ba6cf6d938 | ||
|
|
643545a18a | ||
|
|
b922487d76 | ||
|
|
54eb1ea735 | ||
|
|
9043c774f7 |
@@ -27,14 +27,15 @@ dependencies {
|
|||||||
implementation(project(":open_api"))
|
implementation(project(":open_api"))
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
implementation(project(":frontend"))
|
implementation(project(":frontend"))
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
|
||||||
annotationProcessor("org.projectlombok:lombok")
|
annotationProcessor("org.projectlombok:lombok")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
|
||||||
implementation("com.mysql:mysql-connector-j:8.0.32")
|
implementation("com.mysql:mysql-connector-j:8.0.32")
|
||||||
implementation("org.mybatis:mybatis:3.5.13")
|
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-configuration-processor")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-hateoas")
|
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
|
//AOP
|
||||||
implementation("org.aspectj:aspectjrt:1.9.19")
|
implementation("org.aspectj:aspectjrt:1.9.19")
|
||||||
@@ -42,6 +43,8 @@ dependencies {
|
|||||||
implementation("org.springframework:spring-aop")
|
implementation("org.springframework:spring-aop")
|
||||||
implementation("org.springframework:spring-aspects")
|
implementation("org.springframework:spring-aspects")
|
||||||
|
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.compileJava {
|
tasks.compileJava {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
|||||||
public class AltitudeWebApplication {
|
public class AltitudeWebApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(AltitudeWebApplication.class, args);
|
|
||||||
Connection.initDatabases();
|
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().permitAll()
|
||||||
|
)
|
||||||
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-1
@@ -1,7 +1,7 @@
|
|||||||
package com.alttd.altitudeweb.controllers.history;
|
package com.alttd.altitudeweb.controllers.history;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.api.HistoryApi;
|
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.HistoryCountDto;
|
||||||
import com.alttd.altitudeweb.model.PunishmentHistoryListDto;
|
import com.alttd.altitudeweb.model.PunishmentHistoryListDto;
|
||||||
import com.alttd.altitudeweb.setup.Connection;
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
@@ -153,6 +153,24 @@ public class HistoryApiController implements HistoryApi {
|
|||||||
return ResponseEntity.ok().body(searchResultCountCompletableFuture.join());
|
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
|
@Override
|
||||||
public ResponseEntity<PunishmentHistoryDto> getHistoryById(String type, Integer id) {
|
public ResponseEntity<PunishmentHistoryDto> getHistoryById(String type, Integer id) {
|
||||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
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;
|
package com.alttd.altitudeweb.controllers.team;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.api.TeamApi;
|
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.setup.Connection;
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
import com.alttd.altitudeweb.database.luckperms.Player;
|
import com.alttd.altitudeweb.database.luckperms.Player;
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.alttd.altitudeweb.controllers.limits;
|
package com.alttd.altitudeweb.services.limits;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
+1
-1
@@ -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.ElementType;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
+3
-3
@@ -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.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@@ -26,8 +26,8 @@ public class RateLimitAspect {
|
|||||||
private final InMemoryRateLimiterService rateLimiterService;
|
private final InMemoryRateLimiterService rateLimiterService;
|
||||||
|
|
||||||
@Around("""
|
@Around("""
|
||||||
@annotation(com.alttd.altitudeweb.controllers.limits.RateLimit)
|
@annotation(com.alttd.altitudeweb.services.limits.RateLimit)
|
||||||
|| @within(com.alttd.altitudeweb.controllers.limits.RateLimit)""")
|
|| @within(com.alttd.altitudeweb.services.limits.RateLimit)""")
|
||||||
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
|
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
if (requestAttributes == null) {
|
if (requestAttributes == null) {
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.alttd.altitudeweb.controllers.limits;
|
package com.alttd.altitudeweb.services.limits;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
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.user=${DB_USER:root}
|
||||||
database.password=${DB_PASSWORD:root}
|
database.password=${DB_PASSWORD:root}
|
||||||
cors.allowed-origins=${CORS:https://alttd.com}
|
cors.allowed-origins=${CORS:https://alttd.com}
|
||||||
|
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
||||||
logging.level.com.alttd.altitudeweb=INFO
|
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,
|
private List<HistoryRecord> getRecent(@NotNull String tableName, @NotNull UserType userType,
|
||||||
@NotNull UUID uuid, int page) {
|
@NotNull UUID uuid, int page) {
|
||||||
int offset = page * PAGE_SIZE;
|
int offset = page * PAGE_SIZE;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public interface TeamMemberMapper {
|
|||||||
FROM luckperms_user_permissions AS permissions
|
FROM luckperms_user_permissions AS permissions
|
||||||
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
||||||
WHERE permission = #{groupPermission}
|
WHERE permission = #{groupPermission}
|
||||||
|
AND world = 'global'
|
||||||
""")
|
""")
|
||||||
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
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;
|
||||||
|
}
|
||||||
+102
@@ -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);
|
log.debug("Loaded default database settings {}", databaseSettings);
|
||||||
Connection connection = new Connection(databaseSettings, addMappers);
|
Connection connection = new Connection(databaseSettings, addMappers);
|
||||||
log.debug("Created default database connection {}", connection);
|
log.debug("Created default database connection {}", connection);
|
||||||
|
connections.put(Databases.DEFAULT, connection);
|
||||||
return CompletableFuture.completedFuture(connection);
|
return CompletableFuture.completedFuture(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.alttd.altitudeweb.setup;
|
package com.alttd.altitudeweb.setup;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||||
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.ibatis.session.SqlSession;
|
import org.apache.ibatis.session.SqlSession;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Statement;
|
import java.sql.Statement;
|
||||||
@@ -12,15 +14,21 @@ import java.sql.Statement;
|
|||||||
public class InitializeWebDb {
|
public class InitializeWebDb {
|
||||||
|
|
||||||
protected static void init() {
|
protected static void init() {
|
||||||
log.info("Initializing LiteBans");
|
log.info("Initializing WebDb");
|
||||||
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
||||||
configuration.addMapper(SettingsMapper.class);
|
configuration.addMapper(SettingsMapper.class);
|
||||||
|
configuration.addMapper(KeyPairMapper.class);
|
||||||
}).join()
|
}).join()
|
||||||
.runQuery(InitializeWebDb::createSettingsTable);
|
.runQuery(SqlSession -> {
|
||||||
log.debug("Initialized LuckPerms");
|
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 = """
|
String query = """
|
||||||
CREATE TABLE IF NOT EXISTS db_connection_settings
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,19 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@angular/cdk": "^19.2.18",
|
||||||
"@angular/common": "^19.2.0",
|
"@angular/common": "^19.2.0",
|
||||||
"@angular/compiler": "^19.2.0",
|
"@angular/compiler": "^19.2.0",
|
||||||
"@angular/core": "^19.2.0",
|
"@angular/core": "^19.2.0",
|
||||||
"@angular/forms": "^19.2.0",
|
"@angular/forms": "^19.2.0",
|
||||||
|
"@angular/material": "^19.2.18",
|
||||||
"@angular/platform-browser": "^19.2.0",
|
"@angular/platform-browser": "^19.2.0",
|
||||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||||
"@angular/router": "^19.2.0",
|
"@angular/router": "^19.2.0",
|
||||||
|
"@types/three": "^0.177.0",
|
||||||
"ngx-cookie-service": "^19.1.2",
|
"ngx-cookie-service": "^19.1.2",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
|
"three": "^0.177.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -5,6 +5,10 @@ export const routes: Routes = [
|
|||||||
path: '',
|
path: '',
|
||||||
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
|
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'particles',
|
||||||
|
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'map',
|
path: 'map',
|
||||||
loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
|
loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
|
||||||
@@ -64,7 +68,49 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'warps',
|
path: 'warps',
|
||||||
loadComponent: () => import('./warps/warps.component').then(m => m.WarpsComponent)
|
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>
|
</div>
|
||||||
</app-header>
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="detailsBackButton">
|
||||||
<ng-container *ngIf="punishment === undefined">
|
<ng-container *ngIf="punishment === undefined">
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<a [routerLink]="['/bans']">< Back</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="columnSection center">
|
||||||
<ng-container *ngIf="punishment">
|
<ng-container *ngIf="punishment">
|
||||||
<table [cellSpacing]="0">
|
|
||||||
<div>
|
<div>
|
||||||
<p>type: {{ this.historyFormat.getType(punishment) }}</p>
|
<span class="tag tagInfo"
|
||||||
<p>is active: {{ this.historyFormat.isActive(punishment) }}</p>
|
[ngClass]="{
|
||||||
<tbody>
|
'tagPermanent': this.historyFormat.isPermanent(punishment),
|
||||||
<tr>
|
'tagExpired': !this.historyFormat.isPermanent(punishment)
|
||||||
<td>Player</td>
|
}">
|
||||||
<td>
|
{{ this.historyFormat.getType(punishment) }}
|
||||||
<div class="playerContainer">
|
</span>
|
||||||
<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>
|
</div>
|
||||||
</td>
|
<div>
|
||||||
</tr>
|
<span
|
||||||
<tr>
|
class="tag tagInfo"
|
||||||
<td>Moderator</td>
|
[ngClass]="{
|
||||||
<td>
|
'tagActive': this.historyFormat.isActive(punishment),
|
||||||
<div class="playerContainer">
|
'tagInactive': !this.historyFormat.isActive(punishment)
|
||||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid)" width="25"
|
}">
|
||||||
height="25"
|
{{ this.historyFormat.isActive(punishment) ? 'Active' : 'Inactive' }}
|
||||||
alt="{{punishment.punishedBy}}'s Minecraft skin">
|
</span>
|
||||||
<span class="username">{{ punishment.punishedBy }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</ng-container>
|
||||||
</tr>
|
</section>
|
||||||
<tr>
|
<section class="columnSection">
|
||||||
<td>Reason</td>
|
<div class="columnContainer">
|
||||||
<td>{{ punishment.reason | removeTrailingPeriod }}</td>
|
<div class="columnParagraph">
|
||||||
</tr>
|
<ng-container *ngIf="punishment">
|
||||||
<tr>
|
<div class="playerContainer">
|
||||||
<td>Date</td>
|
<h2>Player</h2>
|
||||||
<td>{{ this.historyFormat.getPunishmentTime(punishment) }}</td>
|
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid, '150')"
|
||||||
</tr>
|
width="150"
|
||||||
<tr>
|
height="150"
|
||||||
<td>Expires</td>
|
alt="{{punishment.username}}'s Minecraft skin"
|
||||||
<td>{{ this.historyFormat.getExpiredTime(punishment) }}</td>
|
>
|
||||||
</tr>
|
<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">
|
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
|
||||||
<tr>
|
<span>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</span>
|
||||||
<td>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</td>
|
<span>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</span>
|
||||||
<td>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</td>
|
|
||||||
</tr>
|
|
||||||
</ng-container>
|
|
||||||
</tbody>
|
|
||||||
</div>
|
|
||||||
</table>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</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 {Component, OnInit} from '@angular/core';
|
||||||
import {HistoryService, PunishmentHistory} from '../../../api';
|
import {HistoryService, PunishmentHistory} from '../../../api';
|
||||||
import {NgIf, NgOptimizedImage} from '@angular/common';
|
import {NgClass, NgIf, NgOptimizedImage} from '@angular/common';
|
||||||
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
|
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
|
||||||
import {HistoryFormatService} from '../history-format.service';
|
import {HistoryFormatService} from '../history-format.service';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute, RouterLink} from '@angular/router';
|
||||||
import {catchError, map} from 'rxjs';
|
import {catchError, map} from 'rxjs';
|
||||||
import {HeaderComponent} from '../../header/header.component';
|
import {HeaderComponent} from '../../header/header.component';
|
||||||
|
|
||||||
@@ -13,7 +13,9 @@ import {HeaderComponent} from '../../header/header.component';
|
|||||||
NgIf,
|
NgIf,
|
||||||
NgOptimizedImage,
|
NgOptimizedImage,
|
||||||
RemoveTrailingPeriodPipe,
|
RemoveTrailingPeriodPipe,
|
||||||
HeaderComponent
|
HeaderComponent,
|
||||||
|
RouterLink,
|
||||||
|
NgClass
|
||||||
],
|
],
|
||||||
templateUrl: './details.component.html',
|
templateUrl: './details.component.html',
|
||||||
styleUrl: './details.component.scss'
|
styleUrl: './details.component.scss'
|
||||||
|
|||||||
@@ -22,9 +22,19 @@ export class HistoryFormatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isActive(entry: PunishmentHistory): boolean {
|
public isActive(entry: PunishmentHistory): boolean {
|
||||||
|
if (entry.removedBy !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (entry.expiryTime <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return entry.expiryTime > Date.now();
|
return entry.expiryTime > Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isPermanent(entry: PunishmentHistory): boolean {
|
||||||
|
return entry.expiryTime <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
public getType(entry: PunishmentHistory): string {
|
public getType(entry: PunishmentHistory): string {
|
||||||
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
|
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
|
||||||
}
|
}
|
||||||
@@ -48,11 +58,11 @@ export class HistoryFormatService {
|
|||||||
}) + " " + suffix;
|
}) + " " + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAvatarUrl(entry: string): string {
|
public getAvatarUrl(entry: string, size: string = '25'): string {
|
||||||
let uuid = entry.replace('-', '');
|
let uuid = entry.replace('-', '');
|
||||||
if (uuid === 'C') {
|
if (uuid === 'C') {
|
||||||
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
|
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
|
||||||
}
|
}
|
||||||
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
|
return `https://crafatar.com/avatars/${uuid}?size=${size}&overlay`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 <amount> -</span> Allows
|
||||||
|
you to buy claimblocks (0.5 in game per block)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sellclaimblocks <amount> -</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 <username/public> -</span>
|
||||||
|
Gives accesstrust to a user/everyone
|
||||||
|
</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/containertrust <username/public> -</span> Gives
|
||||||
|
containertrust to a user/everyone
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/expandclaim <amount> -</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 <ability> -</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 <username> -</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 <name> -</span> Gives your
|
||||||
|
current MyPet the specified name
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petrelease <name> -</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 <username> [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 <%> -</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 <amount> -</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 <username> <amount> [-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 <item> -</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 <item> -</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 <radius> [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 <partyname> <password> -</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 <username> -</span> Invite
|
||||||
|
a user to your party
|
||||||
|
</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/party join <partyname> [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 <message> -</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 <newname> -</span> Leave a
|
||||||
|
party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party owner <username> -</span> Makes
|
||||||
|
the specified user owner of the party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party password <password> -</span> Sets
|
||||||
|
or changes a password for the party
|
||||||
|
</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/party remove (/party kick) <username> -</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 <username> -</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 <username> -</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 <signline> <text> -</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 <username/nickname> -</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 <current xp> <goal xp> -</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 <xp> -</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 <item> -</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 <username> <message> -</span>
|
||||||
|
Sends a mail to the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/ignore <username> -</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 <message> -</span> Replies to the
|
||||||
|
last user to send you a message
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/c <message> -</span> Replies to the
|
||||||
|
last user you sent a message to
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/msg <username> <message> -</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 <homename> -</span> Sets a home
|
||||||
|
with the specified name
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/delhome <homename> -</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 <username> -</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 <username> -</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 <username> -</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>;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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. We’ve since opened
|
||||||
|
our doors to welcome anyone and everyone to enjoy the custom survival experience we’ve 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 wasn’t 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, we’ve 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 <block></b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>McMMO & 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 you’d 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 we’ve 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
<li class="nav_li">
|
<li class="nav_li">
|
||||||
<span
|
<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>
|
'nicknames', 'bans', 'discord-bans'])" class="nav_link fake_link" [ngClass]="active">Reference</span>
|
||||||
<ul class="dropdown">
|
<ul class="dropdown">
|
||||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/guide']">Guide Book</a></li>
|
<li class="nav_li"><a class="nav_link2" [routerLink]="['/guide']">Guide Book</a></li>
|
||||||
|
|||||||
@@ -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 someone’s 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
|
||||||
|
it’s 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 don’t 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>
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 <description of map art></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,41 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
.frames-container {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particles-list {
|
||||||
|
height: 550px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-item {
|
||||||
|
display: flex;
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FramesComponent } from './frames.component';
|
||||||
|
|
||||||
|
describe('FramesComponent', () => {
|
||||||
|
let component: FramesComponent;
|
||||||
|
let fixture: ComponentFixture<FramesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [FramesComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(FramesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatButton, MatIconButton} from "@angular/material/button";
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
|
||||||
|
import {MatTab, MatTabGroup} from "@angular/material/tabs";
|
||||||
|
import {NgForOf, NgIf} from "@angular/common";
|
||||||
|
import {ParticleData} from '../../models/particle.model';
|
||||||
|
import {MatIcon} from '@angular/material/icon';
|
||||||
|
import {ParticleManagerService} from '../../services/particle-manager.service';
|
||||||
|
import {FrameManagerService} from '../../services/frame-manager.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-frames',
|
||||||
|
imports: [
|
||||||
|
MatButton,
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
MatIcon,
|
||||||
|
MatIconButton,
|
||||||
|
MatTab,
|
||||||
|
MatTabGroup,
|
||||||
|
NgForOf,
|
||||||
|
NgIf
|
||||||
|
],
|
||||||
|
templateUrl: './frames.component.html',
|
||||||
|
styleUrl: './frames.component.scss'
|
||||||
|
})
|
||||||
|
export class FramesComponent {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private particleManagerService: ParticleManagerService,
|
||||||
|
private frameManagerService: FrameManagerService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the particle data
|
||||||
|
*/
|
||||||
|
public get particleData(): ParticleData {
|
||||||
|
return this.particleManagerService.getParticleData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current frame
|
||||||
|
*/
|
||||||
|
public get currentFrame(): string {
|
||||||
|
return this.particleManagerService.getCurrentFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all frames
|
||||||
|
*/
|
||||||
|
public get frames(): string[] {
|
||||||
|
return this.particleManagerService.getFrames();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new frame
|
||||||
|
*/
|
||||||
|
public addFrame(): void {
|
||||||
|
this.frameManagerService.addFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a different frame
|
||||||
|
*/
|
||||||
|
public switchFrame(frameId: string): void {
|
||||||
|
this.frameManagerService.switchFrame(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a particle
|
||||||
|
*/
|
||||||
|
public removeParticle(frameId: string, index: number): void {
|
||||||
|
this.particleManagerService.removeParticle(frameId, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a frame
|
||||||
|
*/
|
||||||
|
public removeFrame(frameId: string): void {
|
||||||
|
this.frameManagerService.removeFrame(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {ParticleComponent} from './particle.component';
|
||||||
|
|
||||||
|
describe('ParticleComponent', () => {
|
||||||
|
let component: ParticleComponent;
|
||||||
|
let fixture: ComponentFixture<ParticleComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ParticleComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ParticleComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||||
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
|
import {ParticleManagerService} from '../../services/particle-manager.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-particle',
|
||||||
|
imports: [
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './particle.component.html',
|
||||||
|
styleUrl: './particle.component.scss'
|
||||||
|
})
|
||||||
|
export class ParticleComponent {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private particleManagerService: ParticleManagerService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the selected color
|
||||||
|
*/
|
||||||
|
public get selectedColor(): string {
|
||||||
|
return this.particleManagerService.getSelectedColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the selected color
|
||||||
|
*/
|
||||||
|
public set selectedColor(color: string) {
|
||||||
|
this.particleManagerService.setSelectedColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PropertiesComponent } from './properties.component';
|
||||||
|
|
||||||
|
describe('PropertiesComponent', () => {
|
||||||
|
let component: PropertiesComponent;
|
||||||
|
let fixture: ComponentFixture<PropertiesComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [PropertiesComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(PropertiesComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
|
||||||
|
import {MatCheckbox} from "@angular/material/checkbox";
|
||||||
|
import {MatFormField, MatInput, MatLabel} from "@angular/material/input";
|
||||||
|
import {NgForOf} from "@angular/common";
|
||||||
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||||
|
import {ParticleData, ParticleType} from '../../models/particle.model';
|
||||||
|
import {MatSelect} from '@angular/material/select';
|
||||||
|
import {MatOption} from '@angular/material/core';
|
||||||
|
import {ParticleManagerService} from '../../services/particle-manager.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-particle-properties',
|
||||||
|
imports: [
|
||||||
|
MatCard,
|
||||||
|
MatCardContent,
|
||||||
|
MatCardHeader,
|
||||||
|
MatCardTitle,
|
||||||
|
MatCheckbox,
|
||||||
|
MatFormField,
|
||||||
|
MatInput,
|
||||||
|
MatLabel,
|
||||||
|
MatOption,
|
||||||
|
MatSelect,
|
||||||
|
NgForOf,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './properties.component.html',
|
||||||
|
styleUrl: './properties.component.scss'
|
||||||
|
})
|
||||||
|
export class PropertiesComponent {
|
||||||
|
public particleTypes = Object.values(ParticleType);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private particleManagerService: ParticleManagerService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public get particleData(): ParticleData {
|
||||||
|
return this.particleManagerService.getParticleData();
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
<div #rendererContainer class="renderer-container">
|
||||||
|
<div class="plane-controls-overlay">
|
||||||
|
<button mat-mini-fab color="primary" (click)="togglePlaneLock()"
|
||||||
|
[matTooltip]="isPlaneLocked ? 'Unlock Plane' : 'Lock Plane'">
|
||||||
|
<mat-icon>{{ isPlaneLocked ? 'lock' : 'lock_open' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div *ngIf="isPlaneLocked" class="plane-orientation-buttons">
|
||||||
|
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_ABOVE)"
|
||||||
|
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_ABOVE"
|
||||||
|
matTooltip="Vertical Above">
|
||||||
|
<mat-icon>arrow_upward</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_BELOW)"
|
||||||
|
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_BELOW"
|
||||||
|
matTooltip="Vertical Below">
|
||||||
|
<mat-icon>arrow_downward</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_FRONT)"
|
||||||
|
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_FRONT"
|
||||||
|
matTooltip="Horizontal Front">
|
||||||
|
<mat-icon>arrow_forward</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_BEHIND)"
|
||||||
|
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_BEHIND"
|
||||||
|
matTooltip="Horizontal Behind">
|
||||||
|
<mat-icon>arrow_back</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_RIGHT)"
|
||||||
|
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_RIGHT"
|
||||||
|
matTooltip="Horizontal Right">
|
||||||
|
<mat-icon>arrow_right</mat-icon>
|
||||||
|
</button>
|
||||||
|
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_LEFT)"
|
||||||
|
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_LEFT"
|
||||||
|
matTooltip="Horizontal Left">
|
||||||
|
<mat-icon>arrow_left</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
.renderer-container {
|
||||||
|
height: 1000px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative; /* Added for absolute positioning of overlay */
|
||||||
|
}
|
||||||
|
|
||||||
|
.plane-controls-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plane-orientation-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(3, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plane-orientation-buttons button {
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plane-orientation-buttons button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plane-orientation-buttons button.active {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { RenderContainerComponent } from './render-container.component';
|
||||||
|
|
||||||
|
describe('RenderContainerComponent', () => {
|
||||||
|
let component: RenderContainerComponent;
|
||||||
|
let fixture: ComponentFixture<RenderContainerComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [RenderContainerComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(RenderContainerComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import {AfterViewInit, Component, ElementRef, OnDestroy, ViewChild} from '@angular/core';
|
||||||
|
import {MatMiniFabButton} from '@angular/material/button';
|
||||||
|
import {NgIf} from '@angular/common';
|
||||||
|
import {IntersectionPlaneService, PlaneOrientation} from '../../services/intersection-plane.service';
|
||||||
|
import {MatIcon} from '@angular/material/icon';
|
||||||
|
import {MatTooltip} from '@angular/material/tooltip';
|
||||||
|
import {RendererService} from '../../services/renderer.service';
|
||||||
|
import {PlayerModelService} from '../../services/player-model.service';
|
||||||
|
import {InputHandlerService} from '../../services/input-handler.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-render-container',
|
||||||
|
imports: [
|
||||||
|
MatIcon,
|
||||||
|
MatMiniFabButton,
|
||||||
|
MatTooltip,
|
||||||
|
NgIf
|
||||||
|
],
|
||||||
|
templateUrl: './render-container.component.html',
|
||||||
|
styleUrl: './render-container.component.scss'
|
||||||
|
})
|
||||||
|
export class RenderContainerComponent implements AfterViewInit, OnDestroy {
|
||||||
|
@ViewChild('rendererContainer') rendererContainer!: ElementRef;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private intersectionPlaneService: IntersectionPlaneService,
|
||||||
|
private playerModelService: PlayerModelService,
|
||||||
|
private inputHandlerService: InputHandlerService,
|
||||||
|
private rendererService: RendererService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.initializeScene();
|
||||||
|
this.animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up resources when component is destroyed
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.rendererService.renderer) {
|
||||||
|
this.inputHandlerService.cleanup(this.rendererService.renderer.domElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the 3D scene and all related components
|
||||||
|
*/
|
||||||
|
private initializeScene(): void {
|
||||||
|
this.rendererService.initializeRenderer(this.rendererContainer);
|
||||||
|
this.playerModelService.loadSkinTexture('/public/img/skins/steve.png')
|
||||||
|
.then(() => {
|
||||||
|
// Then create the player model with the texture applied
|
||||||
|
this.playerModelService.createPlayerModel();
|
||||||
|
});
|
||||||
|
this.intersectionPlaneService.createIntersectionPlane();
|
||||||
|
this.inputHandlerService.initializeInputHandlers(this.rendererService.renderer.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animation loop
|
||||||
|
*/
|
||||||
|
private animate(): void {
|
||||||
|
requestAnimationFrame(this.animate.bind(this));
|
||||||
|
this.intersectionPlaneService.updatePlaneOrientation(this.rendererService.camera);
|
||||||
|
this.rendererService.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether the plane is locked
|
||||||
|
*/
|
||||||
|
public get isPlaneLocked(): boolean {
|
||||||
|
return this.intersectionPlaneService.isPlaneLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the plane locked state
|
||||||
|
*/
|
||||||
|
public togglePlaneLock(): void {
|
||||||
|
const newLockedState = !this.isPlaneLocked;
|
||||||
|
this.intersectionPlaneService.setPlaneLocked(newLockedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current plane orientation
|
||||||
|
*/
|
||||||
|
public get currentPlaneOrientation(): PlaneOrientation {
|
||||||
|
return this.intersectionPlaneService.getCurrentOrientation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the plane orientation
|
||||||
|
*/
|
||||||
|
public setPlaneOrientation(orientation: PlaneOrientation): void {
|
||||||
|
this.intersectionPlaneService.setPlaneOrientation(orientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available plane orientations
|
||||||
|
*/
|
||||||
|
public get planeOrientations(): typeof PlaneOrientation {
|
||||||
|
return PlaneOrientation;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Defines the types of particles available in the system
|
||||||
|
*/
|
||||||
|
export enum ParticleType {
|
||||||
|
REDSTONE = 'REDSTONE',
|
||||||
|
// Other particle types can be added later
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single particle's information
|
||||||
|
*/
|
||||||
|
export interface ParticleInfo {
|
||||||
|
particle_type: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
color: string;
|
||||||
|
extra: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the complete particle data structure
|
||||||
|
*/
|
||||||
|
export interface ParticleData {
|
||||||
|
particle_name: string;
|
||||||
|
display_name: string;
|
||||||
|
particle_type: string;
|
||||||
|
lore: string;
|
||||||
|
display_item: string;
|
||||||
|
permission: string;
|
||||||
|
package_permission: string;
|
||||||
|
frame_delay: number;
|
||||||
|
repeat: number;
|
||||||
|
repeat_delay: number;
|
||||||
|
random_offset: number;
|
||||||
|
stationary: boolean;
|
||||||
|
frames: {
|
||||||
|
[frameId: string]: ParticleInfo[];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<app-header current_page="particles" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Particle Creator</h1>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="column">
|
||||||
|
<div class="renderer-section column">
|
||||||
|
<div class="flex row">
|
||||||
|
<div class="flex side-column">
|
||||||
|
<app-particle-properties></app-particle-properties>
|
||||||
|
</div>
|
||||||
|
<div class="flex middle-column">
|
||||||
|
<app-render-container></app-render-container>
|
||||||
|
<div class="plane-controls">
|
||||||
|
<label>Plane Position (Z-axis):</label>
|
||||||
|
<mat-slider [min]="minOffset" [max]="maxOffset" step="1" #planeSlider>
|
||||||
|
<input matSliderThumb [(ngModel)]="planePosition" (input)="updatePlanePosition($event)">
|
||||||
|
</mat-slider>
|
||||||
|
<span>{{ planePosition }} offset from center</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex side-column">
|
||||||
|
<app-particle></app-particle>
|
||||||
|
<app-frames></app-frames>
|
||||||
|
<div>
|
||||||
|
<button mat-fab extended (click)="copyJson()">
|
||||||
|
<mat-icon>content_copy</mat-icon>
|
||||||
|
Copy JSON to clipboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
.renderer-section {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 300px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-column {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle-column {
|
||||||
|
flex: 2;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -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,101 @@
|
|||||||
|
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatInputModule} from '@angular/material/input';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
import {MatSelectModule} from '@angular/material/select';
|
||||||
|
import {MatSliderModule} from '@angular/material/slider';
|
||||||
|
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||||||
|
import {MatTabsModule} from '@angular/material/tabs';
|
||||||
|
import {MatCardModule} from '@angular/material/card';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
import {HeaderComponent} from '../header/header.component';
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import {IntersectionPlaneService} from './services/intersection-plane.service';
|
||||||
|
import {ParticleManagerService} from './services/particle-manager.service';
|
||||||
|
|
||||||
|
// Models
|
||||||
|
import {PropertiesComponent} from './components/properties/properties.component';
|
||||||
|
import {ParticleComponent} from './components/particle/particle.component';
|
||||||
|
import {FramesComponent} from './components/frames/frames.component';
|
||||||
|
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||||
|
import {RenderContainerComponent} from './components/render-container/render-container.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-particles',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatSliderModule,
|
||||||
|
MatCheckboxModule,
|
||||||
|
MatTabsModule,
|
||||||
|
MatCardModule,
|
||||||
|
MatIconModule,
|
||||||
|
HeaderComponent,
|
||||||
|
PropertiesComponent,
|
||||||
|
ParticleComponent,
|
||||||
|
FramesComponent,
|
||||||
|
RenderContainerComponent,
|
||||||
|
],
|
||||||
|
templateUrl: './particles.component.html',
|
||||||
|
styleUrl: './particles.component.scss'
|
||||||
|
})
|
||||||
|
export class ParticlesComponent {
|
||||||
|
@ViewChild('planeSlider') planeSlider!: ElementRef;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private intersectionPlaneService: IntersectionPlaneService,
|
||||||
|
private particleManagerService: ParticleManagerService,
|
||||||
|
private matSnackBar: MatSnackBar,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update plane position based on slider
|
||||||
|
*/
|
||||||
|
public updatePlanePosition(event: Event): void {
|
||||||
|
const slider = event.target as HTMLInputElement;
|
||||||
|
const value = Number(slider.value);
|
||||||
|
this.intersectionPlaneService.updatePlanePosition(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current plane position
|
||||||
|
*/
|
||||||
|
public get planePosition(): number {
|
||||||
|
return this.intersectionPlaneService.getPlanePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
public set planePosition(newPlanePosition: number) {
|
||||||
|
this.intersectionPlaneService.updatePlanePosition(newPlanePosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get maxOffset(): number {
|
||||||
|
return this.intersectionPlaneService.getMaxOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get minOffset(): number {
|
||||||
|
return this.intersectionPlaneService.getMinOffset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate JSON output
|
||||||
|
*/
|
||||||
|
public generateJson(): string {
|
||||||
|
return this.particleManagerService.generateJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
public copyJson() {
|
||||||
|
navigator.clipboard.writeText(this.generateJson()).then(() => {
|
||||||
|
this.matSnackBar.open('Copied to clipboard', '', {duration: 2000})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ParticleManagerService } from './particle-manager.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for managing animation frames
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class FrameManagerService {
|
||||||
|
|
||||||
|
constructor(private particleManager: ParticleManagerService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new frame
|
||||||
|
*/
|
||||||
|
addFrame(): void {
|
||||||
|
const frames = this.particleManager.getFrames();
|
||||||
|
const frameId = `frame${frames.length + 1}`;
|
||||||
|
|
||||||
|
frames.push(frameId);
|
||||||
|
this.particleManager.setFrames(frames);
|
||||||
|
|
||||||
|
const particleData = this.particleManager.getParticleData();
|
||||||
|
particleData.frames[frameId] = [];
|
||||||
|
this.particleManager.setParticleData(particleData);
|
||||||
|
|
||||||
|
this.switchFrame(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches to a different frame
|
||||||
|
*/
|
||||||
|
switchFrame(frameId: string): void {
|
||||||
|
this.particleManager.setCurrentFrame(frameId);
|
||||||
|
this.particleManager.clearParticleVisuals();
|
||||||
|
this.particleManager.renderFrameParticles(frameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a frame
|
||||||
|
*/
|
||||||
|
removeFrame(frameId: string): void {
|
||||||
|
const frames = this.particleManager.getFrames();
|
||||||
|
const index = frames.indexOf(frameId);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
frames.splice(index, 1);
|
||||||
|
this.particleManager.setFrames(frames);
|
||||||
|
|
||||||
|
const particleData = this.particleManager.getParticleData();
|
||||||
|
delete particleData.frames[frameId];
|
||||||
|
this.particleManager.setParticleData(particleData);
|
||||||
|
|
||||||
|
// Switch to first frame if we removed the current one
|
||||||
|
if (frameId === this.particleManager.getCurrentFrame() && frames.length > 0) {
|
||||||
|
this.switchFrame(frames[0]);
|
||||||
|
} else if (frames.length === 0) {
|
||||||
|
// If no frames left, add one
|
||||||
|
this.addFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { Injectable, ElementRef } from '@angular/core';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { RendererService } from './renderer.service';
|
||||||
|
import { IntersectionPlaneService } from './intersection-plane.service';
|
||||||
|
import { ParticleManagerService } from './particle-manager.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for handling user input interactions
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class InputHandlerService {
|
||||||
|
private raycaster = new THREE.Raycaster();
|
||||||
|
private mouse = new THREE.Vector2();
|
||||||
|
private isDragging = false;
|
||||||
|
private mouseDownTime = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private rendererService: RendererService,
|
||||||
|
private intersectionPlaneService: IntersectionPlaneService,
|
||||||
|
private particleManagerService: ParticleManagerService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes input event listeners
|
||||||
|
*/
|
||||||
|
initializeInputHandlers(rendererElement: HTMLElement): void {
|
||||||
|
rendererElement.addEventListener('mousedown', this.onMouseDown.bind(this));
|
||||||
|
rendererElement.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||||
|
rendererElement.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||||
|
window.addEventListener('resize', this.onWindowResize.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles mouse down event
|
||||||
|
*/
|
||||||
|
private onMouseDown(event: MouseEvent): void {
|
||||||
|
this.isDragging = false;
|
||||||
|
this.mouseDownTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles mouse up event
|
||||||
|
*/
|
||||||
|
private onMouseUp(event: MouseEvent): void {
|
||||||
|
// If mouse was down for less than 200ms and didn't move much, consider it a click, not a drag
|
||||||
|
if (Date.now() - this.mouseDownTime < 200 && !this.isDragging) {
|
||||||
|
this.handlePlaneClick(event);
|
||||||
|
}
|
||||||
|
this.isDragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles mouse move event
|
||||||
|
*/
|
||||||
|
private onMouseMove(event: MouseEvent): void {
|
||||||
|
// If mouse moves while button is pressed, it's a drag
|
||||||
|
if (event.buttons > 0) {
|
||||||
|
this.isDragging = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles mouse click on the plane
|
||||||
|
*/
|
||||||
|
private handlePlaneClick(event: MouseEvent): void {
|
||||||
|
// Calculate mouse position in normalized device coordinates
|
||||||
|
const rect = this.rendererService.renderer.domElement.getBoundingClientRect();
|
||||||
|
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||||
|
|
||||||
|
// Update the picking ray with the camera and mouse position
|
||||||
|
this.raycaster.setFromCamera(this.mouse, this.rendererService.camera);
|
||||||
|
|
||||||
|
// Calculate objects intersecting the picking ray
|
||||||
|
const intersects = this.raycaster.intersectObject(this.intersectionPlaneService.getIntersectionPlane());
|
||||||
|
|
||||||
|
if (intersects.length > 0) {
|
||||||
|
const point = intersects[0].point;
|
||||||
|
this.particleManagerService.addParticle(point.x, point.y, point.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles window resize event
|
||||||
|
*/
|
||||||
|
private onWindowResize(): void {
|
||||||
|
// This is delegated to the renderer service
|
||||||
|
const container = this.rendererService.renderer.domElement.parentElement;
|
||||||
|
if (container) {
|
||||||
|
this.rendererService.onWindowResize(new ElementRef(container));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes event listeners
|
||||||
|
*/
|
||||||
|
cleanup(rendererElement: HTMLElement): void {
|
||||||
|
rendererElement.removeEventListener('mousedown', this.onMouseDown.bind(this));
|
||||||
|
rendererElement.removeEventListener('mouseup', this.onMouseUp.bind(this));
|
||||||
|
rendererElement.removeEventListener('mousemove', this.onMouseMove.bind(this));
|
||||||
|
window.removeEventListener('resize', this.onWindowResize.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import {RendererService} from './renderer.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the possible orientations of the intersection plane
|
||||||
|
*/
|
||||||
|
export enum PlaneOrientation {
|
||||||
|
VERTICAL_ABOVE,
|
||||||
|
VERTICAL_BELOW,
|
||||||
|
HORIZONTAL_FRONT,
|
||||||
|
HORIZONTAL_BEHIND,
|
||||||
|
HORIZONTAL_RIGHT,
|
||||||
|
HORIZONTAL_LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for managing the intersection plane
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class IntersectionPlaneService {
|
||||||
|
private intersectionPlane!: THREE.Mesh;
|
||||||
|
private planePosition: number = 0; // Position in 1/16th of a block
|
||||||
|
private currentOrientation: PlaneOrientation = PlaneOrientation.HORIZONTAL_FRONT;
|
||||||
|
private planeLocked: boolean = false;
|
||||||
|
|
||||||
|
constructor(private rendererService: RendererService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the intersection plane and adds it to the scene
|
||||||
|
*/
|
||||||
|
createIntersectionPlane(): THREE.Mesh {
|
||||||
|
const planeGeometry = new THREE.PlaneGeometry(3, 3);
|
||||||
|
const planeMaterial = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0x00AA00,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.05,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
});
|
||||||
|
|
||||||
|
this.intersectionPlane = new THREE.Mesh(planeGeometry, planeMaterial);
|
||||||
|
this.intersectionPlane.position.z = 0;
|
||||||
|
// Center the plane vertically with the player (player is about 2 blocks tall)
|
||||||
|
this.intersectionPlane.position.y = 1;
|
||||||
|
this.rendererService.scene.add(this.intersectionPlane);
|
||||||
|
this.intersectionPlane.renderOrder = 1;
|
||||||
|
|
||||||
|
return this.intersectionPlane;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the plane orientation based on camera position
|
||||||
|
*/
|
||||||
|
private determinePlaneOrientation(camera: THREE.Camera): PlaneOrientation {
|
||||||
|
// Check if camera is looking from above or below first
|
||||||
|
const verticalAngle = Math.atan2(
|
||||||
|
camera.position.y,
|
||||||
|
Math.sqrt(camera.position.x * camera.position.x + camera.position.z * camera.position.z)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Threshold angle for considering the camera to be above/below (about 45 degrees)
|
||||||
|
const verticalThreshold = Math.PI / 4;
|
||||||
|
|
||||||
|
if (verticalAngle > verticalThreshold) {
|
||||||
|
return PlaneOrientation.VERTICAL_ABOVE;
|
||||||
|
} else if (verticalAngle < -verticalThreshold) {
|
||||||
|
return PlaneOrientation.VERTICAL_BELOW;
|
||||||
|
} else {
|
||||||
|
// Calculate the angle between camera and player (in the XZ plane)
|
||||||
|
const cameraAngle = Math.atan2(
|
||||||
|
camera.position.x,
|
||||||
|
camera.position.z
|
||||||
|
);
|
||||||
|
|
||||||
|
// Determine which quadrant the camera is in with a 45-degree offset
|
||||||
|
const quadrant = Math.floor((cameraAngle + Math.PI + Math.PI / 4) / (Math.PI / 2)) % 4;
|
||||||
|
|
||||||
|
switch (quadrant) {
|
||||||
|
case 0:
|
||||||
|
return PlaneOrientation.HORIZONTAL_FRONT;
|
||||||
|
case 1:
|
||||||
|
return PlaneOrientation.HORIZONTAL_RIGHT;
|
||||||
|
case 2:
|
||||||
|
return PlaneOrientation.HORIZONTAL_BEHIND;
|
||||||
|
case 3:
|
||||||
|
return PlaneOrientation.HORIZONTAL_LEFT;
|
||||||
|
default:
|
||||||
|
return PlaneOrientation.HORIZONTAL_FRONT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the plane orientation based on camera position
|
||||||
|
*/
|
||||||
|
public updatePlaneOrientation(camera: THREE.Camera): void {
|
||||||
|
if (!this.intersectionPlane) return;
|
||||||
|
|
||||||
|
if (!this.planeLocked) {
|
||||||
|
this.currentOrientation = this.determinePlaneOrientation(camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateIntersectionPlaneOrientation()
|
||||||
|
|
||||||
|
//Restrict plane position to the new bounds and update it
|
||||||
|
this.planePosition = Math.max(this.getMinOffset(), Math.min(this.getMaxOffset(), this.planePosition));
|
||||||
|
this.updatePlanePosition(this.planePosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the plane position based on slider value
|
||||||
|
*/
|
||||||
|
public updatePlanePosition(value: number): void {
|
||||||
|
this.planePosition = value;
|
||||||
|
// Convert from 1/16th block to Three.js units
|
||||||
|
const position = (this.planePosition / 16)
|
||||||
|
|
||||||
|
this.intersectionPlane.position.y = 0.8;
|
||||||
|
this.intersectionPlane.position.x = 0;
|
||||||
|
this.intersectionPlane.position.z = 0;
|
||||||
|
|
||||||
|
// Position based on the current orientation
|
||||||
|
switch (this.currentOrientation) {
|
||||||
|
case PlaneOrientation.VERTICAL_ABOVE:
|
||||||
|
this.intersectionPlane.position.y = 0.8 - position;
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.VERTICAL_BELOW:
|
||||||
|
this.intersectionPlane.position.y = 0.8 + position;
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||||
|
this.intersectionPlane.position.z = position;
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||||
|
this.intersectionPlane.position.z = -position;
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||||
|
this.intersectionPlane.position.x = position;
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||||
|
this.intersectionPlane.position.x = -position;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the plane material color
|
||||||
|
*/
|
||||||
|
private updatePlaneMaterial(color: number): void {
|
||||||
|
this.intersectionPlane.material = new THREE.MeshBasicMaterial({
|
||||||
|
color: color,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.05,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the intersection plane
|
||||||
|
*/
|
||||||
|
public getIntersectionPlane(): THREE.Mesh {
|
||||||
|
return this.intersectionPlane;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current plane position
|
||||||
|
*/
|
||||||
|
public getPlanePosition(): number {
|
||||||
|
return this.planePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMaxOffset(): number {
|
||||||
|
switch (this.currentOrientation) {
|
||||||
|
case PlaneOrientation.VERTICAL_ABOVE:
|
||||||
|
case PlaneOrientation.VERTICAL_BELOW:
|
||||||
|
return 16;
|
||||||
|
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||||
|
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||||
|
return 8;
|
||||||
|
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||||
|
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMinOffset(): number {
|
||||||
|
return this.getMaxOffset() * -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current plane orientation
|
||||||
|
*/
|
||||||
|
public getCurrentOrientation(): PlaneOrientation {
|
||||||
|
return this.currentOrientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the plane orientation manually
|
||||||
|
*/
|
||||||
|
public setPlaneOrientation(orientation: PlaneOrientation): void {
|
||||||
|
this.currentOrientation = orientation;
|
||||||
|
this.updateIntersectionPlaneOrientation();
|
||||||
|
this.updatePlanePosition(this.planePosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateIntersectionPlaneOrientation() {
|
||||||
|
switch (this.currentOrientation) {
|
||||||
|
case PlaneOrientation.VERTICAL_ABOVE:
|
||||||
|
this.intersectionPlane.rotation.x = -Math.PI / 2;
|
||||||
|
this.intersectionPlane.rotation.y = 0;
|
||||||
|
this.updatePlaneMaterial(0xAA0000);
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.VERTICAL_BELOW:
|
||||||
|
this.intersectionPlane.rotation.x = Math.PI / 2;
|
||||||
|
this.intersectionPlane.rotation.y = 0;
|
||||||
|
this.updatePlaneMaterial(0xAA0000);
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||||
|
this.intersectionPlane.rotation.x = 0;
|
||||||
|
this.intersectionPlane.rotation.y = 0;
|
||||||
|
this.updatePlaneMaterial(0x00AA00);
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||||
|
this.intersectionPlane.rotation.x = 0;
|
||||||
|
this.intersectionPlane.rotation.y = Math.PI;
|
||||||
|
this.updatePlaneMaterial(0x00AA00);
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||||
|
this.intersectionPlane.rotation.x = 0;
|
||||||
|
this.intersectionPlane.rotation.y = Math.PI / 2;
|
||||||
|
this.updatePlaneMaterial(0x0000AA);
|
||||||
|
break;
|
||||||
|
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||||
|
this.intersectionPlane.rotation.x = 0;
|
||||||
|
this.intersectionPlane.rotation.y = -Math.PI / 2;
|
||||||
|
this.updatePlaneMaterial(0x0000AA);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets whether the plane orientation is locked
|
||||||
|
*/
|
||||||
|
public isPlaneLocked(): boolean {
|
||||||
|
return this.planeLocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the plane orientation is locked
|
||||||
|
*/
|
||||||
|
public setPlaneLocked(locked: boolean): void {
|
||||||
|
this.planeLocked = locked;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { RendererService } from './renderer.service';
|
||||||
|
import { ParticleData, ParticleInfo, ParticleType } from '../models/particle.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for managing particles in the scene
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ParticleManagerService {
|
||||||
|
private particles: THREE.Mesh[] = [];
|
||||||
|
private particleData: ParticleData = {
|
||||||
|
particle_name: '',
|
||||||
|
display_name: '',
|
||||||
|
particle_type: ParticleType.REDSTONE,
|
||||||
|
lore: '',
|
||||||
|
display_item: 'REDSTONE',
|
||||||
|
permission: '',
|
||||||
|
package_permission: '',
|
||||||
|
frame_delay: 1,
|
||||||
|
repeat: 1,
|
||||||
|
repeat_delay: 0,
|
||||||
|
random_offset: 0,
|
||||||
|
stationary: true,
|
||||||
|
frames: {
|
||||||
|
'frame1': []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private currentFrame: string = 'frame1';
|
||||||
|
private frames: string[] = ['frame1'];
|
||||||
|
private selectedColor: string = '#ff0000';
|
||||||
|
|
||||||
|
constructor(private rendererService: RendererService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a particle at the specified position
|
||||||
|
*/
|
||||||
|
addParticle(x: number, y: number, z: number): void {
|
||||||
|
// Create a visual representation of the particle
|
||||||
|
const particleGeometry = new THREE.SphereGeometry(0.03, 16, 16);
|
||||||
|
const particleMaterial = new THREE.MeshBasicMaterial({color: this.selectedColor});
|
||||||
|
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
|
||||||
|
|
||||||
|
particleMesh.position.set(x, y, z);
|
||||||
|
this.rendererService.scene.add(particleMesh);
|
||||||
|
this.particles.push(particleMesh);
|
||||||
|
|
||||||
|
// Add to particle data
|
||||||
|
const hexColor = this.selectedColor.replace('#', '');
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all particle visuals from the scene
|
||||||
|
*/
|
||||||
|
clearParticleVisuals(): void {
|
||||||
|
for (const particle of this.particles) {
|
||||||
|
this.rendererService.scene.remove(particle);
|
||||||
|
}
|
||||||
|
this.particles = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders particles for a specific frame
|
||||||
|
*/
|
||||||
|
renderFrameParticles(frameId: string): void {
|
||||||
|
if (!this.particleData.frames[frameId]) return;
|
||||||
|
|
||||||
|
for (const particleInfo of this.particleData.frames[frameId]) {
|
||||||
|
const particleGeometry = new THREE.SphereGeometry(0.03, 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.rendererService.scene.add(particleMesh);
|
||||||
|
this.particles.push(particleMesh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a particle from a specific frame
|
||||||
|
*/
|
||||||
|
removeParticle(frameId: string, index: number): void {
|
||||||
|
if (this.particleData.frames[frameId] && this.particleData.frames[frameId].length > index) {
|
||||||
|
this.particleData.frames[frameId].splice(index, 1);
|
||||||
|
|
||||||
|
// Update visuals if this is the current frame
|
||||||
|
if (frameId === this.currentFrame) {
|
||||||
|
this.clearParticleVisuals();
|
||||||
|
this.renderFrameParticles(frameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected color for new particles
|
||||||
|
*/
|
||||||
|
setSelectedColor(color: string): void {
|
||||||
|
this.selectedColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the selected color
|
||||||
|
*/
|
||||||
|
getSelectedColor(): string {
|
||||||
|
return this.selectedColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the particle data
|
||||||
|
*/
|
||||||
|
getParticleData(): ParticleData {
|
||||||
|
return this.particleData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the particle data
|
||||||
|
*/
|
||||||
|
setParticleData(data: ParticleData): void {
|
||||||
|
this.particleData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current frame
|
||||||
|
*/
|
||||||
|
getCurrentFrame(): string {
|
||||||
|
return this.currentFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current frame
|
||||||
|
*/
|
||||||
|
setCurrentFrame(frameId: string): void {
|
||||||
|
this.currentFrame = frameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all frames
|
||||||
|
*/
|
||||||
|
getFrames(): string[] {
|
||||||
|
return this.frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all frames
|
||||||
|
*/
|
||||||
|
setFrames(frames: string[]): void {
|
||||||
|
this.frames = frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates JSON output of the particle data
|
||||||
|
*/
|
||||||
|
generateJson(): string {
|
||||||
|
return JSON.stringify(this.particleData, null, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import {RendererService} from './renderer.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class PlayerModelService {
|
||||||
|
private playerModel!: THREE.Group;
|
||||||
|
private skinTexture!: THREE.Texture;
|
||||||
|
private textureLoaded = false;
|
||||||
|
|
||||||
|
constructor(private rendererService: RendererService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a Minecraft skin texture from a URL
|
||||||
|
* @param textureUrl The URL of the skin texture to load
|
||||||
|
* @returns A promise that resolves when the texture is loaded
|
||||||
|
*/
|
||||||
|
loadSkinTexture(textureUrl: string): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const loader = new THREE.TextureLoader();
|
||||||
|
loader.load(textureUrl, (texture) => {
|
||||||
|
// Set texture parameters
|
||||||
|
texture.magFilter = THREE.NearestFilter;
|
||||||
|
texture.minFilter = THREE.NearestFilter;
|
||||||
|
this.skinTexture = texture;
|
||||||
|
this.textureLoaded = true;
|
||||||
|
|
||||||
|
// If the player model already exists, rebuild it with textures
|
||||||
|
if (this.playerModel) {
|
||||||
|
// Remove old model
|
||||||
|
this.rendererService.scene.remove(this.playerModel);
|
||||||
|
// Create new model with textures
|
||||||
|
this.createPlayerModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a player model with Minecraft-style textures and adds it to the scene
|
||||||
|
*/
|
||||||
|
createPlayerModel(): THREE.Group {
|
||||||
|
this.playerModel = new THREE.Group();
|
||||||
|
|
||||||
|
if (this.textureLoaded) {
|
||||||
|
// Create textured model if texture is loaded
|
||||||
|
this.createTexturedPlayerModel();
|
||||||
|
} else {
|
||||||
|
// Create simple colored model if no texture is loaded
|
||||||
|
this.createSimplePlayerModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playerModel.renderOrder = 0;
|
||||||
|
this.rendererService.scene.add(this.playerModel);
|
||||||
|
return this.playerModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a simple colored player model (without textures)
|
||||||
|
*/
|
||||||
|
private createSimplePlayerModel(): void {
|
||||||
|
// Head
|
||||||
|
const headGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
|
||||||
|
const headMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
|
||||||
|
const head = new THREE.Mesh(headGeometry, headMaterial);
|
||||||
|
head.position.y = 1.35;
|
||||||
|
this.playerModel.add(head);
|
||||||
|
|
||||||
|
// Body
|
||||||
|
const bodyGeometry = new THREE.BoxGeometry(0.5, 0.7, 0.25);
|
||||||
|
const bodyMaterial = new THREE.MeshLambertMaterial({color: 0x0000ff});
|
||||||
|
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
||||||
|
body.position.y = 0.75;
|
||||||
|
this.playerModel.add(body);
|
||||||
|
|
||||||
|
// Arms
|
||||||
|
const armGeometry = new THREE.BoxGeometry(0.2, 0.7, 0.25);
|
||||||
|
const armMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
|
||||||
|
|
||||||
|
const leftArm = new THREE.Mesh(armGeometry, armMaterial);
|
||||||
|
leftArm.position.set(-0.35, 0.75, 0);
|
||||||
|
this.playerModel.add(leftArm);
|
||||||
|
|
||||||
|
const rightArm = new THREE.Mesh(armGeometry, armMaterial);
|
||||||
|
rightArm.position.set(0.35, 0.75, 0);
|
||||||
|
this.playerModel.add(rightArm);
|
||||||
|
|
||||||
|
// Legs
|
||||||
|
const legGeometry = new THREE.BoxGeometry(0.25, 0.7, 0.25);
|
||||||
|
const legMaterial = new THREE.MeshLambertMaterial({color: 0x000000});
|
||||||
|
|
||||||
|
const leftLeg = new THREE.Mesh(legGeometry, legMaterial);
|
||||||
|
leftLeg.position.set(-0.125, 0.15, 0);
|
||||||
|
this.playerModel.add(leftLeg);
|
||||||
|
|
||||||
|
const rightLeg = new THREE.Mesh(legGeometry, legMaterial);
|
||||||
|
rightLeg.position.set(0.125, 0.15, 0);
|
||||||
|
this.playerModel.add(rightLeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a textured player model using the Minecraft skin
|
||||||
|
*/
|
||||||
|
private createTexturedPlayerModel(): void {
|
||||||
|
// Create the player with properly mapped textures
|
||||||
|
|
||||||
|
// Head - 8x8x8 pixels in the texture
|
||||||
|
this.playerModel.add(this.createBoxWithUvMapping(
|
||||||
|
0.5, 0.5, 0.5, // width, height, depth
|
||||||
|
[
|
||||||
|
{x: 8, y: 0, w: 8, h: 8}, // top
|
||||||
|
{x: 16, y: 0, w: 8, h: 8}, // bottom
|
||||||
|
{x: 16, y: 8, w: 8, h: 8}, // right
|
||||||
|
{x: 0, y: 8, w: 8, h: 8}, // left
|
||||||
|
{x: 8, y: 8, w: 8, h: 8}, // front
|
||||||
|
{x: 24, y: 8, w: 8, h: 8} // back
|
||||||
|
],
|
||||||
|
{x: 0, y: 1.35, z: 0} // position
|
||||||
|
));
|
||||||
|
|
||||||
|
// Body - 8x12x4 pixels in the texture
|
||||||
|
this.playerModel.add(this.createBoxWithUvMapping(
|
||||||
|
0.5, 0.7, 0.25, // width, height, depth
|
||||||
|
[
|
||||||
|
{x: 20, y: 16, w: 8, h: 4}, // top
|
||||||
|
{x: 28, y: 16, w: 8, h: 4}, // bottom
|
||||||
|
{x: 28, y: 20, w: 4, h: 12}, // right
|
||||||
|
{x: 16, y: 20, w: 4, h: 12}, // left
|
||||||
|
{x: 20, y: 20, w: 8, h: 12}, // front
|
||||||
|
{x: 32, y: 20, w: 8, h: 12} // back
|
||||||
|
],
|
||||||
|
{x: 0, y: 0.75, z: 0} // position
|
||||||
|
));
|
||||||
|
|
||||||
|
// Left Arm - 4x12x4 pixels in the texture
|
||||||
|
this.playerModel.add(this.createBoxWithUvMapping(
|
||||||
|
0.2, 0.7, 0.25, // width, height, depth
|
||||||
|
[
|
||||||
|
{x: 44, y: 16, w: 4, h: 4}, // top
|
||||||
|
{x: 48, y: 16, w: 4, h: 4}, // bottom
|
||||||
|
{x: 48, y: 20, w: 4, h: 12}, // right
|
||||||
|
{x: 40, y: 20, w: 4, h: 12}, // left
|
||||||
|
{x: 44, y: 20, w: 4, h: 12}, // front
|
||||||
|
{x: 52, y: 20, w: 4, h: 12} // back
|
||||||
|
],
|
||||||
|
{x: -0.35, y: 0.75, z: 0} // position
|
||||||
|
));
|
||||||
|
|
||||||
|
// Right Arm - 4x12x4 pixels in the texture
|
||||||
|
this.playerModel.add(this.createBoxWithUvMapping(
|
||||||
|
0.2, 0.7, 0.25, // width, height, depth
|
||||||
|
[
|
||||||
|
{x: 44, y: 16, w: 4, h: 4}, // top - mirror of left arm
|
||||||
|
{x: 48, y: 16, w: 4, h: 4}, // bottom - mirror of left arm
|
||||||
|
{x: 40, y: 20, w: 4, h: 12}, // right - mirror of left arm's left
|
||||||
|
{x: 48, y: 20, w: 4, h: 12}, // left - mirror of left arm's right
|
||||||
|
{x: 44, y: 20, w: 4, h: 12}, // front - same as left arm
|
||||||
|
{x: 52, y: 20, w: 4, h: 12} // back - same as left arm
|
||||||
|
],
|
||||||
|
{x: 0.35, y: 0.75, z: 0} // position
|
||||||
|
));
|
||||||
|
|
||||||
|
// Left Leg - 4x12x4 pixels in the texture
|
||||||
|
this.playerModel.add(this.createBoxWithUvMapping(
|
||||||
|
0.26, 0.7, 0.26, // width, height, depth
|
||||||
|
[
|
||||||
|
{x: 4, y: 16, w: 4, h: 4}, // top
|
||||||
|
{x: 8, y: 16, w: 4, h: 4}, // bottom
|
||||||
|
{x: 8, y: 20, w: 4, h: 12}, // right
|
||||||
|
{x: 0, y: 20, w: 4, h: 12}, // left
|
||||||
|
{x: 4, y: 20, w: 4, h: 12}, // front
|
||||||
|
{x: 12, y: 20, w: 4, h: 12} // back
|
||||||
|
],
|
||||||
|
{x: -0.125, y: 0.15, z: 0} // position
|
||||||
|
));
|
||||||
|
|
||||||
|
// Right Leg - 4x12x4 pixels in the texture
|
||||||
|
this.playerModel.add(this.createBoxWithUvMapping(
|
||||||
|
0.26, 0.7, 0.26, // width, height, depth
|
||||||
|
[
|
||||||
|
{x: 4, y: 16, w: 4, h: 4}, // top - mirror of left leg
|
||||||
|
{x: 8, y: 16, w: 4, h: 4}, // bottom - mirror of left leg
|
||||||
|
{x: 0, y: 20, w: 4, h: 12}, // right - mirror of left leg's left
|
||||||
|
{x: 8, y: 20, w: 4, h: 12}, // left - mirror of left leg's right
|
||||||
|
{x: 4, y: 20, w: 4, h: 12}, // front - same as left leg
|
||||||
|
{x: 12, y: 20, w: 4, h: 12} // back - same as left leg
|
||||||
|
],
|
||||||
|
{x: 0.125, y: 0.15, z: 0} // position
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a box with proper UV mapping for a Minecraft character part
|
||||||
|
* @param width Width of the box
|
||||||
|
* @param height Height of the box
|
||||||
|
* @param depth Depth of the box
|
||||||
|
* @param uvMapping Array of UV coordinates for each face (top, bottom, right, left, front, back)
|
||||||
|
* @param position Position of the box
|
||||||
|
* @returns THREE.Mesh with properly mapped textures
|
||||||
|
*/
|
||||||
|
private createBoxWithUvMapping(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
depth: number,
|
||||||
|
uvMapping: Array<{ x: number, y: number, w: number, h: number }>,
|
||||||
|
position: { x: number, y: number, z: number }
|
||||||
|
): THREE.Mesh {
|
||||||
|
const geometry = new THREE.BoxGeometry(width, height, depth);
|
||||||
|
|
||||||
|
// Remap the custom face order to BoxGeometry face order: px, nx, py, ny, pz, nz
|
||||||
|
const faceOrder = [2, 3, 0, 1, 4, 5]; // right, left, top, bottom, front, back
|
||||||
|
const textureWidth = 64;
|
||||||
|
const textureHeight = 64;
|
||||||
|
const uv = geometry.attributes['uv'];
|
||||||
|
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const face = uvMapping[faceOrder[i]];
|
||||||
|
const x1 = face.x / textureWidth;
|
||||||
|
const y1 = 1 - face.y / textureHeight;
|
||||||
|
const x2 = (face.x + face.w) / textureWidth;
|
||||||
|
const y2 = 1 - (face.y + face.h) / textureHeight;
|
||||||
|
|
||||||
|
let uvs: [number, number][] = [
|
||||||
|
[x1, y1], // top-left
|
||||||
|
[x2, y1], // top-right
|
||||||
|
[x1, y2], // bottom-left
|
||||||
|
[x2, y2] // bottom-right
|
||||||
|
];
|
||||||
|
|
||||||
|
const uvOffset = i * 8;
|
||||||
|
for (let j = 0; j < 4; j++) {
|
||||||
|
uv.array[uvOffset + j * 2] = uvs[j][0];
|
||||||
|
uv.array[uvOffset + j * 2 + 1] = uvs[j][1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uv.needsUpdate = true;
|
||||||
|
|
||||||
|
const material = new THREE.MeshBasicMaterial({
|
||||||
|
map: this.skinTexture,
|
||||||
|
transparent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mesh = new THREE.Mesh(geometry, material);
|
||||||
|
mesh.position.set(position.x, position.y, position.z);
|
||||||
|
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import {ElementRef, Injectable} from '@angular/core';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service responsible for managing the Three.js rendering environment
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RendererService {
|
||||||
|
scene!: THREE.Scene;
|
||||||
|
camera!: THREE.PerspectiveCamera;
|
||||||
|
renderer!: THREE.WebGLRenderer;
|
||||||
|
controls!: OrbitControls;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the Three.js scene, camera, renderer, and controls
|
||||||
|
*/
|
||||||
|
initializeRenderer(container: ElementRef): void {
|
||||||
|
// Create scene
|
||||||
|
this.scene = new THREE.Scene();
|
||||||
|
this.scene.background = new THREE.Color(0xf0f0f0);
|
||||||
|
|
||||||
|
// Get container dimensions
|
||||||
|
const containerWidth = container.nativeElement.clientWidth;
|
||||||
|
const containerHeight = container.nativeElement.clientHeight;
|
||||||
|
|
||||||
|
// Create camera
|
||||||
|
this.camera = new THREE.PerspectiveCamera(75, containerWidth / containerHeight, 0.1, 1000);
|
||||||
|
this.camera.position.set(-1, 2, 3);
|
||||||
|
this.camera.lookAt(0, 1, 0);
|
||||||
|
|
||||||
|
// Create renderer
|
||||||
|
this.renderer = new THREE.WebGLRenderer({antialias: true});
|
||||||
|
this.renderer.setSize(containerWidth, containerHeight);
|
||||||
|
|
||||||
|
// Center the canvas in the container
|
||||||
|
this.renderer.domElement.style.display = 'block';
|
||||||
|
this.renderer.domElement.style.margin = 'auto';
|
||||||
|
|
||||||
|
container.nativeElement.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
|
// Initialize orbit controls
|
||||||
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||||
|
this.controls.enableDamping = true;
|
||||||
|
this.controls.dampingFactor = 0.05;
|
||||||
|
this.controls.minDistance = 0.5;
|
||||||
|
this.controls.maxDistance = 4;
|
||||||
|
this.controls.target.set(0, 1, 0);
|
||||||
|
this.controls.update();
|
||||||
|
|
||||||
|
// Add lights
|
||||||
|
this.addLights();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds lighting to the scene
|
||||||
|
*/
|
||||||
|
private addLights(): void {
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
||||||
|
this.scene.add(ambientLight);
|
||||||
|
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
directionalLight.position.set(1, 1, 1);
|
||||||
|
this.scene.add(directionalLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles window resize events
|
||||||
|
*/
|
||||||
|
onWindowResize(container: ElementRef): void {
|
||||||
|
const containerWidth = container.nativeElement.clientWidth;
|
||||||
|
const containerHeight = 400; // Fixed height as defined in CSS
|
||||||
|
|
||||||
|
this.camera.aspect = containerWidth / containerHeight;
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
this.renderer.setSize(containerWidth, containerHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the scene
|
||||||
|
*/
|
||||||
|
render(): void {
|
||||||
|
if (this.controls) {
|
||||||
|
this.controls.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
<li>You can talk about current events and real-world topics. However, if staff recognizes the topic is
|
<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.
|
causing players to become uncomfortable, they may shut the conversation down.
|
||||||
<ul class="full-page-list-2">
|
<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.
|
who want to be involved, or off the server.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -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 <username> -</span>
|
||||||
|
Invite a player to your island
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/island kick <username> -</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 <username> -</span> Set
|
||||||
|
a new owner of your current island
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/island visit <username> -</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 {
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user