Compare commits
42
Commits
a1912b96d8
...
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 | ||
|
|
43b75b8e74 | ||
|
|
07c77f5485 | ||
|
|
d54a7e51ee | ||
|
|
6be6944dea | ||
|
|
3babde5513 |
@@ -27,14 +27,15 @@ dependencies {
|
||||
implementation(project(":open_api"))
|
||||
implementation(project(":database"))
|
||||
implementation(project(":frontend"))
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
annotationProcessor("org.projectlombok:lombok")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
implementation("com.mysql:mysql-connector-j:8.0.32")
|
||||
implementation("org.mybatis:mybatis:3.5.13")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-configuration-processor")
|
||||
implementation("org.springframework.boot:spring-boot-starter-hateoas")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.security:spring-security-oauth2-resource-server")
|
||||
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||
|
||||
//AOP
|
||||
implementation("org.aspectj:aspectjrt:1.9.19")
|
||||
@@ -42,6 +43,8 @@ dependencies {
|
||||
implementation("org.springframework:spring-aop")
|
||||
implementation("org.springframework:spring-aspects")
|
||||
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
}
|
||||
|
||||
tasks.compileJava {
|
||||
|
||||
@@ -10,8 +10,8 @@ import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
public class AltitudeWebApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AltitudeWebApplication.class, args);
|
||||
Connection.initDatabases();
|
||||
SpringApplication.run(AltitudeWebApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.alttd.altitudeweb.config;
|
||||
|
||||
import com.alttd.altitudeweb.controllers.login.KeyPairService;
|
||||
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final KeyPairService keyPairService;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/login/userLogin/**", "/login/requestNewUserLogin/**").permitAll()
|
||||
.requestMatchers("/team/**", "/history/**").permitAll()
|
||||
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
|
||||
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.anyRequest().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");
|
||||
}
|
||||
}
|
||||
+74
-31
@@ -1,13 +1,13 @@
|
||||
package com.alttd.altitudeweb.controllers.history;
|
||||
|
||||
import com.alttd.altitudeweb.api.HistoryApi;
|
||||
import com.alttd.altitudeweb.controllers.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.model.HistoryCountDto;
|
||||
import com.alttd.altitudeweb.model.PunishmentHistoryListDto;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.litebans.*;
|
||||
import com.alttd.altitudeweb.model.PunishmentHistoryDto;
|
||||
import com.alttd.altitudeweb.model.PunishmentHistoryInnerDto;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -19,19 +19,19 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.MINUTES)
|
||||
@RateLimit(limit = 30, timeValue = 10, timeUnit = TimeUnit.SECONDS)
|
||||
public class HistoryApiController implements HistoryApi {
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryDto> getHistoryForAll(String userType, String type, Integer page) {
|
||||
public ResponseEntity<PunishmentHistoryListDto> getHistoryForAll(String userType, String type, Integer page) {
|
||||
return getHistoryForUsers(userType, type, "", page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryDto> getHistoryForUsers(String userType, String type, String user, Integer page) {
|
||||
public ResponseEntity<PunishmentHistoryListDto> getHistoryForUsers(String userType, String type, String user, Integer page) {
|
||||
UserType userTypeEnum = UserType.getUserType(userType);
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto();
|
||||
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS)
|
||||
@@ -46,14 +46,14 @@ public class HistoryApiController implements HistoryApi {
|
||||
historyRecords.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return mapPunishmentHistory(punishmentHistory, historyRecords);
|
||||
return mapPunishmentHistory(punishmentHistoryList, historyRecords);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryDto> getHistoryForUuid(String userType, String type, String uuid, Integer page) {
|
||||
public ResponseEntity<PunishmentHistoryListDto> getHistoryForUuid(String userType, String type, String uuid, Integer page) {
|
||||
UserType userTypeEnum = UserType.getUserType(userType);
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto();
|
||||
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS)
|
||||
@@ -68,7 +68,7 @@ public class HistoryApiController implements HistoryApi {
|
||||
historyRecords.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return mapPunishmentHistory(punishmentHistory, historyRecords);
|
||||
return mapPunishmentHistory(punishmentHistoryList, historyRecords);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,6 +153,44 @@ public class HistoryApiController implements HistoryApi {
|
||||
return ResponseEntity.ok().body(searchResultCountCompletableFuture.join());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryListDto> getAllHistoryForUUID(String uuid) {
|
||||
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||
CompletableFuture<List<HistoryRecord>> historyRecordsCompletableFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||
log.debug("Loading all history for uuid {}", uuid);
|
||||
try {
|
||||
List<HistoryRecord> punishments = sqlSession.getMapper(UUIDHistoryMapper.class)
|
||||
.getAllHistoryForUUID(UUID.fromString(uuid));
|
||||
historyRecordsCompletableFuture.complete(punishments);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load all history for uuid {}", uuid, e);
|
||||
historyRecordsCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return mapPunishmentHistory(punishmentHistoryList, historyRecordsCompletableFuture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryDto> getHistoryById(String type, Integer id) {
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
CompletableFuture<HistoryRecord> historyRecordCompletableFuture = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Loading history by id");
|
||||
try {
|
||||
HistoryRecord punishment = sqlSession.getMapper(IdHistoryMapper.class)
|
||||
.getRecentHistory(historyTypeEnum, id);
|
||||
historyRecordCompletableFuture.complete(punishment);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load history count", e);
|
||||
historyRecordCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return ResponseEntity.ok().body(mapPunishmentHistory(historyRecordCompletableFuture.join()));
|
||||
}
|
||||
|
||||
private ResponseEntity<HistoryCountDto> mapHistoryCount(HistoryCount historyCount) {
|
||||
HistoryCountDto historyCountDto = new HistoryCountDto();
|
||||
historyCountDto.setBans(historyCount.getBans());
|
||||
@@ -162,28 +200,33 @@ public class HistoryApiController implements HistoryApi {
|
||||
return ResponseEntity.ok().body(historyCountDto);
|
||||
}
|
||||
|
||||
private ResponseEntity<PunishmentHistoryDto> mapPunishmentHistory(PunishmentHistoryDto punishmentHistory, CompletableFuture<List<HistoryRecord>> historyRecords) {
|
||||
private ResponseEntity<PunishmentHistoryListDto> mapPunishmentHistory(PunishmentHistoryListDto punishmentHistoryList, CompletableFuture<List<HistoryRecord>> historyRecords) {
|
||||
historyRecords.join().forEach(historyRecord -> {
|
||||
PunishmentHistoryInnerDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) {
|
||||
case "ban" -> PunishmentHistoryInnerDto.TypeEnum.BAN;
|
||||
case "mute" -> PunishmentHistoryInnerDto.TypeEnum.MUTE;
|
||||
case "warn" -> PunishmentHistoryInnerDto.TypeEnum.WARN;
|
||||
case "kick" -> PunishmentHistoryInnerDto.TypeEnum.KICK;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + historyRecord.getType());
|
||||
};
|
||||
PunishmentHistoryInnerDto innerDto = new PunishmentHistoryInnerDto()
|
||||
.uuid(historyRecord.getUuid())
|
||||
.username(historyRecord.getPunishedName())
|
||||
.reason(historyRecord.getReason())
|
||||
.punishedByUuid(historyRecord.getBannedByUuid())
|
||||
.punishedBy(historyRecord.getBannedByName())
|
||||
.removedBy(historyRecord.getRemovedByName())
|
||||
.punishmentTime(historyRecord.getTime())
|
||||
.expiryTime(historyRecord.getUntil())
|
||||
.removedReason(historyRecord.getRemovedByReason())
|
||||
.type(type);
|
||||
punishmentHistory.add(innerDto);
|
||||
PunishmentHistoryDto innerDto = mapPunishmentHistory(historyRecord);
|
||||
punishmentHistoryList.add(innerDto);
|
||||
});
|
||||
return ResponseEntity.ok().body(punishmentHistory);
|
||||
return ResponseEntity.ok().body(punishmentHistoryList);
|
||||
}
|
||||
|
||||
private static PunishmentHistoryDto mapPunishmentHistory(HistoryRecord historyRecord) {
|
||||
PunishmentHistoryDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) {
|
||||
case "ban" -> PunishmentHistoryDto.TypeEnum.BAN;
|
||||
case "mute" -> PunishmentHistoryDto.TypeEnum.MUTE;
|
||||
case "warn" -> PunishmentHistoryDto.TypeEnum.WARN;
|
||||
case "kick" -> PunishmentHistoryDto.TypeEnum.KICK;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + historyRecord.getType());
|
||||
};
|
||||
return new PunishmentHistoryDto()
|
||||
.uuid(historyRecord.getUuid())
|
||||
.username(historyRecord.getPunishedName())
|
||||
.reason(historyRecord.getReason())
|
||||
.punishedByUuid(historyRecord.getBannedByUuid())
|
||||
.punishedBy(historyRecord.getBannedByName())
|
||||
.removedBy(historyRecord.getRemovedByName())
|
||||
.punishmentTime(historyRecord.getTime())
|
||||
.expiryTime(historyRecord.getUntil())
|
||||
.removedReason(historyRecord.getRemovedByReason())
|
||||
.type(type)
|
||||
.id(historyRecord.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.alttd.altitudeweb.controllers.login;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.KeyPairEntity;
|
||||
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.*;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class KeyPairService {
|
||||
|
||||
private KeyPair cachedKeyPair = null;
|
||||
private static final String RSA_ALGORITHM = "RSA";
|
||||
private static final int RSA_KEY_SIZE = 2048;
|
||||
|
||||
public KeyPair getJwtSigningKeyPair() {
|
||||
if (cachedKeyPair != null) {
|
||||
return cachedKeyPair;
|
||||
}
|
||||
KeyPair keyPair = getOrCreateKeyPair();
|
||||
if (keyPair != null) {
|
||||
cachedKeyPair = keyPair;
|
||||
return cachedKeyPair;
|
||||
}
|
||||
throw new IllegalStateException("Failed to generate or load key pair");
|
||||
}
|
||||
|
||||
public KeyPair getOrCreateKeyPair() {
|
||||
CompletableFuture<KeyPairEntity> keyPairFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Loading key pair");
|
||||
try {
|
||||
KeyPairEntity entity = sqlSession.getMapper(KeyPairMapper.class).getKeyPair();
|
||||
keyPairFuture.complete(entity);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to key pair", e);
|
||||
keyPairFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
KeyPairEntity keyPairEntity = keyPairFuture.join();
|
||||
if (keyPairEntity != null) {
|
||||
try {
|
||||
byte[] privateKeyBytes = Base64.getDecoder().decode(keyPairEntity.getPrivateKey());
|
||||
byte[] publicKeyBytes = Base64.getDecoder().decode(keyPairEntity.getPublicKey());
|
||||
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
|
||||
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
|
||||
|
||||
return new KeyPair(publicKey, privateKey);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load key pair from database", e);
|
||||
}
|
||||
}
|
||||
|
||||
KeyPair keyPair = generateKeyPair();
|
||||
|
||||
try {
|
||||
KeyPairEntity entity = new KeyPairEntity();
|
||||
entity.setPrivateKey(Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
|
||||
entity.setPublicKey(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
|
||||
entity.setCreatedAt(Instant.now());
|
||||
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Saving key pair");
|
||||
try {
|
||||
sqlSession.getMapper(KeyPairMapper.class).save(entity);
|
||||
log.info("Generated and saved new key pair");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to key pair", e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to save key pair to database", e);
|
||||
}
|
||||
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
private KeyPair generateKeyPair() {
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
|
||||
keyPairGenerator.initialize(RSA_KEY_SIZE);
|
||||
return keyPairGenerator.generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Error generating key pair", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.alttd.altitudeweb.controllers.login;
|
||||
|
||||
import com.alttd.altitudeweb.api.LoginApi;
|
||||
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.PrivilegedUser;
|
||||
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
public class LoginController implements LoginApi {
|
||||
|
||||
private final JwtEncoder jwtEncoder;
|
||||
|
||||
@Value("${login.secret:#{null}}")
|
||||
private String loginSecret;
|
||||
|
||||
private record CacheEntry(UUID uuid, Instant expiry) {}
|
||||
|
||||
private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
|
||||
|
||||
@Scheduled(fixedRate = 300000) // 5 minutes in milliseconds
|
||||
private void clearExpiredCacheEntries() {
|
||||
Instant now = Instant.now();
|
||||
int initialCacheSize = cache.size();
|
||||
cache.entrySet().removeIf(entry -> entry.getValue().expiry().isBefore(now));
|
||||
log.info("Cleared {} expired cache entries", initialCacheSize - cache.size());
|
||||
}
|
||||
|
||||
@RateLimit(limit = 100, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "addLogin")
|
||||
@Override
|
||||
public ResponseEntity<String> requestLogin(String authorization, String uuid) {
|
||||
UUID uuidFromString;
|
||||
try {
|
||||
uuidFromString = UUID.fromString(uuid);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
if (authorization == null || !authorization.startsWith("SECRET ")) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
String secret = authorization.substring("SECRET ".length());
|
||||
if (!isValidSecret(secret)) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid secret");
|
||||
}
|
||||
|
||||
Optional<String> key = cache.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().uuid.equals(uuidFromString))
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst();
|
||||
|
||||
if (key.isPresent()) {
|
||||
return ResponseEntity.ok(key.get());
|
||||
}
|
||||
|
||||
String loginCode = generateLoginCode(uuidFromString);
|
||||
return ResponseEntity.ok(loginCode);
|
||||
}
|
||||
|
||||
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
|
||||
@Override
|
||||
public ResponseEntity<String> login(String code) {
|
||||
CacheEntry cacheEntry1 = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
|
||||
cache.put("23232323", cacheEntry1);
|
||||
if (code == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
CacheEntry cacheEntry = cache.get(code);
|
||||
if (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now())) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
String token = generateToken(cacheEntry.uuid);
|
||||
|
||||
cache.remove(code);
|
||||
|
||||
return ResponseEntity.ok(token);
|
||||
}
|
||||
|
||||
private String generateLoginCode(UUID uuid) {
|
||||
String characters = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
StringBuilder loginCode = new StringBuilder();
|
||||
for (int i = 0; i < 8; i++) {
|
||||
int index = (int) (Math.random() * characters.length());
|
||||
loginCode.append(characters.charAt(index));
|
||||
}
|
||||
CacheEntry cacheEntry = new CacheEntry(uuid,
|
||||
Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(15)));
|
||||
cache.put(loginCode.toString(), cacheEntry);
|
||||
return loginCode.toString();
|
||||
}
|
||||
|
||||
private boolean isValidSecret(String secret) {
|
||||
if (loginSecret == null) {
|
||||
log.warn("No login secret set, skipping secret validation");
|
||||
return false;
|
||||
}
|
||||
if (loginSecret.length() < 16) {
|
||||
log.warn("Login secret is too short, skipping secret validation");
|
||||
return false;
|
||||
}
|
||||
if (!loginSecret.equals(secret)) {
|
||||
log.info("Received invalid secret attempt");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String generateToken(UUID uuid) {
|
||||
Instant now = Instant.now();
|
||||
//TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour)
|
||||
Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30));
|
||||
CompletableFuture<PrivilegedUser> privilegedUserCompletableFuture = new CompletableFuture<>();
|
||||
List<PermissionClaimDto> claimList = new ArrayList<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
try {
|
||||
PrivilegedUser privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class)
|
||||
.getUserByUuid(uuid.toString());
|
||||
|
||||
privilegedUserCompletableFuture.complete(privilegedUser);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load user by uuid", e);
|
||||
privilegedUserCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
PrivilegedUser privilegedUser = privilegedUserCompletableFuture.join();
|
||||
claimList.add(PermissionClaimDto.USER);
|
||||
if (privilegedUser != null) {
|
||||
privilegedUser.getPermissions().forEach(permission -> {
|
||||
try {
|
||||
claimList.add(PermissionClaimDto.valueOf(permission));
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Received invalid permission claim: {}", permission);
|
||||
}
|
||||
});
|
||||
}
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuer("altitudeweb")
|
||||
.claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList())
|
||||
.issuedAt(now)
|
||||
.expiresAt(expiryTime)
|
||||
.subject(uuid.toString())
|
||||
.build();
|
||||
|
||||
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.alttd.altitudeweb.controllers.team;
|
||||
|
||||
import com.alttd.altitudeweb.api.TeamApi;
|
||||
import com.alttd.altitudeweb.controllers.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.luckperms.Player;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.alttd.altitudeweb.controllers.limits;
|
||||
package com.alttd.altitudeweb.services.limits;
|
||||
|
||||
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.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.HttpServletResponse;
|
||||
@@ -26,8 +26,8 @@ public class RateLimitAspect {
|
||||
private final InMemoryRateLimiterService rateLimiterService;
|
||||
|
||||
@Around("""
|
||||
@annotation(com.alttd.altitudeweb.controllers.limits.RateLimit)
|
||||
|| @within(com.alttd.altitudeweb.controllers.limits.RateLimit)""")
|
||||
@annotation(com.alttd.altitudeweb.services.limits.RateLimit)
|
||||
|| @within(com.alttd.altitudeweb.services.limits.RateLimit)""")
|
||||
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes == null) {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.alttd.altitudeweb.controllers.limits;
|
||||
package com.alttd.altitudeweb.services.limits;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alttd.altitudeweb.services.user;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String uuid) throws UsernameNotFoundException {
|
||||
try {
|
||||
//Validate uuid
|
||||
UUID.fromString(uuid);
|
||||
return new User(uuid, "", Collections.emptyList());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new UsernameNotFoundException("Invalid UUID format: " + uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,5 @@ database.host=${DB_HOST:localhost}
|
||||
database.user=${DB_USER:root}
|
||||
database.password=${DB_PASSWORD:root}
|
||||
cors.allowed-origins=${CORS:https://alttd.com}
|
||||
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
||||
logging.level.com.alttd.altitudeweb=INFO
|
||||
|
||||
@@ -16,6 +16,7 @@ dependencies {
|
||||
implementation("org.mybatis:mybatis:3.5.13")
|
||||
compileOnly("org.slf4j:slf4j-api:2.0.17")
|
||||
compileOnly("org.slf4j:slf4j-simple:2.0.17")
|
||||
compileOnly("org.jetbrains:annotations:26.0.2")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
||||
@Data
|
||||
public class HistoryRecord {
|
||||
private String uuid;
|
||||
private int id;
|
||||
private String punishedName;
|
||||
private String reason;
|
||||
private String bannedByUuid;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.alttd.altitudeweb.database.litebans;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Result;
|
||||
import org.apache.ibatis.annotations.Results;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
public interface IdHistoryMapper {
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
|
||||
@Result(property = "bannedByName", column = "banned_by_name"),
|
||||
@Result(property = "removedByName", column = "removed_by_name"),
|
||||
@Result(property = "time", column = "time"),
|
||||
@Result(property = "until", column = "until"),
|
||||
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||
})
|
||||
@Select("""
|
||||
SELECT punishment.id,
|
||||
punishment.uuid,
|
||||
user_lookup.name AS punished_name,
|
||||
punishment.reason,
|
||||
punishment.banned_by_uuid,
|
||||
punishment.banned_by_name,
|
||||
punishment.removed_by_name,
|
||||
punishment.time,
|
||||
punishment.until,
|
||||
punishment.removed_by_reason
|
||||
FROM (SELECT *
|
||||
FROM ${table_name} AS punishment
|
||||
WHERE punishment.id = #{id})
|
||||
AS punishment
|
||||
INNER JOIN user_lookup
|
||||
ON user_lookup.uuid = punishment.uuid
|
||||
""")
|
||||
HistoryRecord getHistoryForId(@Param("table_name") String tableName,
|
||||
@Param("type") String type,
|
||||
@Param("id") int id);
|
||||
|
||||
default HistoryRecord getRecentHistory(HistoryType historyType, int id) {
|
||||
HistoryRecord historyRecord = switch (historyType) {
|
||||
case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported");
|
||||
case BAN -> getHistoryForId("litebans_bans", "ban", id);
|
||||
case MUTE -> getHistoryForId("litebans_mutes", "mute", id);
|
||||
case KICK -> getHistoryForId("litebans_kicks", "kick", id);
|
||||
case WARN -> getHistoryForId("litebans_warnings", "warn", id);
|
||||
};
|
||||
historyRecord.setType(historyType.name().toLowerCase());
|
||||
return historyRecord;
|
||||
}
|
||||
}
|
||||
+31
-23
@@ -4,6 +4,7 @@ import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Result;
|
||||
import org.apache.ibatis.annotations.Results;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -24,6 +25,7 @@ public interface NameHistoryMapper {
|
||||
* order
|
||||
*/
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@@ -36,7 +38,8 @@ public interface NameHistoryMapper {
|
||||
@Result(property = "type", column = "type")
|
||||
})
|
||||
@Select("""
|
||||
SELECT all_punishments.uuid,
|
||||
SELECT id,
|
||||
all_punishments.uuid,
|
||||
user_lookup.name AS punished_name,
|
||||
reason,
|
||||
banned_by_uuid,
|
||||
@@ -62,7 +65,6 @@ public interface NameHistoryMapper {
|
||||
* Retrieves a list of all types of recent punishment history records sorted
|
||||
* in descending time order. This result does NOT contain kicks history
|
||||
*
|
||||
* @param nameColumn the column name in the database indicating the name to use for filtering
|
||||
* @param limit the maximum number of records to fetch
|
||||
* @param offset the starting offset position of the result set
|
||||
*
|
||||
@@ -70,6 +72,7 @@ public interface NameHistoryMapper {
|
||||
* order
|
||||
*/
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@@ -82,17 +85,19 @@ public interface NameHistoryMapper {
|
||||
@Result(property = "type", column = "type")
|
||||
})
|
||||
@Select("""
|
||||
SELECT punishments.uuid,
|
||||
SELECT punishment.id,
|
||||
punishment.uuid,
|
||||
user_lookup.name AS punished_name,
|
||||
punishments.reason,
|
||||
punishments.banned_by_uuid,
|
||||
punishments.banned_by_name,
|
||||
punishments.removed_by_name,
|
||||
punishments.time,
|
||||
punishments.until,
|
||||
punishments.removed_by_reason,
|
||||
punishments.type
|
||||
punishment.reason,
|
||||
punishment.banned_by_uuid,
|
||||
punishment.banned_by_name,
|
||||
punishment.removed_by_name,
|
||||
punishment.time,
|
||||
punishment.until,
|
||||
punishment.removed_by_reason,
|
||||
punishment.type
|
||||
FROM (SELECT uuid,
|
||||
id,
|
||||
reason,
|
||||
banned_by_uuid,
|
||||
banned_by_name,
|
||||
@@ -103,9 +108,9 @@ public interface NameHistoryMapper {
|
||||
type
|
||||
FROM all_punishments
|
||||
ORDER BY time DESC
|
||||
LIMIT #{limit} OFFSET #{offset}) AS punishments
|
||||
LIMIT #{limit} OFFSET #{offset}) AS punishment
|
||||
INNER JOIN user_lookup
|
||||
ON user_lookup.uuid = punishments.uuid;
|
||||
ON user_lookup.uuid = punishment.uuid;
|
||||
""")
|
||||
List<HistoryRecord> getRecentAllHistory(@Param("limit") int limit,
|
||||
@Param("offset") int offset);
|
||||
@@ -124,6 +129,7 @@ public interface NameHistoryMapper {
|
||||
* order
|
||||
*/
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@@ -135,7 +141,8 @@ public interface NameHistoryMapper {
|
||||
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||
})
|
||||
@Select("""
|
||||
SELECT punishment.uuid,
|
||||
SELECT punishment.id,
|
||||
punishment.uuid,
|
||||
user_lookup.name AS punished_name,
|
||||
punishment.reason,
|
||||
punishment.banned_by_uuid,
|
||||
@@ -162,8 +169,6 @@ public interface NameHistoryMapper {
|
||||
* parameters. The records are sorted in descending order by time and limited to a specified range.
|
||||
*
|
||||
* @param tableName the name of the database table to query the history records from
|
||||
* @param partialName a partial or complete name of the user to filter the records, case-insensitive
|
||||
* @param nameColumn the column name in the database indicating the name to use for filtering
|
||||
* @param limit the maximum number of records to fetch
|
||||
* @param offset the starting offset position of the result set
|
||||
*
|
||||
@@ -171,6 +176,7 @@ public interface NameHistoryMapper {
|
||||
* order
|
||||
*/
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@@ -182,7 +188,8 @@ public interface NameHistoryMapper {
|
||||
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||
})
|
||||
@Select("""
|
||||
SELECT punishment.uuid,
|
||||
SELECT punishment.id,
|
||||
punishment.uuid,
|
||||
user_lookup.name AS punished_name,
|
||||
punishment.reason,
|
||||
punishment.banned_by_uuid,
|
||||
@@ -192,7 +199,8 @@ public interface NameHistoryMapper {
|
||||
punishment.until,
|
||||
punishment.removed_by_reason
|
||||
FROM (
|
||||
SELECT uuid,
|
||||
SELECT id,
|
||||
uuid,
|
||||
reason,
|
||||
banned_by_uuid,
|
||||
banned_by_name,
|
||||
@@ -254,23 +262,23 @@ public interface NameHistoryMapper {
|
||||
}
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentBans(UserType userType, String partialName, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentBans(UserType userType, String partialName, int page) {
|
||||
return addType(getRecent("litebans_bans", userType, partialName, page), "ban");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentKicks(UserType userType, String partialName, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentKicks(UserType userType, String partialName, int page) {
|
||||
return addType(getRecent("litebans_kicks", userType, partialName, page), "kick");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentMutes(UserType userType, String partialName, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentMutes(UserType userType, String partialName, int page) {
|
||||
return addType(getRecent("litebans_mutes", userType, partialName, page), "mute");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentWarns(UserType userType, String partialName, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentWarns(UserType userType, String partialName, int page) {
|
||||
return addType(getRecent("litebans_warnings", userType, partialName, page), "warn");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) {
|
||||
private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, String type) {
|
||||
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
|
||||
return historyRecords;
|
||||
}
|
||||
|
||||
+25
-15
@@ -1,10 +1,11 @@
|
||||
package com.alttd.altitudeweb.database.litebans;
|
||||
|
||||
import com.alttd.altitudeweb.type_handler.UUIDTypeHandler;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Result;
|
||||
import org.apache.ibatis.annotations.Results;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -14,10 +15,11 @@ public interface UUIDHistoryMapper {
|
||||
int PAGE_SIZE = 10;
|
||||
|
||||
@Results({
|
||||
@Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
||||
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
|
||||
@Result(property = "bannedByName", column = "banned_by_name"),
|
||||
@Result(property = "removedByName", column = "removed_by_name"),
|
||||
@Result(property = "time", column = "time"),
|
||||
@@ -26,7 +28,7 @@ public interface UUIDHistoryMapper {
|
||||
@Result(property = "type", column = "type")
|
||||
})
|
||||
@Select("""
|
||||
SELECT all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
||||
SELECT id, all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
||||
removed_by_name, time, until, removed_by_reason, type
|
||||
FROM all_punishments
|
||||
INNER JOIN user_lookup
|
||||
@@ -41,10 +43,11 @@ public interface UUIDHistoryMapper {
|
||||
@Param("offset") int offset);
|
||||
|
||||
@Results({
|
||||
@Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
||||
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
|
||||
@Result(property = "bannedByName", column = "banned_by_name"),
|
||||
@Result(property = "removedByName", column = "removed_by_name"),
|
||||
@Result(property = "time", column = "time"),
|
||||
@@ -52,7 +55,7 @@ public interface UUIDHistoryMapper {
|
||||
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||
})
|
||||
@Select("""
|
||||
SELECT punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
||||
SELECT punishment.id, punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
||||
removed_by_name, time, until, removed_by_reason
|
||||
FROM ${tableName} AS punishment
|
||||
INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid
|
||||
@@ -66,7 +69,8 @@ public interface UUIDHistoryMapper {
|
||||
@Param("limit") int limit,
|
||||
@Param("offset") int offset);
|
||||
|
||||
default List<HistoryRecord> getRecent(HistoryType historyType, UserType userType, UUID uuid, int page) {
|
||||
default List<HistoryRecord> getRecent(@NotNull HistoryType historyType, @NotNull UserType userType,
|
||||
@NotNull UUID uuid, int page) {
|
||||
return switch (historyType) {
|
||||
case ALL -> getRecentAll(userType, uuid, page);
|
||||
case BAN -> getRecentBans(userType, uuid, page);
|
||||
@@ -76,7 +80,12 @@ public interface UUIDHistoryMapper {
|
||||
};
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecent(String tableName, UserType userType, UUID uuid, int page) {
|
||||
default List<HistoryRecord> getAllHistoryForUUID(@NotNull UUID uuid) {
|
||||
return getRecentAllHistory(uuid.toString(), "uuid", 100, 0);
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecent(@NotNull String tableName, @NotNull UserType userType,
|
||||
@NotNull UUID uuid, int page) {
|
||||
int offset = page * PAGE_SIZE;
|
||||
int limit = PAGE_SIZE;
|
||||
return switch (userType) {
|
||||
@@ -85,7 +94,7 @@ public interface UUIDHistoryMapper {
|
||||
};
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentAll(UserType userType, UUID uuid, int page) {
|
||||
private List<HistoryRecord> getRecentAll(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||
int offset = page * PAGE_SIZE;
|
||||
int limit = PAGE_SIZE;
|
||||
return switch (userType) {
|
||||
@@ -96,23 +105,24 @@ public interface UUIDHistoryMapper {
|
||||
};
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentBans(UserType userType, UUID uuid, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentBans(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||
return addType(getRecent("litebans_bans", userType, uuid, page), "ban");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentKicks(UserType userType, UUID uuid, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentKicks(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||
return addType(getRecent("litebans_kicks", userType, uuid, page), "kick");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentMutes(UserType userType, UUID uuid, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentMutes(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||
return addType(getRecent("litebans_mutes", userType, uuid, page), "mute");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentWarns(UserType userType, UUID uuid, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentWarns(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||
return addType(getRecent("litebans_warnings", userType, uuid, page), "warn");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) {
|
||||
@Contract("_, _ -> param1")
|
||||
private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, @NotNull String type) {
|
||||
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
|
||||
return historyRecords;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ public interface TeamMemberMapper {
|
||||
FROM luckperms_user_permissions AS permissions
|
||||
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
||||
WHERE permission = #{groupPermission}
|
||||
AND world = 'global'
|
||||
""")
|
||||
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.alttd.altitudeweb.database.web_db;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class KeyPairEntity {
|
||||
private int id;
|
||||
private String privateKey;
|
||||
private String publicKey;
|
||||
private Instant createdAt;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.alttd.altitudeweb.database.web_db;
|
||||
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
public interface KeyPairMapper {
|
||||
|
||||
@Select("SELECT * FROM key_pair ORDER BY id DESC LIMIT 1")
|
||||
KeyPairEntity getKeyPair();
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO key_pair (id, private_key, public_key, created_at)
|
||||
VALUES (#{id}, #{privateKey}, #{publicKey}, #{createdAt})
|
||||
""")
|
||||
void save(KeyPairEntity keyPair);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.alttd.altitudeweb.database.web_db;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PrivilegedUser {
|
||||
private int id;
|
||||
private String uuid;
|
||||
private List<String> permissions;
|
||||
}
|
||||
+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);
|
||||
Connection connection = new Connection(databaseSettings, addMappers);
|
||||
log.debug("Created default database connection {}", connection);
|
||||
connections.put(Databases.DEFAULT, connection);
|
||||
return CompletableFuture.completedFuture(connection);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ public class InitializeLiteBans {
|
||||
configuration.addMapper(NameHistoryMapper.class);
|
||||
configuration.addMapper(UUIDHistoryMapper.class);
|
||||
configuration.addMapper(HistoryCountMapper.class);
|
||||
configuration.addMapper(IdHistoryMapper.class);
|
||||
}).join()
|
||||
.runQuery(sqlSession -> {
|
||||
createAllPunishmentsView(sqlSession);
|
||||
@@ -29,15 +30,15 @@ public class InitializeLiteBans {
|
||||
private static void createAllPunishmentsView(SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE VIEW IF NOT EXISTS all_punishments AS
|
||||
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
'ban' as type
|
||||
FROM litebans_bans
|
||||
UNION ALL
|
||||
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
'mute' as type
|
||||
FROM litebans_mutes
|
||||
UNION ALL
|
||||
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
'warn' as type
|
||||
FROM litebans_warnings
|
||||
ORDER BY time DESC;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.alttd.altitudeweb.setup;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
@@ -12,15 +14,21 @@ import java.sql.Statement;
|
||||
public class InitializeWebDb {
|
||||
|
||||
protected static void init() {
|
||||
log.info("Initializing LiteBans");
|
||||
log.info("Initializing WebDb");
|
||||
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
||||
configuration.addMapper(SettingsMapper.class);
|
||||
configuration.addMapper(KeyPairMapper.class);
|
||||
}).join()
|
||||
.runQuery(InitializeWebDb::createSettingsTable);
|
||||
log.debug("Initialized LuckPerms");
|
||||
.runQuery(SqlSession -> {
|
||||
createSettingsTable(SqlSession);
|
||||
createKeyTable(SqlSession);
|
||||
createPrivilegedUsersTable(SqlSession);
|
||||
createPrivilegesTable(SqlSession);
|
||||
});
|
||||
log.debug("Initialized WebDb");
|
||||
}
|
||||
|
||||
private static void createSettingsTable(SqlSession sqlSession) {
|
||||
private static void createSettingsTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS db_connection_settings
|
||||
(
|
||||
@@ -40,4 +48,53 @@ public class InitializeWebDb {
|
||||
}
|
||||
}
|
||||
|
||||
private static void createKeyTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS key_pair (
|
||||
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
private_key TEXT NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL
|
||||
);
|
||||
""";
|
||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||
statement.execute(query);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createPrivilegedUsersTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS privileged_users (
|
||||
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(36) NOT NULL
|
||||
);
|
||||
""";
|
||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||
statement.execute(query);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createPrivilegesTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS privileges (
|
||||
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id int NOT NULL,
|
||||
privileges VARCHAR(36) NOT NULL,
|
||||
CONSTRAINT fk_privileges_user FOREIGN KEY (user_id)
|
||||
REFERENCES privileged_users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
""";
|
||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||
statement.execute(query);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,15 +13,19 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^19.2.18",
|
||||
"@angular/common": "^19.2.0",
|
||||
"@angular/compiler": "^19.2.0",
|
||||
"@angular/core": "^19.2.0",
|
||||
"@angular/forms": "^19.2.0",
|
||||
"@angular/material": "^19.2.18",
|
||||
"@angular/platform-browser": "^19.2.0",
|
||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||
"@angular/router": "^19.2.0",
|
||||
"@types/three": "^0.177.0",
|
||||
"ngx-cookie-service": "^19.1.2",
|
||||
"rxjs": "~7.8.0",
|
||||
"three": "^0.177.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -5,6 +5,10 @@ export const routes: Routes = [
|
||||
path: '',
|
||||
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
|
||||
},
|
||||
{
|
||||
path: 'particles',
|
||||
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
||||
},
|
||||
{
|
||||
path: 'map',
|
||||
loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
|
||||
@@ -45,6 +49,10 @@ export const routes: Routes = [
|
||||
path: 'bans',
|
||||
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent)
|
||||
},
|
||||
{
|
||||
path: 'bans/:type/:id',
|
||||
loadComponent: () => import('./bans/details/details.component').then(m => m.DetailsComponent)
|
||||
},
|
||||
{
|
||||
path: 'economy',
|
||||
loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent)
|
||||
@@ -52,7 +60,57 @@ export const routes: Routes = [
|
||||
{
|
||||
path: 'claiming',
|
||||
loadComponent: () => import('./claiming/claiming.component').then(m => m.ClaimingComponent)
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'mypet',
|
||||
loadComponent: () => import('./mypet/mypet.component').then(m => m.MypetComponent)
|
||||
},
|
||||
{
|
||||
path: 'warps',
|
||||
loadComponent: () => import('./warps/warps.component').then(m => m.WarpsComponent)
|
||||
},
|
||||
{
|
||||
path: 'skyblock',
|
||||
loadComponent: () => import('./skyblock/skyblock.component').then(m => m.SkyblockComponent)
|
||||
},
|
||||
{
|
||||
path: 'customfeatures',
|
||||
loadComponent: () => import('./customfeatures/customfeatures.component').then(m => m.CustomfeaturesComponent)
|
||||
},
|
||||
{
|
||||
path: 'guide',
|
||||
loadComponent: () => import('./guide/guide.component').then(m => m.GuideComponent)
|
||||
},
|
||||
{
|
||||
path: 'ranks',
|
||||
loadComponent: () => import('./ranks/ranks.component').then(m => m.RanksComponent)
|
||||
},
|
||||
{
|
||||
path: 'commandlist',
|
||||
loadComponent: () => import('./commandlist/commandlist.component').then(m => m.CommandlistComponent)
|
||||
},
|
||||
{
|
||||
path: 'mapart',
|
||||
loadComponent: () => import('./mapart/mapart.component').then(m => m.MapartComponent)
|
||||
},
|
||||
{
|
||||
path: 'lag',
|
||||
loadComponent: () => import('./lag/lag.component').then(m => m.LagComponent)
|
||||
},
|
||||
{
|
||||
path: 'staffpowers',
|
||||
loadComponent: () => import('./staffpowers/staffpowers.component').then(m => m.StaffpowersComponent)
|
||||
},
|
||||
{
|
||||
path: 'forms/:form',
|
||||
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
||||
},
|
||||
{
|
||||
path: 'forms',
|
||||
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
||||
},
|
||||
{
|
||||
path: 'particles',
|
||||
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
</div>
|
||||
<div class="historyTable">
|
||||
<app-history [userType]="userType" [punishmentType]="punishmentType"
|
||||
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)">
|
||||
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)"
|
||||
(selectItem)="setSearch($event)">
|
||||
</app-history>
|
||||
</div>
|
||||
<div class="changePageButtons">
|
||||
|
||||
@@ -5,6 +5,7 @@ import {HistoryCount, HistoryService} from '../../api';
|
||||
import {NgClass, NgForOf, NgIf} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {catchError, map, Observable} from 'rxjs';
|
||||
import {SearchParams} from './search-terms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bans',
|
||||
@@ -210,4 +211,11 @@ export class BansComponent implements OnInit {
|
||||
this.actualPage = this.page;
|
||||
}
|
||||
}
|
||||
|
||||
public setSearch(searchParams: SearchParams) {
|
||||
this.userType = searchParams.userType
|
||||
this.searchTerm = searchParams.searchTerm
|
||||
this.punishmentType = searchParams.punishmentType
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">>
|
||||
<div class="title" header-content>
|
||||
<h1>Minecraft Punishments</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="columnSection">
|
||||
<div class="detailsBackButton">
|
||||
<ng-container *ngIf="punishment === undefined">
|
||||
<p>Loading...</p>
|
||||
</ng-container>
|
||||
|
||||
<a [routerLink]="['/bans']">< Back</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="columnSection center">
|
||||
<ng-container *ngIf="punishment">
|
||||
<div>
|
||||
<span class="tag tagInfo"
|
||||
[ngClass]="{
|
||||
'tagPermanent': this.historyFormat.isPermanent(punishment),
|
||||
'tagExpired': !this.historyFormat.isPermanent(punishment)
|
||||
}">
|
||||
{{ this.historyFormat.getType(punishment) }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="tag tagInfo"
|
||||
[ngClass]="{
|
||||
'tagActive': this.historyFormat.isActive(punishment),
|
||||
'tagInactive': !this.historyFormat.isActive(punishment)
|
||||
}">
|
||||
{{ this.historyFormat.isActive(punishment) ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</section>
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<ng-container *ngIf="punishment">
|
||||
<div class="playerContainer">
|
||||
<h2>Player</h2>
|
||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid, '150')"
|
||||
width="150"
|
||||
height="150"
|
||||
alt="{{punishment.username}}'s Minecraft skin"
|
||||
>
|
||||
<h3 class="detailsUsername">{{ punishment.username }}</h3>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<ng-container *ngIf="punishment">
|
||||
<div class="playerContainer">
|
||||
<h2>Moderator</h2>
|
||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid, '150')"
|
||||
width="150"
|
||||
height="150"
|
||||
alt="{{punishment.punishedBy}}'s Minecraft skin"
|
||||
>
|
||||
<h3 class="detailsUsername">{{ punishment.punishedBy }}</h3>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<ng-container *ngIf="punishment">
|
||||
<div class="detailsInfo">
|
||||
<h2>Reason</h2>
|
||||
<p>{{ punishment.reason | removeTrailingPeriod }}</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<ng-container *ngIf="punishment">
|
||||
<div class="detailsInfo">
|
||||
<h2>Date</h2>
|
||||
<p>{{ this.historyFormat.getPunishmentTime(punishment) }}</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<section class="columnSection">
|
||||
<ng-container *ngIf="punishment">
|
||||
<span>Expires</span>
|
||||
<span>{{ this.historyFormat.getExpiredTime(punishment) }}</span>
|
||||
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
|
||||
<span>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</span>
|
||||
<span>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</section>
|
||||
@@ -0,0 +1,68 @@
|
||||
.detailsBackButton {
|
||||
font-family: open-sans, sans-serif;
|
||||
}
|
||||
|
||||
.columnSection {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.detailsUsername {
|
||||
font-size: 1.2em;
|
||||
font-family: 'opensans-bold', sans-serif;
|
||||
}
|
||||
|
||||
.detailsInfo {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.detailsInfo p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border-radius: 0.25rem;
|
||||
font-family: 'opensans-bold', sans-serif;
|
||||
}
|
||||
|
||||
.tagInfo {
|
||||
margin: 0 20px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.tagActive {
|
||||
color: #FFFFFF;
|
||||
background-color: #EE5555;
|
||||
}
|
||||
|
||||
.tagInactive {
|
||||
color: #FFFFFF;
|
||||
background-color: #F79720;
|
||||
}
|
||||
|
||||
.tagPermanent {
|
||||
color: #FFFFFF;
|
||||
background-color: #EE5555;
|
||||
}
|
||||
|
||||
.tagExpired {
|
||||
color: #FFFFFF;
|
||||
background-color: #777777
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DetailsComponent } from './details.component';
|
||||
|
||||
describe('DetailsComponent', () => {
|
||||
let component: DetailsComponent;
|
||||
let fixture: ComponentFixture<DetailsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DetailsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DetailsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {HistoryService, PunishmentHistory} from '../../../api';
|
||||
import {NgClass, NgIf, NgOptimizedImage} from '@angular/common';
|
||||
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
|
||||
import {HistoryFormatService} from '../history-format.service';
|
||||
import {ActivatedRoute, RouterLink} from '@angular/router';
|
||||
import {catchError, map} from 'rxjs';
|
||||
import {HeaderComponent} from '../../header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details',
|
||||
imports: [
|
||||
NgIf,
|
||||
NgOptimizedImage,
|
||||
RemoveTrailingPeriodPipe,
|
||||
HeaderComponent,
|
||||
RouterLink,
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './details.component.html',
|
||||
styleUrl: './details.component.scss'
|
||||
})
|
||||
export class DetailsComponent implements OnInit {
|
||||
type: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
|
||||
id: number = -1;
|
||||
punishment: PunishmentHistory | undefined;
|
||||
|
||||
constructor(private route: ActivatedRoute,
|
||||
private historyApi: HistoryService,
|
||||
public historyFormat: HistoryFormatService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.paramMap.subscribe(params => {
|
||||
switch (params.get('type')) {
|
||||
case 'ban':
|
||||
this.type = 'ban';
|
||||
break;
|
||||
case 'mute':
|
||||
this.type = 'mute';
|
||||
break;
|
||||
case 'kick':
|
||||
this.type = 'kick';
|
||||
break;
|
||||
case 'warn':
|
||||
this.type = 'warn';
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
this.id = Number(params.get('id') || '-1');
|
||||
this.loadPunishmentData();
|
||||
});
|
||||
}
|
||||
|
||||
private loadPunishmentData() {
|
||||
if (!this.type || this.type === 'all' || this.id < 0 || isNaN(this.id)) {
|
||||
console.error("Invalid type or id");
|
||||
return;
|
||||
}
|
||||
this.historyApi.getHistoryById(this.type, this.id).pipe(
|
||||
map(punishment => {
|
||||
this.punishment = punishment;
|
||||
}),
|
||||
catchError(err => {
|
||||
console.error(err);
|
||||
return [];
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HistoryFormatService } from './history-format.service';
|
||||
|
||||
describe('HistoryFormatService', () => {
|
||||
let service: HistoryFormatService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(HistoryFormatService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {PunishmentHistory} from '../../api';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HistoryFormatService {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public getPunishmentTime(entry: PunishmentHistory) {
|
||||
const date = new Date(entry.punishmentTime);
|
||||
return date.toLocaleString(navigator.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
|
||||
public isActive(entry: PunishmentHistory): boolean {
|
||||
if (entry.removedBy !== null) {
|
||||
return false;
|
||||
}
|
||||
if (entry.expiryTime <= 0) {
|
||||
return true;
|
||||
}
|
||||
return entry.expiryTime > Date.now();
|
||||
}
|
||||
|
||||
public isPermanent(entry: PunishmentHistory): boolean {
|
||||
return entry.expiryTime <= 0;
|
||||
}
|
||||
|
||||
public getType(entry: PunishmentHistory): string {
|
||||
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
|
||||
}
|
||||
|
||||
public getExpiredTime(entry: PunishmentHistory): string {
|
||||
let suffix: string = '';
|
||||
if (entry.removedBy !== null && entry.removedBy !== undefined) {
|
||||
suffix = '(Unbanned by ' + entry.removedBy + ')';
|
||||
}
|
||||
if (entry.expiryTime <= 0) {
|
||||
return "Permanent " + this.getType(entry) + " " + suffix;
|
||||
}
|
||||
const date = new Date(entry.expiryTime);
|
||||
return date.toLocaleString(navigator.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}) + " " + suffix;
|
||||
}
|
||||
|
||||
public getAvatarUrl(entry: string, size: string = '25'): string {
|
||||
let uuid = entry.replace('-', '');
|
||||
if (uuid === 'C') {
|
||||
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
|
||||
}
|
||||
return `https://crafatar.com/avatars/${uuid}?size=${size}&overlay`;
|
||||
}
|
||||
}
|
||||
@@ -19,24 +19,32 @@
|
||||
<div>
|
||||
<tbody>
|
||||
<tr class="historyPlayerRow" *ngFor="let entry of history">
|
||||
<td class="historyType">{{ getType(entry) }}</td>
|
||||
<td class="historyPlayer">
|
||||
<td class="historyType" (click)="showDetailedPunishment(entry)">
|
||||
{{ this.historyFormat.getType(entry) }}
|
||||
</td>
|
||||
<td class="historyPlayer" (click)="setSearch(entry.username, 'player')">
|
||||
<div class="playerContainer">
|
||||
<img class="avatar" [ngSrc]="getAvatarUrl(entry.uuid)" width="25" height="25"
|
||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.uuid)" width="25" height="25"
|
||||
alt="{{entry.username}}'s Minecraft skin">
|
||||
<span class="username">{{ entry.username }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="historyPlayer">
|
||||
<td class="historyPlayer" (click)="setSearch(entry.punishedBy, 'staff')">
|
||||
<div class="playerContainer">
|
||||
<img class="avatar" [ngSrc]="getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
|
||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
|
||||
alt="{{entry.punishedBy}}'s Minecraft skin">
|
||||
<span>{{ entry.punishedBy }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="historyReason">{{ entry.reason | removeTrailingPeriod }}</td>
|
||||
<td class="historyDate">{{ getPunishmentTime(entry) }}</td>
|
||||
<td class="historyDate">{{ getExpiredTime(entry) }}</td>
|
||||
<td class="historyReason" (click)="showDetailedPunishment(entry)">
|
||||
{{ entry.reason | removeTrailingPeriod }}
|
||||
</td>
|
||||
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||
{{ this.historyFormat.getPunishmentTime(entry) }}
|
||||
</td>
|
||||
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||
{{ this.historyFormat.getExpiredTime(entry) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</div>
|
||||
|
||||
@@ -62,6 +62,7 @@ table tr td {
|
||||
|
||||
.historyPlayerRow:hover {
|
||||
background-color: var(--history-table-row-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.historyTableHead {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core';
|
||||
import {BASE_PATH, HistoryService, PunishmentHistoryInner} from '../../../api';
|
||||
import {BASE_PATH, HistoryService, PunishmentHistory} from '../../../api';
|
||||
import {catchError, map, Observable, shareReplay} from 'rxjs';
|
||||
import {NgForOf, NgIf, NgOptimizedImage} from '@angular/common';
|
||||
import {CookieService} from 'ngx-cookie-service';
|
||||
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
|
||||
import {HttpErrorResponse} from '@angular/common/http';
|
||||
import {environment} from '../../../environments/environment';
|
||||
import {HistoryFormatService} from '../history-format.service';
|
||||
import {SearchParams} from '../search-terms';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-history',
|
||||
@@ -30,12 +33,13 @@ export class HistoryComponent implements OnInit, OnChanges {
|
||||
@Input() searchTerm: string = '';
|
||||
|
||||
@Output() pageChange = new EventEmitter<number>();
|
||||
@Output() selectItem = new EventEmitter<SearchParams>();
|
||||
|
||||
private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||
|
||||
public history: PunishmentHistoryInner[] = []
|
||||
public history: PunishmentHistory[] = []
|
||||
|
||||
constructor(private historyApi: HistoryService) {
|
||||
constructor(private historyApi: HistoryService, public historyFormat: HistoryFormatService, private router: Router) {
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
@@ -47,7 +51,7 @@ export class HistoryComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
private reloadHistory(): void {
|
||||
let historyObservable: Observable<PunishmentHistoryInner[]>;
|
||||
let historyObservable: Observable<PunishmentHistory[]>;
|
||||
if (this.searchTerm.length === 0) {
|
||||
historyObservable = this.historyApi.getHistoryForAll(this.userType, this.punishmentType, this.page);
|
||||
} else {
|
||||
@@ -89,42 +93,16 @@ export class HistoryComponent implements OnInit, OnChanges {
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
public getPunishmentTime(entry: PunishmentHistoryInner) {
|
||||
const date = new Date(entry.punishmentTime);
|
||||
return date.toLocaleString(navigator.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
|
||||
public getType(entry: PunishmentHistoryInner) {
|
||||
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
|
||||
}
|
||||
|
||||
public getExpiredTime(entry: PunishmentHistoryInner) {
|
||||
if (entry.expiryTime <= 0) {
|
||||
return "Permanent " + this.getType(entry);
|
||||
setSearch(name: string, userType: 'player' | 'staff') {
|
||||
let searchParams: SearchParams = {
|
||||
userType: userType,
|
||||
punishmentType: 'all',
|
||||
searchTerm: name
|
||||
}
|
||||
const date = new Date(entry.expiryTime);
|
||||
return date.toLocaleString(navigator.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
this.selectItem.emit(searchParams);
|
||||
}
|
||||
|
||||
public getAvatarUrl(entry: string): string {
|
||||
let uuid = entry.replace('-', '');
|
||||
if (uuid === 'C') {
|
||||
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
|
||||
}
|
||||
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
|
||||
public showDetailedPunishment(entry: PunishmentHistory) {
|
||||
this.router.navigate([`bans/${entry.type}/${entry.id}`]).then();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface SearchParams {
|
||||
userType: 'player' | 'staff';
|
||||
searchTerm: string;
|
||||
punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn';
|
||||
}
|
||||
@@ -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 class="nav_li">
|
||||
<span
|
||||
[id]="getCurrentPageId(['guide', 'faq', 'ranks', 'rules', 'commandlist', 'art', 'lag', 'staffpowers',
|
||||
[id]="getCurrentPageId(['guide', 'faq', 'ranks', 'rules', 'commandlist', 'mapart', 'lag', 'staffpowers',
|
||||
'nicknames', 'bans', 'discord-bans'])" class="nav_link fake_link" [ngClass]="active">Reference</span>
|
||||
<ul class="dropdown">
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/guide']">Guide Book</a></li>
|
||||
|
||||
@@ -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,221 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'mypet'" height="460px" background_image="/public/img/backgrounds/babywither.png"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>MyPet</h1>
|
||||
<h2>Tame almost any mob in-game as your pet and train it to fight along your side, collect dropped items for you,
|
||||
and more!</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Claim a Pet</h2>
|
||||
<p>To claim a pet you will need to find the mob you want in the survival world and <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">kill it with a lead in your hand</span>. You must have a
|
||||
lead in your hand when you kill the mob or it will die instead of becoming your pet.</p>
|
||||
<p>New players can have up to 3 pets in storage and 1 active pet at a time. More pets can be stored by
|
||||
ranking up. To claim an additional pet you must first store your current pet with <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/petstore</span>. Once you have stored your current
|
||||
pet you can simply kill another mob to make it your pet. You will not be able to call it or interact with
|
||||
your other pets while they are in storage. To interact with your MyPet you will need to make it your
|
||||
active MyPet by doing <span style="font-family: 'opensans-bold', sans-serif;">/petswitch</span> and
|
||||
selecting the one you want to use.</p>
|
||||
<img ngSrc="/public/img/items/lead.png" alt="Minecraft lead/leash" style="width: 20%;" height="96"
|
||||
width="96">
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Skilltrees and Levels</h2>
|
||||
<p>Pets can be assigned to a skilltree with <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/pcst</span>, which allows it to level up and unlock new
|
||||
skills. There are 8 skilltrees to choose from, but Mage, Tank, Marksman, and Warrior all require you to
|
||||
start with "Fighter". Pets earn XP when they kill mobs, and also earn a small amount of XP any time you
|
||||
kill a mob.</p>
|
||||
<p><b>Note:</b> pets with inventories will drop it on player death.</p>
|
||||
<ul>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Utility</span> - Specializes in mining with
|
||||
pickup and inventory skills + a Haste II beacon at level 150. (150 levels)
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Mount</span> - A rideable pet specializes in
|
||||
speed and jump height + jump beacon at level 150, and inventory at level 200. (200 levels)
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Defender</span> - Specializes in defending
|
||||
you, it absorbs damage dealt towards you. High HP + regen beacon at level 200. (200 levels)
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Fighter</span> - Starter rank for all other
|
||||
fighter categories. Increases pet attack damage. (50 levels)
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Mage</span> - Specializes in specialty attacks
|
||||
like fireballs and lightning. No melee abilities. (100 levels)
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Tank</span> - Specializes in defense with
|
||||
lots of HP, redirecting damage, and sweeping attacks. Resistance beacon at level 100. (100 levels)
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Marksman</span> - Specializes in ranged
|
||||
attacks with arrow and poison damage, no melee abilities + invisibility beacon at level 50. (100 levels)
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Warrior</span> - An all-around class with no
|
||||
speciality, has some skills from every other class + a strength beacon at level 100. (100 levels)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Equip Your Pet</h2>
|
||||
<p>Some pets can hold items in their hands or wear armor. To equip a pet with something to hold or wear,
|
||||
simply right click on your pet while shifting with the item in your hand. To remove the equipment right
|
||||
click on your MyPet while shifting with shears in your hand.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Naming Your Pet</h2>
|
||||
<p>You can name your pet with <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/petname <name></span>. Please note that
|
||||
inappropriate names (anything that wouldn't be allowed to be said in chat) are not allowed.</p>
|
||||
<p>Duke members and above can change the color of their pet's name. To get a list of all the color codes
|
||||
please do <span style="font-family: 'opensans-bold', sans-serif;">/colors</span> in-game.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Pet Behavior</h2>
|
||||
<p>You can change the way your pet attacks by changing its behavior. Different behaviors are unlocked as
|
||||
your pet levels up. The available behaviors are listed below.</p>
|
||||
<ul>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Friendly:</span> Won't fight, even if it's
|
||||
attacked by anything
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Normal:</span> Acts like a normal wolf</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Aggressive:</span> Attacks everything within
|
||||
15 blocks of the owner
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Farm:</span> Attacks every hostile mob within
|
||||
15 blocks of the owner
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Raid:</span> Like normal, but the MyPet won't
|
||||
attack players and their pets
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Duel</span> Attacks other pets with active
|
||||
duel behavior within a 5 block radius
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Feed Your Pet</h2>
|
||||
<p>You must feed your pet to keep its health up or it will die from hunger! If your pet is hungry it will
|
||||
also be weaker and will deal less damage when attacking.</p>
|
||||
<p>Each pet has a special food that it must be fed. You can see which food is right for your pet by doing
|
||||
<span style="font-family: 'opensans-bold', sans-serif;">/petinfo</span>.</p>
|
||||
<div class="inlineIcons">
|
||||
<img ngSrc="/public/img/items/potato.png" alt="Minecraft potato" height="81" width="77">
|
||||
<img ngSrc="/public/img/items/carrot.png" alt="Minecraft carrot" height="81" width="89">
|
||||
<img ngSrc="/public/img/items/steak.png" alt="Minecraft steak" height="81" width="88">
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>How to Hide/Disable a MyPet</h2>
|
||||
<p>There are two ways to disable a pet, and they work very differently. The first, <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/petsendaway</span>, will hide your active MyPet until
|
||||
you un-hide it with <span style="font-family: 'opensans-bold', sans-serif;">/petcall</span>. A pet that
|
||||
has been hidden is still considered your "active MyPet", it just isn't visible right now. It also only
|
||||
hides your pet during your current session.</p>
|
||||
<p>The second way to disable a pet is to put it into storage with <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/petstore</span>. Pets that are in storage are
|
||||
completely deactivated until you use <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/petswitch</span> to choose one you want to use. If
|
||||
all of your pets are in storage, the server will tell you that you do not have a MyPet. This is also how
|
||||
you can claim additional MyPets - by completely deactivating your current one first.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Blocked Mob Types</h2>
|
||||
<p>The following mobs are not allowed to be claimed as a pet: the enderdragon, villagers and villager-like
|
||||
mobs, shulkers, ghasts, and elder guardians. If you kill any of these mobs it will die and will NOT become
|
||||
your pet.</p>
|
||||
<p>Furthermore, all horses (including donkeys and mules) are currently disabled as pets due to them being
|
||||
buggy.</p>
|
||||
<div class="inlineIcons">
|
||||
<img ngSrc="/public/img/items/enderdragon.png"
|
||||
style="padding: 25px 25px;"
|
||||
alt="Minecraft enderdragon mob"
|
||||
height="100" width="100">
|
||||
<img ngSrc="/public/img/items/villager.png"
|
||||
style="padding: 25px 25px;"
|
||||
alt="Minecraft villager mob"
|
||||
height="100" width="100">
|
||||
<img ngSrc="/public/img/items/zombievillager.png"
|
||||
style="padding: 25px 25px;"
|
||||
alt="Minecraft zombie villager mob"
|
||||
height="100" width="100">
|
||||
<img ngSrc="/public/img/items/shulker.png"
|
||||
style="padding: 25px 25px;"
|
||||
alt="Minecraft shulker mob"
|
||||
height="100" width="100">
|
||||
<img ngSrc="/public/img/items/ghast.png"
|
||||
style="padding: 25px 25px;"
|
||||
alt="Minecraft ghast mob"
|
||||
height="100" width="100">
|
||||
<img ngSrc="/public/img/items/elderguardian.png"
|
||||
style="padding: 25px 25px;"
|
||||
alt="Minecraft elder guardian mob"
|
||||
height="100" width="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Disclaimer</h2>
|
||||
<p>When you die with your MyPet out, the MyPet will drop all of the items in its inventory. This means that
|
||||
if your MyPet is in a dangerous area when you die, your items can be lost. And if you die in pvp the items
|
||||
won't be protected. If your MyPet dies on it’s own, it will not drop it’s inventory.</p>
|
||||
<p>Some skill trees require a base skill to be able to select them. The fighter skill tree is a base skill
|
||||
and will not level beyond 50. When you reach level 50, you will need to switch to one of the secondary
|
||||
skill trees with <span style="font-family: 'opensans-bold', sans-serif;">/pcst.</span></p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Useful Commands</h2>
|
||||
<ul>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petcall -</span> Make your pet come to you if
|
||||
it vanished
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petsendaway -</span> Make your pet vanish
|
||||
temporarily
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petrelease <name>-</span> Release
|
||||
your pet forever
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinfo -</span> Check your pet's health and
|
||||
food as well as other stats
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/pcst -</span> Choose a skilltree for your
|
||||
pet.
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petskill -</span> Check your pet's skills and
|
||||
strengths
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petname <name>-</span> Change the name
|
||||
of your pet (see Naming Your Pet above)
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinventory -</span> Open your pet's
|
||||
inventory to see what it has picked up
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petpickup -</span> Toggle whether your pet
|
||||
will pick up items or not
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbehavior -</span> Change the behavior
|
||||
class of your pet (see Pet Behavior above)
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbeacon -</span> Turn your pet into a
|
||||
walking beacon to give you strength, speed, and other power-ups
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petswitch -</span> Allows you to switch
|
||||
between your MyPets
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petstore -</span> Store your current pet to
|
||||
claim another
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,15 @@
|
||||
main ul {
|
||||
font-family: opensans, sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
main li {
|
||||
margin-left: 30px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.inlineIcons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MypetComponent } from './mypet.component';
|
||||
|
||||
describe('MypetComponent', () => {
|
||||
let component: MypetComponent;
|
||||
let fixture: ComponentFixture<MypetComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MypetComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MypetComponent);
|
||||
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 {NgOptimizedImage} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mypet',
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
NgOptimizedImage
|
||||
],
|
||||
templateUrl: './mypet.component.html',
|
||||
styleUrl: './mypet.component.scss'
|
||||
})
|
||||
export class MypetComponent {
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user