Compare commits
22
Commits
a1912b96d8
...
particles
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtEncoder jwtEncoder() {
|
||||
KeyPair keyPair = keyPairService.getJwtSigningKeyPair();
|
||||
JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
|
||||
.privateKey((RSAPrivateKey) keyPair.getPrivate())
|
||||
.build();
|
||||
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
|
||||
return new NimbusJwtEncoder(jwkSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
KeyPair keyPair = keyPairService.getJwtSigningKeyPair();
|
||||
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
|
||||
}
|
||||
}
|
||||
+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"
|
||||
},
|
||||
|
||||
@@ -2,13 +2,15 @@ import {Component} from '@angular/core';
|
||||
import {ScrollService} from '../scroll/scroll.service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {RemoveTrailingPeriodPipe} from "../util/RemoveTrailingPeriodPipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent
|
||||
HeaderComponent,
|
||||
RemoveTrailingPeriodPipe
|
||||
],
|
||||
templateUrl: './about.component.html',
|
||||
styleUrl: './about.component.scss'
|
||||
|
||||
@@ -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,189 @@
|
||||
<app-header current_page="particles" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Particle Creator</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="column">
|
||||
<div class="renderer-section row column">
|
||||
<div #rendererContainer class="renderer-container column row"></div>
|
||||
<div class="plane-controls">
|
||||
<label>Plane Position (Z-axis):</label>
|
||||
<mat-slider min="0" max="16" step="1" #planeSlider>
|
||||
<input matSliderThumb [(ngModel)]="planePosition" (input)="updatePlanePosition($event)">
|
||||
</mat-slider>
|
||||
<span>{{ planePosition }}/16 blocks</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column controls-section">
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Particle Properties</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Particle Name</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.particle_name" placeholder="Enter particle name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Display Name</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.display_name" placeholder="Enter display name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Particle Type</mat-label>
|
||||
<mat-select [(ngModel)]="particleData.particle_type">
|
||||
<mat-option *ngFor="let type of particleTypes" [value]="type">{{ type }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Lore</mat-label>
|
||||
<textarea matInput [(ngModel)]="particleData.lore" placeholder="Enter lore"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Display Item</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.display_item" placeholder="Enter display item">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Permission</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.permission" placeholder="Enter permission">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Package Permission</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.package_permission" placeholder="Enter package permission">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Frame Delay</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.frame_delay" placeholder="Enter frame delay">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Repeat</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.repeat" placeholder="Enter repeat count">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Repeat Delay</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.repeat_delay"
|
||||
placeholder="Enter repeat delay">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Random Offset</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.random_offset"
|
||||
placeholder="Enter random offset">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-checkbox [(ngModel)]="particleData.stationary">Stationary</mat-checkbox>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<mat-card class="color-picker-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Particle Color</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="color-picker">
|
||||
<input type="color" [(ngModel)]="selectedColor">
|
||||
<span>Selected Color: {{ selectedColor }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<div class="column frames-section">
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Frames</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="frames-container">
|
||||
<mat-tab-group [selectedIndex]="frames.indexOf(currentFrame)"
|
||||
(selectedIndexChange)="switchFrame(frames[$event])">
|
||||
<mat-tab *ngFor="let frameId of frames" [label]="frameId">
|
||||
<div class="frame-content">
|
||||
<h3>Particles in {{ frameId }}</h3>
|
||||
<div class="particles-list">
|
||||
<div *ngFor="let particle of particleData.frames[frameId]; let i = index" class="particle-item">
|
||||
<span>Particle {{ i + 1 }}: ({{ particle.x.toFixed(2) }}, {{ particle.y.toFixed(2) }}
|
||||
, {{ particle.z.toFixed(2) }})</span>
|
||||
<button mat-icon-button color="warn" (click)="removeParticle(frameId, i)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="!particleData.frames[frameId] || particleData.frames[frameId].length === 0"
|
||||
class="no-particles">
|
||||
No particles in this frame. Click on the plane to add particles.
|
||||
</div>
|
||||
</div>
|
||||
<div class="frame-actions">
|
||||
<button mat-raised-button color="warn" (click)="removeFrame(frameId)"
|
||||
[disabled]="frames.length <= 1">
|
||||
Remove Frame
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
<div class="add-frame">
|
||||
<button mat-raised-button color="primary" (click)="addFrame()">
|
||||
Add New Frame
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<div class="column json-output">
|
||||
|
||||
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>JSON Output</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<pre>{{ generateJson() }}</pre>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
@@ -0,0 +1,128 @@
|
||||
.renderer-section {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.renderer-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.plane-controls {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.plane-controls mat-slider {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.color-picker-card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.color-picker input[type="color"] {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.frames-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.frames-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.frame-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.particles-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.particle-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.particle-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.no-particles {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.frame-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.add-frame {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.json-output {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.json-output pre {
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
max-height: 300px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ParticlesComponent } from './particles.component';
|
||||
|
||||
describe('ParticlesComponent', () => {
|
||||
let component: ParticlesComponent;
|
||||
let fixture: ComponentFixture<ParticlesComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ParticlesComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ParticlesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,494 @@
|
||||
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatSelectModule} from '@angular/material/select';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||||
import {MatTabsModule} from '@angular/material/tabs';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import * as THREE from 'three';
|
||||
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
|
||||
// Define particle types
|
||||
export enum ParticleType {
|
||||
REDSTONE = 'REDSTONE',
|
||||
// Other particle types can be added later
|
||||
}
|
||||
|
||||
// Interface for particle information
|
||||
interface ParticleInfo {
|
||||
particle_type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
color: string;
|
||||
extra: number;
|
||||
}
|
||||
|
||||
// Interface for the complete particle data
|
||||
interface ParticleData {
|
||||
particle_name: string;
|
||||
display_name: string;
|
||||
particle_type: string;
|
||||
lore: string;
|
||||
display_item: string;
|
||||
permission: string;
|
||||
package_permission: string;
|
||||
frame_delay: number;
|
||||
repeat: number;
|
||||
repeat_delay: number;
|
||||
random_offset: number;
|
||||
stationary: boolean;
|
||||
frames: {
|
||||
[frameId: string]: ParticleInfo[];
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-particles',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatSliderModule,
|
||||
MatCheckboxModule,
|
||||
MatTabsModule,
|
||||
MatCardModule,
|
||||
MatIconModule,
|
||||
HeaderComponent
|
||||
],
|
||||
templateUrl: './particles.component.html',
|
||||
styleUrl: './particles.component.scss'
|
||||
})
|
||||
export class ParticlesComponent implements OnInit, AfterViewInit {
|
||||
@ViewChild('rendererContainer') rendererContainer!: ElementRef;
|
||||
@ViewChild('planeSlider') planeSlider!: ElementRef;
|
||||
|
||||
// Three.js objects
|
||||
private scene!: THREE.Scene;
|
||||
private camera!: THREE.PerspectiveCamera;
|
||||
private renderer!: THREE.WebGLRenderer;
|
||||
private controls!: OrbitControls;
|
||||
private playerModel!: THREE.Group;
|
||||
private intersectionPlane!: THREE.Mesh;
|
||||
private particles: THREE.Mesh[] = [];
|
||||
private raycaster = new THREE.Raycaster();
|
||||
private mouse = new THREE.Vector2();
|
||||
|
||||
// Particle data
|
||||
public particleData: ParticleData = {
|
||||
particle_name: '',
|
||||
display_name: '',
|
||||
particle_type: ParticleType.REDSTONE,
|
||||
lore: '',
|
||||
display_item: 'REDSTONE',
|
||||
permission: '',
|
||||
package_permission: '',
|
||||
frame_delay: 1,
|
||||
repeat: 1,
|
||||
repeat_delay: 0,
|
||||
random_offset: 0,
|
||||
stationary: true,
|
||||
frames: {
|
||||
'frame1': []
|
||||
}
|
||||
};
|
||||
|
||||
// UI state
|
||||
public currentFrame: string = 'frame1';
|
||||
public planePosition: number = 8; // Position in 1/16th of a block
|
||||
public selectedColor: string = '#ff0000';
|
||||
public particleTypes = Object.values(ParticleType);
|
||||
public frames: string[] = ['frame1'];
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Initialize component
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.initThreeJS();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
// Initialize Three.js scene
|
||||
private initThreeJS(): void {
|
||||
// Create scene
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0xf0f0f0);
|
||||
|
||||
// Get container dimensions
|
||||
const containerWidth = this.rendererContainer.nativeElement.clientWidth;
|
||||
const containerHeight = 400; // Fixed height as defined in CSS
|
||||
|
||||
// Create camera
|
||||
this.camera = new THREE.PerspectiveCamera(75, containerWidth / containerHeight, 0.1, 1000);
|
||||
this.camera.position.set(0, 1, 3);
|
||||
this.camera.lookAt(0, 1, 0);
|
||||
|
||||
// Create renderer
|
||||
this.renderer = new THREE.WebGLRenderer({antialias: true});
|
||||
this.renderer.setSize(containerWidth, containerHeight);
|
||||
|
||||
// Center the canvas in the container
|
||||
this.renderer.domElement.style.display = 'block';
|
||||
this.renderer.domElement.style.margin = 'auto';
|
||||
|
||||
this.rendererContainer.nativeElement.appendChild(this.renderer.domElement);
|
||||
|
||||
// Initialize orbit controls
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true; // Add smooth damping effect
|
||||
this.controls.dampingFactor = 0.05;
|
||||
this.controls.minDistance = 2; // Minimum zoom distance
|
||||
this.controls.maxDistance = 10; // Maximum zoom distance
|
||||
this.controls.target.set(0, 1, 0); // Set target to player's center
|
||||
this.controls.update();
|
||||
|
||||
// Add lights
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
directionalLight.position.set(1, 1, 1);
|
||||
this.scene.add(directionalLight);
|
||||
|
||||
// Create player model (simple representation)
|
||||
this.createPlayerModel();
|
||||
|
||||
// Create intersection plane
|
||||
this.createIntersectionPlane();
|
||||
|
||||
// Add event listeners
|
||||
this.renderer.domElement.addEventListener('mousedown', this.onMouseDown.bind(this));
|
||||
this.renderer.domElement.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||
this.renderer.domElement.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||
window.addEventListener('resize', this.onWindowResize.bind(this));
|
||||
}
|
||||
|
||||
// Create a simple player model
|
||||
private createPlayerModel(): void {
|
||||
this.playerModel = new THREE.Group();
|
||||
|
||||
// Head
|
||||
const headGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
|
||||
const headMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
|
||||
const head = new THREE.Mesh(headGeometry, headMaterial);
|
||||
head.position.y = 1.35;
|
||||
this.playerModel.add(head);
|
||||
|
||||
// Body
|
||||
const bodyGeometry = new THREE.BoxGeometry(0.5, 0.7, 0.25);
|
||||
const bodyMaterial = new THREE.MeshLambertMaterial({color: 0x0000ff});
|
||||
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
||||
body.position.y = 0.75;
|
||||
this.playerModel.add(body);
|
||||
|
||||
// Arms
|
||||
const armGeometry = new THREE.BoxGeometry(0.2, 0.7, 0.25);
|
||||
const armMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
|
||||
|
||||
const leftArm = new THREE.Mesh(armGeometry, armMaterial);
|
||||
leftArm.position.set(-0.35, 0.75, 0);
|
||||
this.playerModel.add(leftArm);
|
||||
|
||||
const rightArm = new THREE.Mesh(armGeometry, armMaterial);
|
||||
rightArm.position.set(0.35, 0.75, 0);
|
||||
this.playerModel.add(rightArm);
|
||||
|
||||
// Legs
|
||||
const legGeometry = new THREE.BoxGeometry(0.25, 0.7, 0.25);
|
||||
const legMaterial = new THREE.MeshLambertMaterial({color: 0x000000});
|
||||
|
||||
const leftLeg = new THREE.Mesh(legGeometry, legMaterial);
|
||||
leftLeg.position.set(-0.125, 0.15, 0);
|
||||
this.playerModel.add(leftLeg);
|
||||
|
||||
const rightLeg = new THREE.Mesh(legGeometry, legMaterial);
|
||||
rightLeg.position.set(0.125, 0.15, 0);
|
||||
this.playerModel.add(rightLeg);
|
||||
|
||||
this.scene.add(this.playerModel);
|
||||
}
|
||||
|
||||
// Create the intersection plane
|
||||
private createIntersectionPlane(): void {
|
||||
const planeGeometry = new THREE.PlaneGeometry(3, 3);
|
||||
const planeMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0x00AA00,
|
||||
transparent: true,
|
||||
opacity: 0.05,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
this.intersectionPlane = new THREE.Mesh(planeGeometry, planeMaterial);
|
||||
this.intersectionPlane.position.z = 0;
|
||||
// Center the plane vertically with the player (player is about 2 blocks tall)
|
||||
this.intersectionPlane.position.y = 1;
|
||||
this.scene.add(this.intersectionPlane);
|
||||
}
|
||||
|
||||
// Update plane position based on slider
|
||||
public updatePlanePosition(event: Event): void {
|
||||
// Access the value from the slider element
|
||||
const slider = event.target as HTMLInputElement;
|
||||
this.planePosition = Number(slider.value);
|
||||
// Convert from 1/16th block to Three.js units
|
||||
const position = (this.planePosition / 16) - 0.5; // Center at 0
|
||||
|
||||
// Apply position based on the plane's current rotation
|
||||
// This ensures the plane always moves towards/away from the viewer
|
||||
const rotation = this.intersectionPlane.rotation.y;
|
||||
|
||||
if (Math.abs(rotation) < 0.1 || Math.abs(rotation - Math.PI) < 0.1) {
|
||||
// Camera in front (0) or behind (PI)
|
||||
// For camera behind, we need to invert the direction
|
||||
const direction = Math.abs(rotation) < 0.1 ? 1 : -1;
|
||||
this.intersectionPlane.position.z = position * direction;
|
||||
// Reset x position to avoid cumulative changes
|
||||
this.intersectionPlane.position.x = 0;
|
||||
} else {
|
||||
// Camera on right (PI/2) or left (-PI/2)
|
||||
// For camera on left, we need to invert the direction
|
||||
const direction = rotation > 0 ? 1 : -1;
|
||||
this.intersectionPlane.position.x = position * direction;
|
||||
// Reset z position to avoid cumulative changes
|
||||
this.intersectionPlane.position.z = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Track if mouse is being dragged
|
||||
private isDragging = false;
|
||||
private mouseDownTime = 0;
|
||||
|
||||
// Handle mouse down event
|
||||
private onMouseDown(event: MouseEvent): void {
|
||||
this.isDragging = false;
|
||||
this.mouseDownTime = Date.now();
|
||||
}
|
||||
|
||||
// Handle mouse up event
|
||||
private onMouseUp(event: MouseEvent): void {
|
||||
// If mouse was down for less than 200ms and didn't move much, consider it a click, not a drag
|
||||
if (Date.now() - this.mouseDownTime < 200 && !this.isDragging) {
|
||||
this.handlePlaneClick(event);
|
||||
}
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
// Handle mouse move event
|
||||
private onMouseMove(event: MouseEvent): void {
|
||||
// If mouse moves while button is pressed, it's a drag
|
||||
if (event.buttons > 0) {
|
||||
this.isDragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle mouse click on the plane
|
||||
private handlePlaneClick(event: MouseEvent): void {
|
||||
// Calculate mouse position in normalized device coordinates
|
||||
const rect = this.renderer.domElement.getBoundingClientRect();
|
||||
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
// Update the picking ray with the camera and mouse position
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera);
|
||||
|
||||
// Calculate objects intersecting the picking ray
|
||||
const intersects = this.raycaster.intersectObject(this.intersectionPlane);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const point = intersects[0].point;
|
||||
this.addParticle(point.x, point.y, point.z);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a particle at the specified position
|
||||
private addParticle(x: number, y: number, z: number): void {
|
||||
// Create a visual representation of the particle
|
||||
const particleGeometry = new THREE.SphereGeometry(0.03, 16, 16);
|
||||
const particleMaterial = new THREE.MeshBasicMaterial({color: this.selectedColor});
|
||||
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
|
||||
|
||||
particleMesh.position.set(x, y, z);
|
||||
this.scene.add(particleMesh);
|
||||
this.particles.push(particleMesh);
|
||||
|
||||
// Add to particle data
|
||||
const hexColor = this.selectedColor.replace('#', '');
|
||||
const r = parseInt(hexColor.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(hexColor.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(hexColor.substring(4, 6), 16) / 255;
|
||||
|
||||
const particleInfo: ParticleInfo = {
|
||||
particle_type: ParticleType.REDSTONE,
|
||||
x: x,
|
||||
y: y,
|
||||
z: z,
|
||||
color: `${r},${g},${b}`,
|
||||
extra: 1
|
||||
};
|
||||
|
||||
if (!this.particleData.frames[this.currentFrame]) {
|
||||
this.particleData.frames[this.currentFrame] = [];
|
||||
}
|
||||
|
||||
this.particleData.frames[this.currentFrame].push(particleInfo);
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
private onWindowResize(): void {
|
||||
const containerWidth = this.rendererContainer.nativeElement.clientWidth;
|
||||
const containerHeight = 400; // Fixed height as defined in CSS
|
||||
|
||||
this.camera.aspect = containerWidth / containerHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(containerWidth, containerHeight);
|
||||
}
|
||||
|
||||
// Animation loop
|
||||
private animate(): void {
|
||||
requestAnimationFrame(this.animate.bind(this));
|
||||
|
||||
// Update controls
|
||||
if (this.controls) {
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
// Update plane orientation based on camera position
|
||||
if (this.intersectionPlane && this.camera) {
|
||||
// Calculate the angle between camera and player (in the XZ plane)
|
||||
const cameraAngle = Math.atan2(
|
||||
this.camera.position.x,
|
||||
this.camera.position.z
|
||||
);
|
||||
|
||||
// Determine which quadrant the camera is in with a 45-degree offset
|
||||
// Adding Math.PI/4 (45 degrees) to the angle before determining the quadrant
|
||||
// This shifts the quadrant boundaries by 45 degrees
|
||||
const quadrant = Math.floor((cameraAngle + Math.PI + Math.PI / 4) / (Math.PI / 2)) % 4;
|
||||
|
||||
// Rotate the plane to face the camera
|
||||
if (quadrant === 0) {
|
||||
this.intersectionPlane.rotation.y = 0; // Camera in front
|
||||
this.updateFrameMaterial(0x00AA00);
|
||||
} else if (quadrant === 1) {
|
||||
this.intersectionPlane.rotation.y = Math.PI / 2; // Camera on right
|
||||
this.updateFrameMaterial(0x0000AA);
|
||||
} else if (quadrant === 2) {
|
||||
this.intersectionPlane.rotation.y = Math.PI; // Camera behind
|
||||
this.updateFrameMaterial(0x00AA00);
|
||||
} else {
|
||||
this.intersectionPlane.rotation.y = -Math.PI / 2; // Camera on left
|
||||
this.updateFrameMaterial(0x0000AA);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
public updateFrameMaterial(color: number) {
|
||||
this.intersectionPlane.material = new THREE.MeshBasicMaterial({
|
||||
color: color,
|
||||
transparent: true,
|
||||
opacity: 0.05,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
}
|
||||
|
||||
// Add a new frame
|
||||
public addFrame(): void {
|
||||
const frameId = `frame${this.frames.length + 1}`;
|
||||
this.frames.push(frameId);
|
||||
this.particleData.frames[frameId] = [];
|
||||
this.currentFrame = frameId;
|
||||
this.clearParticleVisuals();
|
||||
}
|
||||
|
||||
// Switch to a different frame
|
||||
public switchFrame(frameId: string): void {
|
||||
this.currentFrame = frameId;
|
||||
this.clearParticleVisuals();
|
||||
this.renderFrameParticles(frameId);
|
||||
}
|
||||
|
||||
// Clear particle visuals from the scene
|
||||
private clearParticleVisuals(): void {
|
||||
for (const particle of this.particles) {
|
||||
this.scene.remove(particle);
|
||||
}
|
||||
this.particles = [];
|
||||
}
|
||||
|
||||
// Render particles for a specific frame
|
||||
private renderFrameParticles(frameId: string): void {
|
||||
if (!this.particleData.frames[frameId]) return;
|
||||
|
||||
for (const particleInfo of this.particleData.frames[frameId]) {
|
||||
const particleGeometry = new THREE.SphereGeometry(0.03, 16, 16);
|
||||
|
||||
// Parse color
|
||||
const colorParts = particleInfo.color.split(',');
|
||||
const color = new THREE.Color(
|
||||
parseFloat(colorParts[0]),
|
||||
parseFloat(colorParts[1]),
|
||||
parseFloat(colorParts[2])
|
||||
);
|
||||
|
||||
const particleMaterial = new THREE.MeshBasicMaterial({color});
|
||||
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
|
||||
|
||||
particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z);
|
||||
this.scene.add(particleMesh);
|
||||
this.particles.push(particleMesh);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate JSON output
|
||||
public generateJson(): string {
|
||||
return JSON.stringify(this.particleData, null, 2);
|
||||
}
|
||||
|
||||
// Remove a particle
|
||||
public removeParticle(frameId: string, index: number): void {
|
||||
if (this.particleData.frames[frameId] && this.particleData.frames[frameId].length > index) {
|
||||
this.particleData.frames[frameId].splice(index, 1);
|
||||
|
||||
// Update visuals if this is the current frame
|
||||
if (frameId === this.currentFrame) {
|
||||
this.clearParticleVisuals();
|
||||
this.renderFrameParticles(frameId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a frame
|
||||
public removeFrame(frameId: string): void {
|
||||
const index = this.frames.indexOf(frameId);
|
||||
if (index !== -1) {
|
||||
this.frames.splice(index, 1);
|
||||
delete this.particleData.frames[frameId];
|
||||
|
||||
// Switch to first frame if we removed the current one
|
||||
if (frameId === this.currentFrame && this.frames.length > 0) {
|
||||
this.switchFrame(this.frames[0]);
|
||||
} else if (this.frames.length === 0) {
|
||||
// If no frames left, add one
|
||||
this.addFrame();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'ranks'" height="460px" background_image="/public/img/backgrounds/118fjord.png"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Server Ranks</h1>
|
||||
<h2>The standard ranks are unlocked by playing on the server. As you accumulate time you will automatically rank
|
||||
up. Do /rank to check your playtime.</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="tableTitle">Rank</th>
|
||||
<th scope="col" class="tableTitle">Requirements</th>
|
||||
<th scope="col" class="tableTitle">Perks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank1">Drifter</span>]</td>
|
||||
<td data-label="Requirements">Join the server!</td>
|
||||
<td data-label="Perks" class="perks">
|
||||
All basic permissions<br>
|
||||
Up to 3 homes in survival<br>
|
||||
Up to 2 MyPet slots<br>
|
||||
Up to 5 claims in survival
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank1">Nomad</span>]</td>
|
||||
<td data-label="Requirements">Play for 2 hours</td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Up to 4 homes in survival<br>
|
||||
Up to 3 MyPet slots<br>
|
||||
Up to 10 claims in survival
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank1">Peddler</span>]</td>
|
||||
<td data-label="Requirements">Play for 1 day</td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Up to 5 homes in survival<br>
|
||||
Up to 4 MyPet slots<br>
|
||||
Up to 15 claims in survival
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank1">Settler</span>]</td>
|
||||
<td data-label="Requirements">Play for 3 days</td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Up to 6 homes in survival<br>
|
||||
Up to 5 MyPet slots<br>
|
||||
Up to 20 claims in survival
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank1">Resident</span>]</td>
|
||||
<td data-label="Requirements">Play for 7 days</td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Up to 7 homes in survival<br>
|
||||
Up to 6 MyPet slots<br>
|
||||
Up to 25 claims in survival
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank1">Esquire</span>]</td>
|
||||
<td data-label="Requirements">Play for 14 days</td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Up to 8 homes in survival<br>
|
||||
Up to 7 MyPet slots<br>
|
||||
Up to 30 claims in survival
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank1">Knight</span>]</td>
|
||||
<td data-label="Requirements">Play for 30 days</td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Up to 9 homes in survival<br>
|
||||
Up to 8 MyPet slots
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank1">Baron</span>]</td>
|
||||
<td data-label="Requirements">Play for 60 days</td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Up to 10 homes in survival<br>
|
||||
Up to 9 MyPet slots
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank1">Viscount</span>]</td>
|
||||
<td data-label="Requirements">Play for 180 days</td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Up to 11 homes in survival<br>
|
||||
Up to 9 MyPet slots
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section id="subheaderDonor">
|
||||
<div class="title" style="margin-top: 0; height: 100%;">
|
||||
<h1>Premium Ranks</h1>
|
||||
<h2>Premium ranks are a symbol of those who have purchased a rank to support the server. Purchase a rank today
|
||||
at: <a href="https://store.alttd.com">donate.alttd.com</a>. Thank you!</h2>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<table style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="tableTitle">Rank</th>
|
||||
<th scope="col" class="tableTitle">Requirements</th>
|
||||
<th scope="col" class="tableTitle">Perks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank2">Count</span>]</td>
|
||||
<td data-label="Requirements">Purchase for $15 on <a href="https://store.alttd.com">Buycraft</a></td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Donor Discord channel access<br>
|
||||
Up to 12 homes in survival<br>
|
||||
Up to 10 MyPet slots<br>
|
||||
More on the <a href="https://store.alttd.com/category/514458">store page</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank2">Viceroy</span>]</td>
|
||||
<td data-label="Requirements">Purchase for $30 on <a href="https://store.alttd.com">Buycraft</a></td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Priority queue for full servers<br>
|
||||
Up to 13 homes in survival<br>
|
||||
Up to 18 MyPet slots<br>
|
||||
More on the <a href="https://store.alttd.com/category/514458">store page</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank2">Duke</span>]</td>
|
||||
<td data-label="Requirements">Purchase for $50 on <a href="https://store.alttd.com">Buycraft</a></td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Nickname in chat<br>
|
||||
Up to 14 homes in survival<br>
|
||||
Up to 27 MyPet slots<br>
|
||||
More on the <a href="https://store.alttd.com/category/514458">store page</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank2">Archduke</span>]</td>
|
||||
<td data-label="Requirements">Purchase for $100 on <a href="https://store.alttd.com">Buycraft</a></td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Custom prefix in chat<br>
|
||||
Up to 16 homes in survival<br>
|
||||
Up to 36 MyPet slots<br>
|
||||
More on the <a href="https://store.alttd.com/category/514458">store page</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank2">α</span>]</td>
|
||||
<td data-label="Requirements">Purchase for $25 on <a href="https://store.alttd.com">Buycraft</a></td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Time-limited rank<br>
|
||||
Access to alpha testing<br>
|
||||
This custom prefix<br>
|
||||
More on the <a href="https://store.alttd.com/category/514458">store page</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank4">▲</span>]</td>
|
||||
<td data-label="Requirements">Boosting with <a href="https://discordapp.com/nitro">Discord Nitro</a></td>
|
||||
<td data-label="Perks" class="perks">
|
||||
Discord Nitro perks<br>
|
||||
This custom prefix
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section id="subheaderCommunity">
|
||||
<div class="title" style="margin-top: 0; height: 100%;">
|
||||
<h1>Community Ranks</h1>
|
||||
<h2>Community ranks are for talented people who support Altitude in more than one way.</h2>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<table style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="tableTitle">Rank</th>
|
||||
<th scope="col" class="tableTitle">Requirements</th>
|
||||
<th scope="col" class="tableTitle">Perks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank5">Social Media</span>]</td>
|
||||
<td data-label="Requirements">See requirements on the <a href="/community.php">Community</a> page</td>
|
||||
<td data-label="Perks" class="perks">/record<br>/ptime<br>/pweather</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank5">Streamer</span>]</td>
|
||||
<td data-label="Requirements">See requirements on the <a href="/community.php">Community</a> page</td>
|
||||
<td data-label="Perks" class="perks">/record<br>/ptime<br>/pweather</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank5">YouTube</span>]</td>
|
||||
<td data-label="Requirements">See requirements on the <a href="/community.php">Community</a> page</td>
|
||||
<td data-label="Perks" class="perks">/record<br>/ptime<br>/pweather</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank5">Event Leader</span>]</td>
|
||||
<td data-label="Requirements">See requirements on the <a href="/community.php">Community</a> page</td>
|
||||
<td data-label="Perks" class="perks">Build perms on event server</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-label="Rank" class="rankTitle">[<span class="rank5">Event Team</span>]</td>
|
||||
<td data-label="Requirements">See requirements on the <a href="/community.php">Community</a> page</td>
|
||||
<td data-label="Perks" class="perks">Build perms on event server</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,151 @@
|
||||
#subheaderDonor {
|
||||
height: 350px;
|
||||
background: -webkit-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/newtown.jpg");
|
||||
background: -o-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/newtown.jpg");
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/newtown.jpg");
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#subheaderCommunity {
|
||||
height: 350px;
|
||||
background: -webkit-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/caruselimage4.png");
|
||||
background: -o-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/caruselimage4.png");
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/caruselimage4.png");
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
font-family: 'opensans', sans-serif;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
table tr {
|
||||
padding: .35em;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: .625em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table th {
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
.perks {
|
||||
text-align: left;
|
||||
padding-left: 105px;
|
||||
}
|
||||
|
||||
.tableTitle {
|
||||
font-family: 'opensans-bold', sans-serif;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.rankTitle {
|
||||
font-family: 'opensans-bold', sans-serif;
|
||||
}
|
||||
|
||||
.rank1 {
|
||||
color: #265ac9;
|
||||
}
|
||||
|
||||
.rank2 {
|
||||
color: #980399;
|
||||
}
|
||||
|
||||
.rank3 {
|
||||
color: #0c3999;
|
||||
}
|
||||
|
||||
.rank4 {
|
||||
color: #c949c9;
|
||||
}
|
||||
|
||||
.rank5 {
|
||||
color: #ffaa00;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.perks {
|
||||
padding-left: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.perks {
|
||||
padding-left: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 690px) {
|
||||
main .container {
|
||||
width: 80% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
table {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
padding: .400em;
|
||||
}
|
||||
|
||||
table thead {
|
||||
border: none;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
table tr {
|
||||
display: block;
|
||||
margin-bottom: .625em;
|
||||
}
|
||||
|
||||
table td {
|
||||
display: block;
|
||||
font-size: .8em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table td::before {
|
||||
content: attr(data-label);
|
||||
float: left;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
table td:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.perks {
|
||||
text-align: right;
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RanksComponent } from './ranks.component';
|
||||
|
||||
describe('RanksComponent', () => {
|
||||
let component: RanksComponent;
|
||||
let fixture: ComponentFixture<RanksComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RanksComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RanksComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {HeaderComponent} from "../header/header.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-ranks',
|
||||
imports: [
|
||||
HeaderComponent
|
||||
],
|
||||
templateUrl: './ranks.component.html',
|
||||
styleUrl: './ranks.component.scss'
|
||||
})
|
||||
export class RanksComponent {
|
||||
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
<li>You can talk about current events and real-world topics. However, if staff recognizes the topic is
|
||||
causing players to become uncomfortable, they may shut the conversation down.
|
||||
<ul class="full-page-list-2">
|
||||
<li>You are welcome to take conversations regarding sensitive topics to dm's and party chat with people
|
||||
<li>You are welcome to take conversations regarding sensitive topics to DMs and party chat with people
|
||||
who want to be involved, or off the server.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {LoginService} from '../../api';
|
||||
import {CookieService} from 'ngx-cookie-service';
|
||||
import {BehaviorSubject, Observable, throwError} from 'rxjs';
|
||||
import {catchError, tap} from 'rxjs/operators';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
|
||||
public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
|
||||
|
||||
private userClaimsSubject = new BehaviorSubject<any>(null);
|
||||
public userClaims$ = this.userClaimsSubject.asObservable();
|
||||
|
||||
constructor(
|
||||
private loginService: LoginService,
|
||||
private cookieService: CookieService,
|
||||
private snackBar: MatSnackBar
|
||||
) {
|
||||
// Check if user is already logged in on service initialization
|
||||
this.checkAuthStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to login with the provided code
|
||||
*/
|
||||
public login(code: string): Observable<any> {
|
||||
return this.loginService.login(code).pipe(
|
||||
tap(jwt => {
|
||||
this.saveJwt(jwt as JsonWebKey);
|
||||
this.isAuthenticatedSubject.next(true);
|
||||
}),
|
||||
catchError(error => {
|
||||
this.snackBar.open('Login failed', '', {duration: 2000});
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the user out by removing the JWT
|
||||
*/
|
||||
public logout(): void {
|
||||
this.cookieService.delete('jwt', '/');
|
||||
this.isAuthenticatedSubject.next(false);
|
||||
this.userClaimsSubject.next(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is authenticated
|
||||
*/
|
||||
public checkAuthStatus(): boolean {
|
||||
const jwt = this.getJwt();
|
||||
if (jwt) {
|
||||
try {
|
||||
const claims = this.extractJwtClaims(jwt as JsonWebKey);
|
||||
// Check if token is expired
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
if (claims.exp && claims.exp < currentTime) {
|
||||
this.logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.userClaimsSubject.next(claims);
|
||||
this.isAuthenticatedSubject.next(true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.logout();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JWT from cookies
|
||||
*/
|
||||
public getJwt(): string | null {
|
||||
return this.cookieService.check('jwt') ? this.cookieService.get('jwt') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the JWT to cookies
|
||||
*/
|
||||
private saveJwt(jwt: JsonWebKey): void {
|
||||
this.cookieService.set('jwt', jwt.toString(), {
|
||||
path: '/',
|
||||
secure: true,
|
||||
sameSite: 'Strict'
|
||||
});
|
||||
|
||||
const claims = this.extractJwtClaims(jwt);
|
||||
this.userClaimsSubject.next(claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract claims from JWT
|
||||
*/
|
||||
private extractJwtClaims(jwt: JsonWebKey): any {
|
||||
const token = jwt.toString();
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
return JSON.parse(window.atob(base64));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user authorizations from claims
|
||||
*/
|
||||
public getUserAuthorizations(): string[] {
|
||||
const claims = this.userClaimsSubject.getValue();
|
||||
return claims?.authorizations || [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'skyblock'" height="460px" background_image="/public/img/backgrounds/babywither.png"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Skyblock</h1>
|
||||
<h2>A skyblock server blending vanilla gameplay with unique enhancements for an elevated, community-driven sky
|
||||
adventure.</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Discover Comet</h2>
|
||||
<p>Welcome to Comet, a skyblock server that offers players a unique blend of the features you know and love
|
||||
with a dash of exiciting enhancements to elevate your sky-high adventure.</p>
|
||||
<p>The heart of the skyblock challenge is the same as your traditional experience: start on a small island
|
||||
in the sky with limited resources and aim to expand your domain and conquer the endless void. However,
|
||||
Comet is not your ordinary skyblock experience. We've carefully introduced a selection of enhancements to
|
||||
enrich the gameplay without overwhelming the core vanilla experience that players love and cherish.</p>
|
||||
<p style="padding-bottom: 5px !important;">Some of the key features and enhancements includes:</p>
|
||||
<ul>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Explorable Biomes:</span> All islands are on
|
||||
the same public seed, you can expand your island and explore to find different biomes. Certain mobs only
|
||||
spawn in certain biomes.
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Dynamic Economy:</span> Engage with our
|
||||
vibrant server economy, where you can trade items with other players, visit the spawn shops or even set
|
||||
up your own shops on your island.
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Custom Achievements:</span> Beyond the
|
||||
traditional skyblock objectives, Comet introduces a series of custom achievements which will test your
|
||||
skills and reward you with valuable resources and unique items.
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">Cooperative Play:</span> Team up with friends
|
||||
or make new allies to build your sky empire together.
|
||||
</li>
|
||||
</ul>
|
||||
<p style="padding-top: 10px;">Comet is the perfect experience for those who love skyblock but crave a bit
|
||||
more excitement and community interaction. Whether you're a seasoned player or new to the sky life, Comet
|
||||
provides a rich platform for exploration, creativity and fun.</p>
|
||||
<p>Join Comet today and discover that in the world of skyblock, the sky is just the beginning.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Useful commands</h2>
|
||||
<ul>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/island -</span> Opens a GUI where you can see
|
||||
information about your island, change settings and do upgrades
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/island go -</span> Start a new island or go
|
||||
to your current island
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/island invite <username> -</span>
|
||||
Invite a player to your island
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/island kick <username> -</span> Kick a
|
||||
player from your island
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/island leave -</span> Leave your current
|
||||
island
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/island restart -</span> Reset your island
|
||||
without affecting removing your island members
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/island setowner <username> -</span> Set
|
||||
a new owner of your current island
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/island visit <username> -</span> Visit
|
||||
the specificed player's island
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Cobble Generator Levels</h2>
|
||||
<p><span style="font-family: 'opensans-bold', sans-serif;">Note:</span> Did you mess up your generator and
|
||||
accidentally made an obsidian block? No fear, simply right click the obsidian with your bucket and it will
|
||||
change lava in your bucket!</p>
|
||||
<ul>
|
||||
<p style="padding-bottom: 5px !important;">Level 1, coal</p>
|
||||
<li>Island level 20</li>
|
||||
<li>64 cobblestone</li>
|
||||
<p style="padding-bottom: 5px !important;">Level 2, iron</p>
|
||||
<li>Island level 35</li>
|
||||
<li>128 cobblestone</li>
|
||||
<li>35 coal</li>
|
||||
<p style="padding-bottom: 5px !important;">Level 3, redstone</p>
|
||||
<li>Island level 50</li>
|
||||
<li>192 cobblestone</li>
|
||||
<li>50 iron</li>
|
||||
<p style="padding-bottom: 5px !important;">Level 4, lapis</p>
|
||||
<li>Island level 60</li>
|
||||
<li>256 cobblestone</li>
|
||||
<li>60 redstone</li>
|
||||
<p style="padding-bottom: 5px !important;">Level 5, copper</p>
|
||||
<li>Island level 70</li>
|
||||
<li>320 cobblestone</li>
|
||||
<li>70 lapis</li>
|
||||
<p style="padding-bottom: 5px !important;">Level 6, gold</p>
|
||||
<li>Island level 80</li>
|
||||
<li>384 cobblestone</li>
|
||||
<li>80 copper</li>
|
||||
<p style="padding-bottom: 5px !important;">Level 7, emerald</p>
|
||||
<li>Island level 90</li>
|
||||
<li>448 cobblestone</li>
|
||||
<li>90 gold</li>
|
||||
<p style="padding-bottom: 5px !important;">Level 8, diamond</p>
|
||||
<li>Island level 100</li>
|
||||
<li>512 cobblestone</li>
|
||||
<li>100 emerald</li>
|
||||
<p style="padding-bottom: 5px !important;">Level 9, ancient debris</p>
|
||||
<li>Island level 150</li>
|
||||
<li>576 cobblestone</li>
|
||||
<li>150 diamond</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,9 @@
|
||||
main ul {
|
||||
font-family: opensans, sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
main li {
|
||||
margin-left: 30px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SkyblockComponent } from './skyblock.component';
|
||||
|
||||
describe('SkyblockComponent', () => {
|
||||
let component: SkyblockComponent;
|
||||
let fixture: ComponentFixture<SkyblockComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SkyblockComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SkyblockComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-skyblock',
|
||||
imports: [
|
||||
HeaderComponent
|
||||
],
|
||||
templateUrl: './skyblock.component.html',
|
||||
styleUrl: './skyblock.component.scss'
|
||||
})
|
||||
export class SkyblockComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'staffpowers'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Staff Powers</h1>
|
||||
<h2>Our staff members have numerous powers to assist them in moderating effectively.</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Staff Powers</h2>
|
||||
<p>We believe it's important to be transparent with our community about what our staff can see and do. To do
|
||||
this we've created this web page to explain all the things staff can see/do and why.</p>
|
||||
<p>We store a lot of information, basically everything you do on Altitude is logged. We only use this data
|
||||
to moderate, we will never sell it or give it to anyone who isn't an active staff member. Any misuse of
|
||||
the data we store by staff members results in their immediate removal from the team.</p>
|
||||
<p>This is not a complete list of all powers the staff team has, these are just a list of the commands that
|
||||
could be misinterpreted as giving staff members an advantage or are related to your data.</p>
|
||||
<p>If you have a concern about any of the powers talked about on the rest of this page, or you feel like
|
||||
something is missing. Please get in contact with head staff through the AltitudeBot on Discord or by
|
||||
submitting a contact form with a valid email.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Staff Rooms</h2>
|
||||
<p>Staff rooms are places where staff may need to go to handle a variety of staff-related tasks. We don’t
|
||||
want staff to seem unresponsive when working on a task if a player sees them unmoving on the server
|
||||
somewhere when sorting through logs, coreprotect data, or sending mails to players. Often, secure
|
||||
‘physical’ space is needed by staff to deal with an issue on the server (For example, storing stolen items
|
||||
in one organized place, where all other mods can safely access them so they can be returned to the player
|
||||
who owns them as quickly as possible). Staff meetings also often occur in these rooms so that the server
|
||||
isn’t left unattended during the meeting, while the team can still remain engaged in the meeting.</p>
|
||||
<p>Previously, staff rooms were placed under server spawns for quick access by that server’s staff team.
|
||||
Now, staff rooms have been removed from the survival world and exist in a dimension similar to the nether
|
||||
and the end.<br>
|
||||
Concerns have been raised in the past that this space is not accessible to players. Due to the purpose of
|
||||
the space (solely to handle staff-related tasks/player issues, and host meetings), this space will forever
|
||||
remain inaccessible to players as that access would eliminate the purpose of the space.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Spy (seeing DMs & PartyChat)</h2>
|
||||
<p>Staff members are able to see all private messages and messages sent in party chats. This is vital for
|
||||
protecting our players, we would not be able to moderate effectively without also being able to see
|
||||
private messages.</p>
|
||||
<p>We will never purposely spy on your private conversation for personal gain, the only time we will
|
||||
interfere with these messages is if we see a severely concerning conversation happening; harassment,
|
||||
bullying, suicide/self harm discussion, sexual discussion, etc. We will almost always step in through just
|
||||
a simple DM to check in on things. We will take relevant private messages into account when collecting
|
||||
evidence on a player.</p>
|
||||
<p>Please keep in mind, this does not mean staff are constantly moderating your DM’s in the way they do
|
||||
chat. If you are experiencing an issue in DM’s and need help, please reach out to a member of staff.<br>
|
||||
Altitude staff are <span style="font-family: 'opensans-bold', sans-serif;">not</span> trained mental
|
||||
health professionals, and we are not qualified to counsel through suicide and self harm, but we value our
|
||||
community members deeply and will do our best to give you some official resources that you can use.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Player Data (IPs and logs)</h2>
|
||||
<p>All staff members have access to your IP through /whois and /iphist. They will never use this information
|
||||
outside of moderation, and it will never be shared outside of our staff team. Everytime you visit a place
|
||||
on the internet, your IP is used to share data back to you. If we did not have access to your IP, you
|
||||
would not be able to play on Altitude. The only times we will use your IP for staff purposes is to compare
|
||||
your IP to an account we may think is an alt (this is also why we have a history of your previous IPs
|
||||
you’ve logged in with).</p>
|
||||
<p>Almost every single action taken on Altitude is tracked through some sort of log. We’ve created a very
|
||||
detailed logging system to help ensure that we are always well informed when dealing with different staff
|
||||
scenarios, and these logs help us tremendously. We do not share these logs outside of the staff team and
|
||||
we will never use them for anything unrelated to staff. We will not list everything we log because it’s
|
||||
not necessary and would be too long of a list, but just beware that basically anything you do on Altitude
|
||||
is logged in some way. This is not only to protect Altitude, but you as well.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>CoreProtect</h2>
|
||||
<p>Coreprotect is a log on the server that allows staff to see a variety of interactions on the server- From
|
||||
block deletion to buttons pressed to commands issued. 90% of the time, staff use coreprotect when dealing
|
||||
with issues related to blocks/entities, such as griefing and theft. Staff may use coreprotect in
|
||||
conjunction with our database logs if a situation is bigger than coreprotect can support us with. As a
|
||||
result, staff may take a bit of time when investigating a player issue due to the searching they must do
|
||||
with coreprotect, especially if there is a lot of related data to search through!</p>
|
||||
<p>Many servers use coreprotect as it is the most reliable source of this information, is well organized and
|
||||
is extremely well documented. Coreprotect data is considered confidential, and staff will <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">not</span> release or share any coreprotect data with
|
||||
players. The only exception to this rule is cases of theft under trust - where staff will share the name
|
||||
of the thief with the claim owner for the purpose of them being removed from their claimlist and not being
|
||||
re-trusted in the future.</p>
|
||||
<p>Coreprotect also allows us to fix some serious problems using a tool called <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">Rollback</span>. Rollback allows staff to reverse the
|
||||
interactions done with blocks/entities/containers within a selected period of time, and helps us fix
|
||||
things quickly for you after griefing occurs!</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Head Staff</h2>
|
||||
<p>Head staff (everyone above mod) have access to a lot of things depending on their exact job. The most
|
||||
relevant things they have access to are: creative, worldedit, and invload. These powers can be used to
|
||||
restore items we can't properly restore with coreprotect and remove/move/restore builds as needed.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/fly</h3>
|
||||
<p>Fly allows staff members to freely do their job without having to worry about being limited to just the
|
||||
ground. There are a lot of instances where they need to be able to check out something high off the ground
|
||||
or they just need to be able to travel more efficiently, and flying allows them to do this. We allow our
|
||||
staff members to use fly for their own personal builds, sort of as a thank you and to help make up for the
|
||||
time they lose through all of their moderation. We do not want any staff members to be openly flying around
|
||||
in front of players, and they will never use it for anything other than building (looting, exploring, etc.
|
||||
is not allowed).</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/god</h3>
|
||||
<p>God mode allows staff members to work on something in game without being physically disturbed or harmed.
|
||||
This is a tool that is only ever used for moderation purposes, to allow staff to focus on their job without
|
||||
being interrupted by outside elements.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/ignoreclaims</h3>
|
||||
<p>The ignore claims command is extremely helpful when a staff member is tracking down stolen items, checking
|
||||
trustlists, helping players with other miscellaneous issues, etc. This command is very powerful and all of
|
||||
our staff members acknowledge this, it will never be used outside of a staff related incident.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">CoreProtect</h3>
|
||||
<p>Coreprotect is used to track and view information on block/item/etc placements and gives us the ability to
|
||||
restore these actions. This is only used to fix and track damage done by stealing/griefing/etc.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/vanish</h3>
|
||||
<p>Vanish is used to check on what players are doing. This allows us to ensure all players are following the
|
||||
rules. We hide ourselves while doing so to avoid disrupting the player experience.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/patrol</h3>
|
||||
<p>This command allows mods to teleport to each player one by one without missing anyone. This helps staff to
|
||||
moderate all players equally and efficiently.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/invsee - /ec</h3>
|
||||
<p>These commands allow the inventory/enderchest of a player to be viewed and edited. This is only used in
|
||||
situations where a player breaks a rule by taking items from someone else and refuses to return them.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/whois - /whowas</h3>
|
||||
<p>Whois is a command that shows a lot of player information, namely IP, UUID, player balance, number of
|
||||
homes, etc. Your IP is kept private and will never be used maliciously, and your UUID is public information.
|
||||
This command will never be used for anything outside of moderating purposes. Whowas is a command that lists
|
||||
all previous usernames of a specific player, as well as your UUID again. This information is public
|
||||
information that can easily be accessed through a number of different websites.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/flag</h3>
|
||||
<p>We use flags as an internal tool to inform other staff members of things a player has done, these are
|
||||
usually in the form of short notes.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/alts - /iphist</h3>
|
||||
<p>These commands can be used to show if a player has other players on the same ip as them. This is necessary
|
||||
to make it harder to bypass punishments. Your ip history is kept private and will never be shared outside of
|
||||
staff related incidents.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/spy</h3>
|
||||
<p>All staff usually have /spy on, this allows them to see all conversations in DM's and party chat. This is
|
||||
important to ensure people aren't using our server to harass players without staff being able to see and
|
||||
respond to it.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/cuff</h3>
|
||||
<p>This command is used when a player is being uncooperative or not listening to staff after being instructed
|
||||
to do something. When a player is cuffed, they cannot move, they cannot teleport, etc., but they can still
|
||||
chat or message players. This is mainly used in cases where a player is attempting to interfere with a staff
|
||||
situation (Ex: theft). This command will only be used for moderation purposes.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/afklist</h3>
|
||||
<p>This command lists everyone who has been marked as AFK by our plugin. It also notifies us if a player gets
|
||||
marked as AFK. This is used to check if a player isn't going AFK near farms which might give them an
|
||||
advantage over other players.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/heal - /feed</h3>
|
||||
<p>These commands are only used during moderation to deal with unexpected situations where a player might need
|
||||
to be healed. They are not used by any staff member while playing on the server.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/tp - /tphere - /spawn</h3>
|
||||
<p>These commands are used to get a player away from a situation that requires staff intervention such as
|
||||
attempted player killing or if they are in a place they aren't allowed to be in anymore. Tp can be used
|
||||
while in vanish to check on a player or to easily and quickly get to a player who's asking for help.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px;">/cmi invlist</h3>
|
||||
<p>This command is used to see all of a player's recent deaths. It allows us to see your death inventory, your
|
||||
levels, the time of your death, location, cause, etc.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h3 style="padding-top: 15px; padding-bottom: 0 !important;">Ability to see emails during appeals, contact
|
||||
forms & staff applications</h3>
|
||||
<p style="font-size: 0.75em;">(Head-Staff only)</p>
|
||||
<p>When you send in a ban appeal, a contact form, or a staff application, we will be given your email (if you
|
||||
send in an anonymous contact form we cannot see your email). We will never use your email for anything
|
||||
besides replying. Head-staff are the only staff members who are able to see your email.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StaffpowersComponent } from './staffpowers.component';
|
||||
|
||||
describe('StaffpowersComponent', () => {
|
||||
let component: StaffpowersComponent;
|
||||
let fixture: ComponentFixture<StaffpowersComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [StaffpowersComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(StaffpowersComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {HeaderComponent} from "../header/header.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-staffpowers',
|
||||
imports: [
|
||||
HeaderComponent
|
||||
],
|
||||
templateUrl: './staffpowers.component.html',
|
||||
styleUrl: './staffpowers.component.scss'
|
||||
})
|
||||
export class StaffpowersComponent {
|
||||
|
||||
}
|
||||
@@ -44,7 +44,7 @@
|
||||
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||
height="160" width="160" style="width: 160px;">
|
||||
<h2>{{ member.name }}</h2>
|
||||
<p>Admin</p>
|
||||
<p>Head Mod</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'economy'" height="460px" background_image="/public/img/backgrounds/path.jpg"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Player Warps</h1>
|
||||
<h2>Set up your own public warp to let everyone visit your town, shop, and more!</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>What are Warps?</h2>
|
||||
<p>Warps allow everyone to teleport to a specific place within the world. They work just like homes
|
||||
(/sethome), but everyone has access to the warps. We are very excited to have our own custom warp system
|
||||
that allows players to easily apply for their own warp wherever they want and maintain their warp (the
|
||||
name, description, and icon) on their own! This lets you easily promote your town, shop, farm, or just
|
||||
about anything else.</p>
|
||||
<p>All warps are separated into categories so it's easy to find the category of warp you're looking for.</p>
|
||||
<img ngSrc="/public/img/random/warpgui.png"
|
||||
alt="In-game warp GUI"
|
||||
style="width: 80%; padding-bottom: 15px;"
|
||||
height="232" width="356">
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Applying for a Warp</h2>
|
||||
<p>Once you've built something that you want to share, and made sure that it meets the "Warp Requirements",
|
||||
you can apply for a warp in-game! Applying is easy, just do <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/warps apply</span> and follow the prompts.</p>
|
||||
<p>The application process will ask you for a name, description, icon, category, and location for your warp.
|
||||
Before beginning the application process, be sure you are standing where you want the warp to be! Make
|
||||
sure you are also facing the direction you want players to face when they teleport to your warp. Don't
|
||||
look at the ground! Your warp name must be under 30 characters and your description must be under 120. Do
|
||||
not use any color codes. When it asks for the item you want to have as an icon, simply hold the desired
|
||||
item in your hand.</p>
|
||||
<p><span style="font-family: 'opensans-bold', sans-serif;">Attention</span>: Warps cost $25000. If your
|
||||
application is denied, you will be refunded $15000. To continue, type continue, or to cancel, type cancel.
|
||||
However, if your warp is approved, but you fail to maintain it, your warp will be deleted without a
|
||||
refund!</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="columnSection" style="padding-top: 0;">
|
||||
<div class="columnParagraph" style="padding-left: 15px;">
|
||||
<h2>Warp Requirements</h2>
|
||||
<p>You need to be the owner of the claim your warp is placed in. It should look good, and be as finished as
|
||||
your warp type allows you to have it. Safety is an important aspect as well, visitors should not be
|
||||
accidently dying by falling in dangerous areas or having mobs spawn on them. Phantoms are an exception to
|
||||
this rule as you can't easily prevent those spawns. Your warp should be in one claim*, and divided with
|
||||
subclaims if you need to allow different levels of trust in different areas (house plots in towns, shop
|
||||
plots in malls, etc).</p>
|
||||
<p><sub>*If a warp cannot be done in one claim due to surrounding claims, you are allowed to use up to 3
|
||||
separate claims to claim everything. If you can reduce the total size of your claim by at least roughly 30%
|
||||
by using an extra claim, you may do so for up to a total of 3 claims.</sub></p>
|
||||
<p>It should be easy for players to find where they want to go. You can accomplish this through signs or by
|
||||
designing it in such a way that players can see their destination when they warp in. Your warp's claim
|
||||
border should be 500 blocks away from any other warp's claim border, and two warps can not lead to the same
|
||||
area. We encourage making it possible for players to leave the warp, either by walking or elytra use, so
|
||||
that they can use it as a starting point for exploring as well.</p>
|
||||
<p>If you have any farms in the area of your warp that players don't benefit from through your warp it should
|
||||
not be possible for players to load those farms during regular use. We don't want players being used as
|
||||
chunkloaders.</p>
|
||||
<p>It's important to make sure your warp is as lag friendly as possible. Our rules on lag can be found here on
|
||||
the <a [routerLink]="['/lag']">lag</a> page. If we find your warp creates more lag than necessary for its
|
||||
function
|
||||
we will deny the application.</p>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h3>Towns</h3>
|
||||
<span
|
||||
style="font-family: 'opensans-bold', sans-serif;">Any warp that is designed to house other players</span>
|
||||
<ul>
|
||||
<li>It should be clear how to navigate the town and how to get a plot</li>
|
||||
<li>You need to be active enough to regularly assign players new plots</li>
|
||||
<li>Your town must have a community that actively participates in your town by living/building there or
|
||||
using your towns features both before applying and while it's a warp (Minimum 3 active players other
|
||||
than you)
|
||||
</li>
|
||||
<li>Players should be able to leave the town area in order to use the warp as a travel hub</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h3>Shops</h3>
|
||||
<span style="font-family: 'opensans-bold', sans-serif;">Any warp that sells/trades/buys items using shops. This also includes malls.</span>
|
||||
<ul>
|
||||
<li>It should be clear where to go to find the items that are being sold from the warp spawn point</li>
|
||||
<li>Your shops should sell items players want and aren’t readily available elsewhere</li>
|
||||
<li>Your prices should be competitive with spawn and other warps</li>
|
||||
<li>You are responsible for keeping stock up. Staff will regularly check random shops within your warp, if
|
||||
they find understocked shops they will issue warnings for it and eventually remove it. This counts for
|
||||
mall owners as well!
|
||||
</li>
|
||||
<li>Players should be able to leave the area in order to use the warp as a travel hub</li>
|
||||
</ul>
|
||||
<span style="font-family: 'opensans-bold', sans-serif;">General stock requirements</span>
|
||||
<ul>
|
||||
<li>Easily obtainable items such as concrete: full chest</li>
|
||||
<li>Hard to obtain, common items such as diamonds: 1 stack</li>
|
||||
<li>Hard to obtain, uncommon items such as netherite/beacons: 16</li>
|
||||
<li>Maps: 16 for 1x1, 8 for anything over 1x1 up to 2x2, and 4 for anything over 2x2</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Warp Notes</h2>
|
||||
<p><span style="font-family: 'opensans-bold', sans-serif;">-</span> If a new warp application has
|
||||
improvements or additional, useful features that a current warp lacks, the current warp may be replaced
|
||||
with the new one. For example, if there already is a single-spawner spider farm that offers drops at a
|
||||
certain price, and a warp application is submitted for a spider farm with multiple spawners and offers
|
||||
free drops, the new warp is likely to be favored.</p>
|
||||
<p><span style="font-family: 'opensans-bold', sans-serif;">-</span> Warps that are malls (primarily focused
|
||||
on having many shop plots for many players) are limited to 2.</p>
|
||||
<p><span style="font-family: 'opensans-bold', sans-serif;">-</span> Smaller shop warps are limited to 1 shop
|
||||
per type. For example, only 1 warp for redstone-related shops.</p>
|
||||
<p><span style="font-family: 'opensans-bold', sans-serif;">-</span> In order to always keep our warps fair
|
||||
and updated, we will automatically remove a warp if the warp owner has been offline for 30+ days. We need
|
||||
to be able to contact our warp owners in a timely manner incase any issues with the warp happen to pop up.
|
||||
If we notice that a player is fairly inactive and another warp application comes in for a similar warp,
|
||||
the application will be more likely to be accepted if the warp meets all of our requirements. If you plan
|
||||
on being gone for an extended period of time, 14+ days, you can let a staff member know and they will note
|
||||
it down and try to work with you.</p>
|
||||
<p><span style="font-family: 'opensans-bold', sans-serif;">-</span> The staff team does weekly warp checks
|
||||
to make sure our warps continue to stay up to meet our requirements. They will mail a warp owner if they
|
||||
notice an issue (low stock, sudden lighting issue, farm isn't working, etc.), and we expect all issues to
|
||||
be fixed within a week of receiving that mail. If an issue goes unfixed, we will remove the warp due to
|
||||
the lack of upkeep. If the issue is something you need help fixing or you're unable to work on it during
|
||||
the week, you can reach out to any member of the staff team and they will work with you!</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Useful Commands</h2>
|
||||
<ul>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/warps -</span> Open a GUI showing all warps
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/warps apply -</span> Apply for your own warp
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h3>Farms</h3>
|
||||
<span style="font-family: 'opensans-bold', sans-serif;">Any warps that give XP</span>
|
||||
<ul>
|
||||
<li>It should be easy to find where to go and how to use the farm</li>
|
||||
<li>Players should have access to all of the drops either through shops with competitive prices, or for
|
||||
free
|
||||
</li>
|
||||
<li>XP farms should not attempt to bypass our anti lag systems</li>
|
||||
<li>Kill chambers should be 1x1 or 1x2, exceptions to this rule are listed below</li>
|
||||
<li>Amount of farms allowed as warps:
|
||||
<ul>
|
||||
<li>Skeleton/spider - 1 each</li>
|
||||
<li>Zombie/drowned - 1</li>
|
||||
<li>Bad Omen - 1</li>
|
||||
<li>Raid - 2</li>
|
||||
<li>Guardian - 2</li>
|
||||
<li>Endermen - 2</li>
|
||||
<li>Creeper - 2</li>
|
||||
<li>Shulker - 2</li>
|
||||
<li>Wither skeleton - 2</li>
|
||||
</ul>
|
||||
</li>
|
||||
<p>Exceptions can be made for this rule if the farms have different, unique designs (such as a one-player
|
||||
vs two-player farm). Head Staff will approve these exceptions on a case-by-case basis</p>
|
||||
</ul>
|
||||
<span style="font-family: 'opensans-bold', sans-serif;">Kill chamber size exceptions</span>
|
||||
<ul>
|
||||
<li>Magma kill chamber may be up to 3x3</li>
|
||||
<li>Hoglin kill chamber may be up to 2x2</li>
|
||||
<li>Ravagers during pillager raids must be killed automatically and the kill chamber should still be 2x1
|
||||
for the rest of the mobs
|
||||
</li>
|
||||
<li>Enderman kill chamber may be up to 3x3</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h3>Other</h3>
|
||||
<span style="font-family: 'opensans-bold', sans-serif;">Any warp that doesn't fit in the other categories can go in here</span>
|
||||
<ul>
|
||||
<li>Warps designed for player events need to look aesthetically pleasing and accommodate a large amount of
|
||||
players
|
||||
</li>
|
||||
<li>Casino warps should, at a minimum, display or list the prizes that can be won from each game
|
||||
<ul>
|
||||
<li style="padding-top: 2px;">If the game allows you to win crate items, listing the rarity of prizes
|
||||
that can be won is acceptable
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Casino warps should have a disclaimer posted visibly that tells players that they can lose money and
|
||||
should expect to lose money playing any gambling machines
|
||||
</li>
|
||||
<li>Portal warps need to be the only one of its kind</li>
|
||||
<li>Villager trading areas have their villagers on no AI blocks and be named public for public
|
||||
accessibility
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Maintaining a Warp</h2>
|
||||
<p>We've made it easy for you to maintain your warp on your own. As the warp owner you can change the name,
|
||||
description, and icon anytime. There is no charge for modifying a warp. To access the GUI to manage your
|
||||
warp, just do <span style="font-family: 'opensans-bold', sans-serif;">/warps</span> and click on the chest
|
||||
labeled "<span style="font-family: 'opensans-bold', sans-serif;">My Warps</span>" in the bottom left
|
||||
corner.</p>
|
||||
<img ngSrc="/public/img/random/editwarpgui.png"
|
||||
alt="In-game warp edit GUI"
|
||||
style="width: 80%;"
|
||||
width="384" height="170">
|
||||
<p>Maintaining a warp also involves making sure it looks nice, shops are well stocked, and, if it's a town,
|
||||
open plots are always available for residents to move in. Make sure you keep up on maintaining your warp
|
||||
or it could be deleted! If a warp is deleted by a staff member you will not receive a refund for the
|
||||
creation cost.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user