Rework folder structure in frontend
Pages are now grouped per group they appear in on in the header (where possible) Utilities used by multiple pages in the project are grouped in folders such as services/pipes/etc
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'about'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>About Us</h1>
|
||||
<h2>We are a simple server born out of nostalgia, and working to maintain a semi-vanilla feel today's Minecraft
|
||||
world.</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Our Concept</h2>
|
||||
<p>You could say it all started with a bit of nostalgia. Altitude was inspired by a small server back in
|
||||
2012. It was a simple survival and creative server with a community that felt like family, and when the
|
||||
server closed we took that concept and created Altitude.</p>
|
||||
<p>From the very beginning we've been all about our community - adding features suggested by our members,
|
||||
and designing a server that encourages cooperation and playing together. On Altitude, it's about coming
|
||||
together as a community and creating new adventures every day.</p>
|
||||
<p>We want to be the server you call home and always feel welcome to come on and relax for a while.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Our Hardware</h2>
|
||||
<p>As the community has grown and evolved so has the server. We started with a little desktop PC running in
|
||||
the basement and have expanded through the years from Minecraft hosting, to a VPS, and finally to
|
||||
dedicated servers hosted by OVH in Canada.</p>
|
||||
<p>Today we run on a VPS and a gaming server from OVH with <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">32GB of DDR4 memory</span>, an <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">Intel i7 6700k</span> processor, <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">2x 500GB SSD drives</span> and <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">500Mbps bandwidth</span>. The VPS serves as our proxy
|
||||
and website, while the gaming server hosts our Minecraft servers and SQL databases.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Our History</h2>
|
||||
<p>Altitude began as a private server to get together with friends and it quickly became tradition to come
|
||||
together every afternoon. Eventually we wanted to share our adventures with others, and went public,
|
||||
inviting people from all around the globe to be a part of the community they had formed.</p>
|
||||
<p>In the beginning, the server was named "The Underground", and was a small hangout spot mostly spread by
|
||||
word of mouth. A few months after going public we renamed ourselves to "Altitude" and have kept the name
|
||||
ever since. Altitude has evolved quite a bit over the years, but the concept has never changed - we are
|
||||
home.</p>
|
||||
<p>Today, Altitude thrives on the community that has formed. Many great people have decided to call Altitude
|
||||
home, and many more people come every day. We are constantly learning more about what makes Altitude
|
||||
great, and what we can do to make it even greater.</p>
|
||||
<p>We hope you will join us on this adventure and learn with us as we embark on this amazing journey!</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AboutComponent } from './about.component';
|
||||
|
||||
describe('AboutComponent', () => {
|
||||
let component: AboutComponent;
|
||||
let fixture: ComponentFixture<AboutComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AboutComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AboutComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent
|
||||
],
|
||||
templateUrl: './about.component.html',
|
||||
styleUrl: './about.component.scss'
|
||||
})
|
||||
export class AboutComponent {
|
||||
constructor(public scrollService: ScrollService) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'birthdays'" height="460px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Famous Altitude Staff Birthdays</h1>
|
||||
<h2>(and previous staff)</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,14 @@
|
||||
.birthdayMember {
|
||||
width: 33%;
|
||||
min-width: 250px;
|
||||
padding-bottom: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.birthdayMember img {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.birthdayMember p {
|
||||
font-family: 'opensans-bold', sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BirthdaysComponent } from './birthdays.component';
|
||||
|
||||
describe('BirthdaysComponent', () => {
|
||||
let component: BirthdaysComponent;
|
||||
let fixture: ComponentFixture<BirthdaysComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [BirthdaysComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BirthdaysComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-birthdays',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent
|
||||
],
|
||||
templateUrl: './birthdays.component.html',
|
||||
styleUrl: './birthdays.component.scss'
|
||||
})
|
||||
export class BirthdaysComponent {
|
||||
constructor(public scrollService: ScrollService) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'socials'" height="300px" background_image="/public/img/backgrounds/trees.jpg"
|
||||
[overlay_gradient]="0.5">>
|
||||
<div class="title" header-content>
|
||||
<h1 class="titleColor">Altitude Socials</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<div class="socialContainer" style="padding-top: 50px;">
|
||||
<a href="https://discord.com/invite/alttd">
|
||||
<img class="socialIcon" ngSrc="/public/img/logos/discordsocial.png"
|
||||
title="discord.gg/alttd" alt="Altitude Discord" height="150"
|
||||
width="150" style="width: 150px;">
|
||||
</a>
|
||||
<a href="https://www.tiktok.com/@alttdmc">
|
||||
<img class="socialIcon" ngSrc="/public/img/logos/tiktoksocial.png"
|
||||
title="alttdmc" alt="Altitude TikTok" height="150" width="150"
|
||||
style="width: 150px;">
|
||||
</a>
|
||||
</div>
|
||||
<div class="socialContainer">
|
||||
<a href="https://www.instagram.com/alttdmc/">
|
||||
<img class="socialIcon"
|
||||
ngSrc="/public/img/logos/instagramsocial.png"
|
||||
title="alttdmc" alt="Altitude Instagram" height="150"
|
||||
width="150" style="width: 150px;">
|
||||
</a>
|
||||
<a href="https://twitter.com/alttdmc">
|
||||
<img class="socialIcon" ngSrc="/public/img/logos/twittersocial.png"
|
||||
title="alttdmc" alt="Altitude Twitter. Yes, Twitter. Not X."
|
||||
height="150" width="150"
|
||||
style="width: 150px;">
|
||||
</a>
|
||||
</div>
|
||||
<div class="socialContainer" style="padding-bottom: 50px;">
|
||||
<a href="https://www.youtube.com/c/alttd">
|
||||
<img class="socialIcon" ngSrc="/public/img/logos/youtubesocial.png"
|
||||
title="Altitude Community" alt="Altitude YouTube" height="150"
|
||||
width="150" style="width: 150px;">
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,32 @@
|
||||
main .socialContainer {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.socialContainer {
|
||||
width: 80%;
|
||||
max-width: 1020px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.socialIcon {
|
||||
max-width: 150px;
|
||||
width: 100%;
|
||||
padding: 0 50px 0 50px;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.socialIcon:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
//Different resolution changes
|
||||
@media (max-width: 500px) {
|
||||
.socialIcon {
|
||||
max-width: 120px;
|
||||
width: 100%;
|
||||
padding: 0 20px 0 20px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SocialsComponent } from './socials.component';
|
||||
|
||||
describe('SocialsComponent', () => {
|
||||
let component: SocialsComponent;
|
||||
let fixture: ComponentFixture<SocialsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SocialsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SocialsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-socials',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent,
|
||||
NgOptimizedImage
|
||||
],
|
||||
templateUrl: './socials.component.html',
|
||||
styleUrl: './socials.component.scss'
|
||||
})
|
||||
export class SocialsComponent {
|
||||
constructor(public scrollService: ScrollService) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'team'" height="450px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">>
|
||||
<div class="title" header-content>
|
||||
<h1>Staffing Team</h1>
|
||||
<h2>The team that makes Altitude happen. Your owners, admins, moderators, and trainees are all working together to
|
||||
create Altitude, to create home. This is where the magic happens.</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container teamContainer">
|
||||
<h2 class="sectionTitle">Management</h2>
|
||||
<div *ngFor="let member of getTeamMembers('owner') | async" class="member">
|
||||
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||
height="160" width="160" style="width: 160px;">
|
||||
<h2>{{ member.name }}</h2>
|
||||
<p>Owner</p>
|
||||
</div>
|
||||
<div *ngFor="let member of getTeamMembers('manager') | async" class="member">
|
||||
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||
height="160" width="160" style="width: 160px;">
|
||||
<h2>{{ member.name }}</h2>
|
||||
<p>Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container teamContainer">
|
||||
<h2 class="sectionTitle">Admins</h2>
|
||||
<div *ngFor="let member of getTeamMembers('admin') | async" class="member">
|
||||
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||
height="160" width="160" style="width: 160px;">
|
||||
<h2>{{ member.name }}</h2>
|
||||
<p>Admin</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container teamContainer">
|
||||
<h2 class="sectionTitle">Head Moderators</h2>
|
||||
<div *ngFor="let member of getTeamMembers('headmod') | async" class="member">
|
||||
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||
height="160" width="160" style="width: 160px;">
|
||||
<h2>{{ member.name }}</h2>
|
||||
<p>Head Mod</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container teamContainer">
|
||||
<h2 class="sectionTitle">Moderators</h2>
|
||||
<div *ngFor="let member of getTeamMembers('moderator') | async" class="member">
|
||||
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||
height="160" width="160" style="width: 160px;">
|
||||
<h2>{{ member.name }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section *ngIf="(getTeamMembers('trainee') | async)?.length ?? 0 > 0" class="darkmodeSection">
|
||||
<div class="container teamContainer">
|
||||
<h2 class="sectionTitle">Trainees</h2>
|
||||
<div *ngFor="let member of getTeamMembers('trainee') | async" class="member">
|
||||
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||
height="160" width="160" style="width: 160px;">
|
||||
<h2>{{ member.name }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,26 @@
|
||||
.sectionTitle {
|
||||
flex: 0 0 100%;
|
||||
text-align: center;
|
||||
padding-bottom: 20px;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.member {
|
||||
width: 33%;
|
||||
min-width: 250px;
|
||||
padding-bottom: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.member img {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.member p {
|
||||
font-family: 'opensans-bold', sans-serif;
|
||||
}
|
||||
|
||||
.teamContainer {
|
||||
padding: 50px 0 0 0;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TeamComponent } from './team.component';
|
||||
|
||||
describe('TeamComponent', () => {
|
||||
let component: TeamComponent;
|
||||
let fixture: ComponentFixture<TeamComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TeamComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TeamComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
import {BASE_PATH, Player, TeamService} from '@api';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {CookieService} from 'ngx-cookie-service';
|
||||
import {map, Observable, shareReplay} from 'rxjs';
|
||||
import {environment} from '@environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-team',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent,
|
||||
NgOptimizedImage
|
||||
],
|
||||
providers: [
|
||||
CookieService,
|
||||
{provide: BASE_PATH, useValue: environment.apiUrl}
|
||||
],
|
||||
|
||||
templateUrl: './team.component.html',
|
||||
styleUrl: './team.component.scss'
|
||||
})
|
||||
export class TeamComponent {
|
||||
|
||||
private teamMembersCache: { [key: string]: Observable<Player[]> } = {};
|
||||
|
||||
constructor(public scrollService: ScrollService, public teamApi: TeamService) {
|
||||
}
|
||||
|
||||
public getTeamMembers(team: string): Observable<Player[]> {
|
||||
if (!this.teamMembersCache[team]) {
|
||||
this.teamMembersCache[team] = this.teamApi.getTeamMembers(team).pipe(
|
||||
map(res => this.removeDuplicates(res)),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
return this.teamMembersCache[team];
|
||||
}
|
||||
|
||||
private removeDuplicates(array: Player[]): Player[] {
|
||||
return array.filter((player, index, self) =>
|
||||
index === self.findIndex((p) => p.uuid === player.uuid)
|
||||
);
|
||||
}
|
||||
|
||||
public getAvatarUrl(entry: Player): string {
|
||||
let uuid = entry.uuid.replace('-', '');
|
||||
return `https://crafatar.com/avatars/${uuid}?overlay`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'claiming'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Claiming</h1>
|
||||
<h2>Claiming allows you to protect your items and builds from other players.</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Getting Started</h2>
|
||||
<p>You can “claim” land on Altitude to protect your builds. When you claim a section of land, other players
|
||||
will be unable to place or break blocks, kill animals, take crops, or open chests. Everything will be
|
||||
completely protected!</p>
|
||||
<p>To claim land, you will need some basic tools: a golden shovel, and a stick. You can craft these items
|
||||
yourself or do <b>/claim</b> to receive them for free. The shovel is used for modifying claims and the
|
||||
stick is used for viewing claim information.</p>
|
||||
<img ngSrc="/public/img/items/shovel.png" alt="Minecraft golden shovel" style="width: 25%;" height="114"
|
||||
width="114">
|
||||
<p>Players start with an allowance of 500 “claim blocks” and can purchase additional “claim blocks” with
|
||||
in-game currency using <span style="font-family: 'opensans-bold', sans-serif;">/buyclaimblocks</span>. How
|
||||
many claims you can create depends on the rank you have - the amount can be found on the <a
|
||||
[routerLink]="['/ranks']">ranks</a> page. Additionally, there is also a limit on how many blocks in
|
||||
total you can
|
||||
claim.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Creating a Claim</h2>
|
||||
<p>Before creating any claim, we should check if there are other claims by other players nearby. Claims by
|
||||
different people should be at least 100 blocks apart unless both parties discuss otherwise. To do this,
|
||||
hold a stick in your hand and <b>shift + right click</b> to visualize all nearby claims. Claims are
|
||||
visualized using dotted lines of gold blocks, so look around for any dotted lines on the ground around
|
||||
you.</p>
|
||||
<p>Once you've confirmed that there are no claims too close, we can begin claiming! A claim doesn't go down
|
||||
all the way. The lowest point of the claim starts at 5 blocks below the lower corner you made. It'll keep
|
||||
expanding down as you place blocks lower and lower in the claim, but it may reset when you change the
|
||||
claim size, trust, subclaims etc. If you want to make sure your claim stays extended all the way down,
|
||||
make one of the initial corners at bedrock level. Remember to not claim further down than you need to.</p>
|
||||
<p>If a claim is within 100 blocks you will be unable to create your claim. If the original claim owner is
|
||||
fine with you having a claim closer than the 100 blocks, then they can trust you using <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/claimnearbytrust</span>. You will need to be trusted
|
||||
in order to claim and resize. The trust expires on every server reboot, this is to remind you that there
|
||||
are nearby claims.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Automatic Claim Deletion</h2>
|
||||
<p>When a player has been inactive for 60 consecutive days, on a server (not cross-server), their claim(s)
|
||||
on that server will be transferred into an admin claim(s). The claim will be in that state for 24 hours
|
||||
after which is will be fully unclaimed. During the time the claim is an admin claim everyone will be
|
||||
trusted. This means that you will be able to raid the claim, but not claim it. If you wish to claim the
|
||||
build you will have to return 24 hours later.</p>
|
||||
<p>If you are planning on being gone this long, all you need to do is join for a couple of minutes within
|
||||
those 60 days to reset the timer. Unclaimed land is fair game to loot, so make sure your claims are active
|
||||
and properly sized!</p>
|
||||
<p>Any containers (i.e shulker, chest) that are placed within an active raid-claim are fair game - this also
|
||||
includes containers that raiders place. This means that items a raider puts into their personal shulker
|
||||
aren't safe while placed down or when on the ground as an item after being broken, as anyone that comes by
|
||||
can take it.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Useful Commands</h2>
|
||||
<ul>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/buyclaimblocks -</span> Buy more claim
|
||||
blocks
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/sellclaimblocks -</span> Sell claim blocks
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/unclaim -</span> Remove a protection</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/claimslist -</span> List all of your
|
||||
protections
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/expandclaim <number> -</span> Expand
|
||||
the claim the direction you're facing (can also be used with negative numbers to downsize the claim)
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/containertrust <player> -</span> Give
|
||||
someone access to chests in your claim
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/permissiontrust <player> -</span> Give
|
||||
someone the ability to trust and untrust others in your claim
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/accesstrust <player> -</span> Give
|
||||
someone access to doors and trapdoors in your claim
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/subdivide -</span> Create claims within your
|
||||
main claim that you can trust people to
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/restrictsubclaim -</span> Sets subclaim NOT
|
||||
to inherit perms from the parent claim
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/trust <player> -</span> Allow someone
|
||||
to build and use chests within your claim
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/trustlist -</span> See who is trusted in your
|
||||
claim
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/untrust <player> -</span> Remove a
|
||||
player from your protection
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/kickfromclaim <player> -</span> Kick a
|
||||
player outside of your claim
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/petkick -</span> Enter pet kicking mode
|
||||
(right click pets you don't want in your claim)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Resizing Your Claim</h2>
|
||||
<p>You can make your claim bigger or smaller any time by using a golden shovel. The process is similar to
|
||||
creating the claim. To make things easier, start by right clicking within the claim with a stick. This
|
||||
will show the border of the claim so you can see where the corners are.</p>
|
||||
<p>To resize a claim, right click a corner you would like to move. Then, right click again wherever you
|
||||
would like to move that corner to. To make things easier, you don't need to right click the actual block,
|
||||
you can just point your crosshair at the block and right click from a distance.</p>
|
||||
<p>If you don't have the needed amount of claim blocks to expand (or create) your claim, you will need to
|
||||
purchase additional claim blocks. The price for purchasing claim blocks is different depending on how many
|
||||
you have:</p>
|
||||
<ul>
|
||||
<li>0-10,000 blocks = $0.50/block</li>
|
||||
<li>10,000-50,000 blocks = $0.75/block</li>
|
||||
<li>50,000-300,000 blocks = $1.00/block</li>
|
||||
<li>300,000-1,000,000 blocks = $2.00/block</li>
|
||||
<li>1,000,000+ blocks = $5.00/block</li>
|
||||
</ul>
|
||||
<p>If you have left over claim blocks you can sell them using <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/sellclaimblocks</span>. The return will always be
|
||||
$0.50/block. This means that you need to be mindful of purchasing large amounts of claim blocks; don't
|
||||
purchase them unless you actually need them.</p>
|
||||
<img ngSrc="/public/img/backgrounds/claim.png" alt="Screenshot showing claim border in-game"
|
||||
style="width: 100%;"
|
||||
width="480" height="250">
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Subdividing Claims</h2>
|
||||
<p>Subdividing claims allows you to give players access to specific parts of your claim. This is the best
|
||||
way to manage a town or shop!</p>
|
||||
<p>The idea behind subdivisions is easy. You are creating mini-claims within your main claim so that you can
|
||||
trust people to certain areas, but not the entire claim. You are still in control of the border of these
|
||||
mini-claims (size and location). Once a subdivision is created you can trust people the same way as
|
||||
before. Just stand in the mini-claim and use the trust commands. It will only trust them to the
|
||||
mini-claim. (To trust someone to your main claim you will need to stand outside of any mini-claims you've
|
||||
created.)</p>
|
||||
<p>To create a subdivision you need to go into "subdivision mode". You can access this mode with <b>/subdivideclaims</b>.
|
||||
Once you are in this mode you can create the subdivisions the same way you would make a regular claim,
|
||||
just right click two corners. Subdivisions will be outlined with a white dotted line. To leave subdivision
|
||||
mode use <b>/basicclaims</b>.</p>
|
||||
<img ngSrc="/public/img/backgrounds/townsubdivisions.png" alt="Screenshot of town subdivided"
|
||||
style="width: 100%;"
|
||||
width="480" height="250">
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Trusting Others</h2>
|
||||
<p>You can add other players to your protected area with various levels of access. To trust everyone on the
|
||||
server replace <player> with "all". This works with all of the commands below.</p>
|
||||
<ul>
|
||||
<li>To give someone full access to your claim (break, build, access chests, etc) do: <b>/trust <player></b>
|
||||
</li>
|
||||
<li>To give someone just the ability to access storage items, but NOT build, do: <b>/containertrust <player></b>
|
||||
</li>
|
||||
<li>To give someone visitor access (opening the doors, but not take anything or build anything) do: <b>/accesstrust
|
||||
<player></b></li>
|
||||
<li>To give someone permission to manage your claim (trusting other players) do: <b>/permissiontrust <player></b>
|
||||
</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 { ClaimingComponent } from './claiming.component';
|
||||
|
||||
describe('ClaimingComponent', () => {
|
||||
let component: ClaimingComponent;
|
||||
let fixture: ComponentFixture<ClaimingComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ClaimingComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ClaimingComponent);
|
||||
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 {NgOptimizedImage} from '@angular/common';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-claiming',
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
NgOptimizedImage,
|
||||
RouterLink
|
||||
],
|
||||
templateUrl: './claiming.component.html',
|
||||
styleUrl: './claiming.component.scss'
|
||||
})
|
||||
export class ClaimingComponent {
|
||||
|
||||
}
|
||||
@@ -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,135 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'economy'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Economy</h1>
|
||||
<h2>Altitude features a dynamic in-game economy that uses virtual currency to allow for easier trading between
|
||||
players.</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>What is the Economy?</h2>
|
||||
<p>Altitude uses a combination of simple player shops and dynamic NPC merchants to create a truly
|
||||
one-of-a-kind experience for our players. Shops and Merchants allow you to exchange items for virtual
|
||||
currency, and vice-versa. The spawn of each server has some items that can be bought or sold by talking to
|
||||
the various merchants, but player shops are the backbone of the system. These shops are created and run by
|
||||
our players, and they allow those players to determine and set their own prices for their goods.</p>
|
||||
<p>The virtual currency on Altitude has no value outside of the server, nor any item in-game representing
|
||||
it. Each player has a number representing how much in-game currency they have. You can check this any time
|
||||
with <span style="font-family: 'opensans-bold', sans-serif;">/bal</span>.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Player Shops</h2>
|
||||
<p>Altitude allows players to create chest shops, which other players can either buy or sell items to. It’s
|
||||
a really simple task to create your shops. There’s no complex commands or multi-line sign setups -
|
||||
everything is done using a GUI.</p>
|
||||
<p>To get started, grab a sign and place it on a container (does not work on double chests). Write [shop] on
|
||||
the first line and you’re ready to go. Exit the sign interface and the GUI will open. With these simple
|
||||
steps you will have successfully created your first shop!
|
||||
Right click the sign to manage the shop. From there you can control the shop balance, item, item amount
|
||||
and price.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Shop Balance</h2>
|
||||
<p>Every shop will have its own balance. The balance is used to for making changes in the shop management
|
||||
GUI, paying players if they sell items to the shop and receiving money when players buy items from the
|
||||
shop.</p>
|
||||
</div>
|
||||
<!-- <div class="columnparagraph">
|
||||
<h2>Shop Upkeep</h2>
|
||||
<p>Once a week shop upkeep need to be paid. The amount depends on the amount of shops you have; the higher the amount of shops, the higher the fee.
|
||||
The fee will be taken from the shop’s balance. Should there not be enough money in the balance to pay the upkeep fee the shop will be deactivated. Once it had been deactivated other players will no longer be able to interact with it until the shop owner has reactivated it.</p>
|
||||
<p>A shop that hasn’t been interacted with (players using the shop) for three weeks, even if it has the balance to pay for upkeep, will be deactivated and will also need to be reactivated. This is done to prevent dormant and forgotten shops.</p>
|
||||
</div> -->
|
||||
<div class="columnParagraph">
|
||||
<h2>Economist's Tips</h2>
|
||||
<p>To get the most of the in-game economy, keep these tips in mind!</p>
|
||||
<ul>
|
||||
<li>Use the spawn merchants to buy and sell as a last resort! These merchants have intentionally worse
|
||||
prices to allow for player markets to emerge for those same items.
|
||||
</li>
|
||||
<li>Build multiple, smaller farms for different types of items to sell rather than one massive farm that
|
||||
only generates one type of item. You’ll earn more money because of the dynamic prices!
|
||||
</li>
|
||||
<li>Keep track of the points you have with each merchant! Knowing how many points you have will allow you
|
||||
to time your buying and selling for the biggest potential profit!
|
||||
</li>
|
||||
<li>Remember that player shops have flat prices, while the spawn merchants have dynamic prices.</li>
|
||||
<li>Try not to buy or sell when your points are at the worst levels! You will get a huge penalty for
|
||||
having such an extreme amount of points and the transaction will likely not be worth it.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<h2>Spawn Merchants</h2>
|
||||
<p>Every merchant at spawn buys and sells certain types of items. You can interact and trade with them
|
||||
similar to how you would trade emeralds with a regular villager. Each of these merchants are located
|
||||
around each of the server spawn, which you can conveniently get to by doing <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/spawn</span>!</p>
|
||||
|
||||
<!-- INSERT IMAGE OF VILLAGER MERCHANT -->
|
||||
|
||||
<p>These merchants buy and sell directly from the server, not from other players. Prices from these
|
||||
merchants are typically quite high for buying, and quite low for selling. This means that they should be
|
||||
used as a last resort for some quick cash or some quick materials; player shops will generally be a much
|
||||
better value, especially because the prices are static.</p>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>The Point System</h2>
|
||||
<p>The prices offered by the merchants at spawn are dynamic in nature. This means that they change based on
|
||||
the amount and type of items that you've bought or sold recently. Every merchant at spawn keeps track of
|
||||
how many items you’ve traded with it using ‘points’. The amount of points you currently have with each
|
||||
merchant is shown in the trading screen. To understand the point system, all you need to know is:</p>
|
||||
<ul>
|
||||
<li>Positive points = higher buy prices, higher sell prices</li>
|
||||
<li>Negative points = lower buy prices, lower sell prices</li>
|
||||
<li>Points decay naturally over time. The decay is faster if you are online</li>
|
||||
</ul>
|
||||
<p>For most players, this dynamic point system won’t affect you much at all. As long as you aren’t buying or
|
||||
selling huge quantities of items, you won’t notice a large change in the prices as you use the spawn
|
||||
merchants.</p>
|
||||
<p>However, players can take advantage of the point system to make the most amount of profit possible if
|
||||
they are so inclined. To do this, you need to understand the multipliers that affect prices at different
|
||||
price levels. You can see your current price multipliers and points at each merchant by doing <span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/villagerui points</span>.</p>
|
||||
<p style="padding-bottom: 0;">There are different thresholds for each price multiplier to take effect.
|
||||
Between -500 and 500 points, prices will be unaltered. Once your points go above or below that range,
|
||||
prices will become more and more extreme. You can see where these thresholds are based on your current
|
||||
points here:</p>
|
||||
<img ngSrc="/public/img/random/pointbracket.png" alt="Visualization of the point bracket."
|
||||
style="width: 100%; padding: 0 0 15px 0;"
|
||||
height="100" width="800">
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Useful Commands</h2>
|
||||
<ul>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/bal -</span> Check your current balance</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/pay <player> <amount> -</span>
|
||||
Pay other players
|
||||
</li>
|
||||
<li><span style="font-family: 'opensans-bold', sans-serif;">/shop -</span> Click on settings, from there
|
||||
you can change notification settings
|
||||
</li>
|
||||
<li><span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/villagerui points <villagerType> -</span>
|
||||
Allows you to check your current points with each spawn merchant, or any specific one. Also shows your
|
||||
current buy/sell price multipliers for those merchants
|
||||
</li>
|
||||
<li><span
|
||||
style="font-family: 'opensans-bold', sans-serif;">/villagerui buy/sell <item_name> -</span> Tells
|
||||
you if a spawn merchant buys or sells that item, and where that merchant is located
|
||||
</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 { EconomyComponent } from './economy.component';
|
||||
|
||||
describe('EconomyComponent', () => {
|
||||
let component: EconomyComponent;
|
||||
let fixture: ComponentFixture<EconomyComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EconomyComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EconomyComponent);
|
||||
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-economy',
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
NgOptimizedImage
|
||||
],
|
||||
templateUrl: './economy.component.html',
|
||||
styleUrl: './economy.component.scss'
|
||||
})
|
||||
export class EconomyComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'map'"
|
||||
[background_image]="'public/img/backgrounds/map.jpg'"
|
||||
[overlay_gradient]=0.65>
|
||||
<div class="title" header-content>
|
||||
<h1>Dynamic Server Maps</h1>
|
||||
<h2>Which server would you like to see?</h2>
|
||||
<div>
|
||||
<a href="https://gmap.alttd.com">
|
||||
<div style="background-color: #F547B2;" class="button-outer map-button">
|
||||
<span class="button-inner">Grove</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="https://cmap.alttd.com">
|
||||
<div style="background-color: #C46213;" class="button-outer map-button">
|
||||
<span class="button-inner">Creative</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</app-header>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,13 @@
|
||||
.map-button {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.title h2 {
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
font-family: 'open-sans', sans-serif;
|
||||
text-shadow: none;
|
||||
font-size: 1.2em;
|
||||
padding-bottom: 25px;
|
||||
font-weight: 100;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MapComponent } from './map.component';
|
||||
|
||||
describe('MapComponent', () => {
|
||||
let component: MapComponent;
|
||||
let fixture: ComponentFixture<MapComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MapComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MapComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent
|
||||
],
|
||||
selector: 'app-map',
|
||||
templateUrl: './map.component.html',
|
||||
styleUrl: './map.component.scss'
|
||||
})
|
||||
export class MapComponent {
|
||||
|
||||
}
|
||||
@@ -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,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,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>
|
||||
@@ -0,0 +1,14 @@
|
||||
main ul {
|
||||
font-family: opensans, sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
main li {
|
||||
margin-left: 30px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.columnParagraph > span {
|
||||
color: var(--font-color);
|
||||
transition: 0.5s ease;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { WarpsComponent } from './warps.component';
|
||||
|
||||
describe('WarpsComponent', () => {
|
||||
let component: WarpsComponent;
|
||||
let fixture: ComponentFixture<WarpsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [WarpsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(WarpsComponent);
|
||||
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 {NgOptimizedImage} from '@angular/common';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-warps',
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
NgOptimizedImage,
|
||||
RouterLink
|
||||
],
|
||||
templateUrl: './warps.component.html',
|
||||
styleUrl: './warps.component.scss'
|
||||
})
|
||||
export class WarpsComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<footer>
|
||||
<div class="footer">
|
||||
<div class="footerInner">
|
||||
<div class="footerText">
|
||||
<h2>ABOUT US</h2>
|
||||
<p>Altitude is a community-centered {{ ALTITUDE_VERSION }} survival server. We're one of those servers you come
|
||||
to call "home". We are your place to get together with friends and play survival, with a few extra features
|
||||
suggested by our community!</p>
|
||||
<div class="followUs" style="height: 35px; display: flex; align-items: flex-end;">
|
||||
<a target="_blank" rel="noopener" href="https://discordapp.com/invite/TGqpzCJ">
|
||||
<img ngSrc="/public/img/logos/discord.png" alt="Discord Button" height="32" width="32">
|
||||
</a>
|
||||
<a target="_blank" rel="noopener" href="https://twitter.com/alttdmc">
|
||||
<img ngSrc="/public/img/logos/twitter.png" alt="Twitter Button" height="32" width="32">
|
||||
</a>
|
||||
<a target="_blank" rel="noopener" href="https://instagram.com/alttdmc">
|
||||
<img ngSrc="/public/img/logos/instagram.png" alt="Instagram Button" height="32" width="32">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footerNav">
|
||||
<h2>COMMUNITY</h2>
|
||||
<ul>
|
||||
<li><a target="_blank" rel="noopener" href="https://discordapp.com/invite/TGqpzCJ">Discord</a></li>
|
||||
<li><a target="_blank" rel="noopener" href="https://alttd.com/blog">Blog</a></li>
|
||||
<li><a target="_blank" rel="noopener" href="https://twitter.com/alttdmc">Twitter</a></li>
|
||||
<li><a target="_blank" rel="noopener" href="https://instagram.com/alttdmc">Instagram</a></li>
|
||||
<li><a target="_blank" rel="noopener" href="https://reddit.com/r/alttd">Reddit</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footerNav">
|
||||
<h2>SERVER</h2>
|
||||
<ul>
|
||||
<li><a [routerLink]="['/about']">About Us</a></li>
|
||||
<li><a [routerLink]="['/team']">Staffing Team</a></li>
|
||||
<li><a [routerLink]="['/privacy']">Privacy Policy</a></li>
|
||||
<li><a [routerLink]="['/terms']">Terms of Use</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="copyright">Copyright © 2015-{{ getCurrentYear() }} Altitude. All rights Reserved. Not affiliated with
|
||||
Mojang AB or Microsoft.</p>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,69 @@
|
||||
footer {
|
||||
background-color: var(--footer-color);
|
||||
transition: 0.5s ease;
|
||||
}
|
||||
|
||||
footer h2 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.3em;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
footer ul, footer p {
|
||||
list-style: none;
|
||||
font-size: 0.9em;
|
||||
color: rgba(168, 168, 168);
|
||||
font-family: 'opensans', sans-serif;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 80px 0;
|
||||
width: 80%;
|
||||
max-width: 1020px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footerInner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.footerText {
|
||||
flex-basis: 45%;
|
||||
margin-right: 60px;
|
||||
}
|
||||
|
||||
.followUs {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.followUs img {
|
||||
margin-right: 5px;
|
||||
float: left;
|
||||
filter: grayscale(100%);
|
||||
-webkit-filter: grayscale(100%);
|
||||
transition: all 0.2s;
|
||||
-webkit-transition: all 0.2s;
|
||||
}
|
||||
|
||||
.followUs img:hover {
|
||||
margin-bottom: 5px;
|
||||
filter: grayscale(0%);
|
||||
-webkit-filter: grayscale(0%);
|
||||
transform: scale(1.1);
|
||||
-webkit-transform: scale(1.1);
|
||||
}
|
||||
|
||||
.footerNav {
|
||||
flex-grow: 1;
|
||||
border-left: 1px solid rgba(19, 19, 19);
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.footerNav li {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
margin-top: 50px;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FooterComponent } from './footer.component';
|
||||
|
||||
describe('FooterComponent', () => {
|
||||
let component: FooterComponent;
|
||||
let fixture: ComponentFixture<FooterComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FooterComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FooterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ALTITUDE_VERSION} from '@custom-types/constant';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
NgOptimizedImage
|
||||
],
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrl: './footer.component.scss'
|
||||
})
|
||||
export class FooterComponent {
|
||||
public getCurrentYear() {
|
||||
return new Date().getFullYear();
|
||||
}
|
||||
|
||||
protected readonly ALTITUDE_VERSION = ALTITUDE_VERSION;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'privacy'" height="300px" background_image="/public/img/backgrounds/trees.jpg"
|
||||
[overlay_gradient]="0.5">>
|
||||
<div class="title" header-content>
|
||||
<h1 class="titleColor">Privacy Policy</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<p>Last updated: April 6th, 2025</p>
|
||||
<p>Welcome to Altitude! This privacy policy (“Policy”) describes the data practices of the Altitude
|
||||
Community (a group consisting of server owner and staffing team members) and its subsidiaries
|
||||
(collectively “we” , or "Altitude"). This Policy covers the services under our control (collectively
|
||||
"Services"), which includes, but is not limited to, this website and our Minecraft server. Before using
|
||||
our Services please take a moment to read this Privacy Policy. This Privacy Policy outlines what
|
||||
information we collect from you and how we use it. Any use of our Services will constitute your acceptance
|
||||
of this Policy as well as our <a [routerLink]="['/terms']">Terms of Use</a>. If you do not agree to our
|
||||
Privacy
|
||||
Policy or Terms of Use, please do not use our Services. Thank you!</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Information we collect</h2>
|
||||
<p>When you use our Services we may collect PII, DII, and log information about your interactions with our
|
||||
Services as described below.</p>
|
||||
<p>Personally identifiable information (PII) is information that can be used to identify or contact you
|
||||
online or offline, such as your name, username, Minecraft UUID, address, email, phone number, and payment
|
||||
information. The Services may collect PII when it is provided to us, such as when you use our Services,
|
||||
attempt to contact us, submit an application using one of our forms, or connect with us on social media or
|
||||
one of our partners.</p>
|
||||
<p>We may also create or collect device-identifiable information (DII), such as cookies, unique device and
|
||||
advertising identifiers, and similar identifiers that are linkable to a browser or device. We may also
|
||||
receive other information during your use of our Services, such as your IP address, user agent,
|
||||
timestamps, precise and imprecise geolocation, sensor data, apps, fonts, battery life information, and
|
||||
screen size.</p>
|
||||
<p>Our Services also collect information about your use of our Services, including navigation paths, search
|
||||
queries, crashes, timestamps, purchases, clicks and shares, and referral URLs. We may combine this data
|
||||
with PII and DII. We may also partner with third parties that collect additional information or assist us
|
||||
with processing the information we collect – please see their privacy policies for more details and see
|
||||
below for your choices regarding these parties.</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>How we use information we collect</h2>
|
||||
<p>We use the information we collect from our Services to provide, maintain, protect and improve our
|
||||
Services, to develop new Services and offerings, and to protect us and our users.</p>
|
||||
<p>PII is primarily used for business purposes, such as for sending you occasional newsletters and updates,
|
||||
responding to inquiries and applications, logins, and providing Services. When you contact us, we may keep
|
||||
a record of your communication as well as the other information provided to us. We may use your email
|
||||
address to inform you about our Services, such as letting you know about changes or improvements.</p>
|
||||
<p>We share PII with companies, outside organizations, and individuals for limited reasons, outlined
|
||||
below:</p>
|
||||
<ol>
|
||||
<li>With your consent - We will share PII with companies, outside organizations or individuals if we have
|
||||
your consent to do so.
|
||||
</li>
|
||||
<li>For external processing - We provide PII to our affiliates or other trusted businesses or persons to
|
||||
process it for us, based on our instructions and in compliance with our Privacy Policy and any other
|
||||
appropriate confidentiality and security measures.
|
||||
</li>
|
||||
<li>For legal reasons - We will share PII with companies, outside organizations or individuals if we have
|
||||
a good-faith belief that access, use, preservation or disclosure of the information is reasonably
|
||||
necessary to meet any applicable law, regulation, legal process or enforceable governmental request,
|
||||
detect, prevent, or otherwise address fraud, security or technical issues or protect against harm to the
|
||||
rights, property or safety of our users or the public as required or permitted by law.
|
||||
</li>
|
||||
<li>In case of a sale or asset transfer - If we become involved in a merger, acquisition or other
|
||||
transaction involving the sale of some or all of our assets, user information, including PII collected
|
||||
from you through your use of our Services, could be included in the transferred assets. Should such an
|
||||
event occur, we will use reasonable means to notify you, either through email and/or a prominent notice
|
||||
on the Services.
|
||||
</li>
|
||||
<li>In aggregated form for business purposes - We may share aggregated information and DII with our
|
||||
partners such as businesses we have a relationship with, advertisers or connected sites. For example, we
|
||||
may share information to show trends about the general use of our Services.
|
||||
</li>
|
||||
</ol>
|
||||
<p>We use DII to operate our Services and manage user sessions, including analyzing usage of our Services,
|
||||
preventing malicious behavior and fraud, improving the content, to link your identity across devices and
|
||||
browsers in order to provide you with a more seamless experience online, and helping third parties provide
|
||||
relevant advertising and related metrics. We share DII with third parties primarily for advertising and
|
||||
analytics purposes, for external processing, and for security purposes.</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Third parties</h2>
|
||||
<p>Altitude uses many external services to provide additional services or features for you. Whenever you are
|
||||
using any of these external services you are bound by the respective service's Privacy Policy. This Policy
|
||||
does not apply to services offered by other companies or individuals, including products or sites that may
|
||||
be displayed to you on our Services. Please be aware that Altitude will not be responsible or liable for
|
||||
the collection of any information by these external services or their use of any information collected.
|
||||
You should always read the Privacy Policy of any service you use.</p>
|
||||
<p>We may use third parties, such as Google, to help offer you more tailored ads and better Services, such
|
||||
as obtaining analytics about the users of our site and to help tailor advertising to your preferences.
|
||||
Google's use of advertising cookies enables it and its partners to serve ads to your users based on their
|
||||
visit to your sites and/or other sites on the Internet. Users may opt out of personalized advertising by
|
||||
visiting <a href="https://www.google.com/settings/ads">www.google.com/settings/ads</a>. You may also be
|
||||
able to opt-out of some third-party vendors individually by visiting <a
|
||||
href="https://www.aboutads.info/choices/">www.aboutads.info</a>.</p>
|
||||
<p>Please note that any use of the Minecraft game and launcher is governed by Microsoft's Privacy Policy.
|
||||
Therefore, by visiting our Minecraft server, you consent to Microsoft's Privacy Policy and Terms of Use. To
|
||||
see Microsoft's Privacy Policy please visit their website at <a
|
||||
href="https://www.microsoft.com/en-us/privacy/privacystatement">microsoft.com/en-us/privacy/privacystatement</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Children under 13</h2>
|
||||
<p>We do not knowingly collect personally identifying information from children under the age of 13. If you
|
||||
are under the age of 13, please do not submit any personal information through and of our services. Never
|
||||
share your name, address, or any other personal information on our Minecraft server, forums, or any of our
|
||||
other services. Always ask a parent or legal guardian to make purchases for you if you wish to make a
|
||||
purchase from Altitude.</p>
|
||||
<p>If you are the parent or guardian of a child under the age of 13 and have a concern regarding your
|
||||
child’s information on our Services, please contact us at <a
|
||||
[routerLink]="['/contact']">alttd.com/contact</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Managing your information</h2>
|
||||
<p>If you believe we have information that needs to be removed, or would like to know what information we've
|
||||
collected from you, you may contact us at <a
|
||||
[routerLink]="['/contact']">alttd.com/contact</a>. We will work with you to ensure your
|
||||
privacy is protected. In some cases, we may keep PII for legitimate business or legal purposes.</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Changes to this policy</h2>
|
||||
<p>Our Privacy Policy may change from time to time. We will post any Policy changes on this page. Please
|
||||
check back periodically to view changes to our privacy policy. By continuing to access or use our website
|
||||
or services after any revisions become effective, you agree to be bound by the revised Policy. If you do
|
||||
not agree to the new Policy, please stop using our website and services.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<a (click)="this.scrollService.scrollToTop()" class="scroll-up-button, active">
|
||||
<span></span>
|
||||
<p style="display: none;">Scroll Down</p>
|
||||
</a>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,12 @@
|
||||
main .container {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.paragraph p, .paragraph ol {
|
||||
margin-bottom: 15px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PrivacyComponent } from './privacy.component';
|
||||
|
||||
describe('PrivacyComponent', () => {
|
||||
let component: PrivacyComponent;
|
||||
let fixture: ComponentFixture<PrivacyComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PrivacyComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PrivacyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-privacy',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent,
|
||||
RouterLink
|
||||
],
|
||||
templateUrl: './privacy.component.html',
|
||||
styleUrl: './privacy.component.scss'
|
||||
})
|
||||
export class PrivacyComponent {
|
||||
constructor(public scrollService: ScrollService) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'terms'" height="300px" background_image="/public/img/backgrounds/trees.jpg"
|
||||
[overlay_gradient]="0.5">>
|
||||
<div class="title" header-content>
|
||||
<h1 class="titleColor">Terms of Use</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<p>Last updated: April 6th, 2025</p>
|
||||
<p>Welcome to Altitude! These Terms of Use (“Terms”) is a binding agreement between you and the Altitude
|
||||
Community (a group consisting of server owner and staffing team members) and our subsidiaries (collectively
|
||||
“we”, or "Altitude"). These Terms describe the manner in which you may use our services. Any use of our
|
||||
Services will constitute your acceptance of these Terms as well as our <a [routerLink]="['/privacy']">Privacy
|
||||
Policy</a>.
|
||||
If you do not agree to our Privacy Policy or Terms of Use, please do not use our Services. Thank you!</p>
|
||||
<p>Also, please note that Altitude is not affiliated with Mojang and is run independently by the server owner.
|
||||
Any questions or concerns should be directed to our support team at <a [routerLink]="['/contact']">alttd.com/contact</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Fair Use</h2>
|
||||
<p>Our Minecraft server is governed under the Minecraft EULA (End User License Agreement). The Minecraft EULA
|
||||
states that everyone who accesses our server must have fair and reasonable access to all parts of the game
|
||||
and server. Hacking, duping, or using modifications to have an advantage over other players is not allowed
|
||||
and will result in termination of access to the server. Vanilla exploits that all users are capable of using
|
||||
with a vanilla copy of the game are generally allowed, but can be restricted or prohibited at our
|
||||
discretion. By joining our Minecraft server you have agreed to the Minecraft EULA and to <a
|
||||
[routerLink]="['/rules']">our server rules</a>.
|
||||
You should read the Minecraft EULA before joining any Minecraft server at <a
|
||||
href="https://www.minecraft.net/en-us/eula">minecraft.net/en-us/eula</a>.</p>
|
||||
<p>Any actions that are interpreted to have negative intentions towards another person using our services, or
|
||||
towards the services themselves, could result in termination of access to our services. Final interpretation
|
||||
of actions performed by any person using our services, and choice of correction for said actions, is at the
|
||||
discretion of our staff. This decision can only be overridden by any member of the <a
|
||||
[routerLink]="['/team']">staffing
|
||||
team</a> here on Altitude.</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Appropriate Use</h2>
|
||||
<p>All of the services we offer are intended for a mature audience. We do not guarantee that the content found
|
||||
on any of our services will be appropriate for younger audiences. That said, we want to highlight the term
|
||||
"mature". We believe that part of maturity is knowing what is appropriate, and what isn't, for each
|
||||
individual circumstance. For example, you will not find any set rules on profanity for any of our services.
|
||||
We reserve the right to determine, at our discretion, what is appropriate and what is not in every
|
||||
circumstance. Furthermore, we reserve the right to determine appropriate correction of inappropriate actions
|
||||
by anyone using our services.</p>
|
||||
<p>We want to provide a server that is appropriate for all ages, while still keeping a mature feel that is
|
||||
welcoming to an older audience. Please help us maintain this balance. We are a place for everyone to come
|
||||
together, and many parents would like a safe environment for their children to play as well. Thank you.</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Intellectual Property Rights</h2>
|
||||
<p>By posting or creating content of any form on any of our services you grant us a license to modify,
|
||||
commercialize, or delete your content at our discretion. We may commercialize your content by using the
|
||||
content in any form of advertising that we do. We will NEVER sell your content or the rights to your
|
||||
content, only use it to promote Altitude. Also, please keep our services clean by only creating appropriate
|
||||
content. We may remove any content you create if we determine it to be inappropriate! </p>
|
||||
<p>Second, you can request a copy of your content by contacting us. Only the author of the content will be
|
||||
allowed to request a copy. We will always provide a copy of your content as long as we still have the
|
||||
ability to do so (that is, if is still exists!). We may retain a copy of anything you create, even if you
|
||||
delete it (often in the backups of our Minecraft server), so be careful what you create!</p>
|
||||
<p>Finally, if you believe your intellectual property rights have been infringed upon you may contact us at
|
||||
<a [routerLink]="['/contact']">alttd.com/contact</a>. Please note that this only applies for
|
||||
content that was originally created outside of our services.</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Donations and Purchases</h2>
|
||||
<p>Please note that in-game items, ranks, and other stuff received from donating (collectively referred to as
|
||||
"perks") are subject to change as our services change. Our server is constantly changing with features being
|
||||
added and removed, and the perks donors receive may change with it. We make no guarantee that the perks you
|
||||
receive today will always be available.</p>
|
||||
<p>Any package, rank, or other perk that is defined as "lifetime" is intended to last the lifetime of the
|
||||
service they apply to, not the lifetime of the receiver of the perks. Lifetime perks may be terminated if
|
||||
they are abused, please see "Termination of Perks".</p>
|
||||
<p>Additionally, abuse of any of your perks will result in termination of some or all of your perks. "Abuse"
|
||||
of a perk includes using the perk to harm our services or individuals using our services (such as taunting a
|
||||
player about how you have a special perk and they don't), or finding a way to exploit the perk and use it in
|
||||
a manner that was not intended (such as changing your nickname to impersonate other players).</p>
|
||||
<p>We will not transfer the perks you've received from donating to another account. Please do not attempt to
|
||||
give away your perks, or sell them. If you wish to give someone a perk as a gift you use the "Gift This
|
||||
Package" option when you add an item to your cart in the store. We will make an exception of this rule for
|
||||
users who have their Minecraft accounts hacked, stolen, or lost. If you are using a new Minecraft account
|
||||
and need perks transferred please contact us at <a [routerLink]="['/contact']">alttd.com/contact</a>.
|
||||
</p>
|
||||
<p>For Stripe transactions only: temporary packages, perks or other including but not limited to the McMMO
|
||||
boosters, are non-refundable. Packages, perks, or other that do not expire can be refunded up to 14 days
|
||||
from the day of purchase. Lifetime Ranks can be refunded within 90 days of purchase if the perks of the
|
||||
purchased rank change after your purchase date. After this time period we will not refund any perks,
|
||||
packages, or other purchased.</p>
|
||||
<p>Any donations made through Patreon are non-refundable. You can cancel your Patreon donation at any time.
|
||||
You will continue to have access to your perks until the next billing date.</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Termination and Limitation</h2>
|
||||
<p>We may terminate or suspend any user's access to one or all of our services immediately, without prior
|
||||
notice or liability, for any reason. This includes failure to follow these Terms! A termination of access
|
||||
will prohibit you from accessing one or all of our services. Termination of access on our Minecraft Server
|
||||
(a server ban) can be appealed by <a [routerLink]="['/appeal']">submitting a form here</a>. Submitting
|
||||
an appeal does not guarantee a lift of the termination. We will attempt to process your appeal within 48
|
||||
hours of submitting at which point it will be decided if your termination is lifted or not.</p>
|
||||
<p>We also hold the right to put your use of any of our services under limitation without prior notice or
|
||||
liability, for any reason. Limitations can be in any form up to complete termination of access. Limitations
|
||||
on the Minecraft server include, but are not limited to, being "jailed", muted, or having specific features
|
||||
taken away from your Minecraft account on the server.</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>External Services</h2>
|
||||
<p>Altitude uses many external services to provide additional services or features for you. Whenever you are
|
||||
using any of these external services you are bound by the respective service's Terms and Condition. Please
|
||||
be aware that Altitude will not be responsible or liable for any damage or loss caused by, or in connection
|
||||
with, the use of any external services. You should always read the Terms and Conditions of any service you
|
||||
use.</p>
|
||||
<p>This means that by joining our Minecraft server you have agreed to Minecraft's Terms of Use (<a
|
||||
href="https://www.microsoft.com/en-us/legal/terms-of-use">microsoft.com/en-us/legal/terms-of-use</a>), and
|
||||
the Minecraft EULA (<a
|
||||
href="https://www.minecraft.net/en-us/eula">minecraft.net/en-us/eula</a>).
|
||||
</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Accuracy of Information</h2>
|
||||
<p>All the information on our website and services is published in good faith and is for general information
|
||||
purposes only. Altitude does not make any warranties about the completeness, reliability or accuracy of any
|
||||
information found on our website or services. Any action you take upon the information you find on any of
|
||||
our services is strictly at your own risk. Altitude will not be responsible or liable for any damage or loss
|
||||
caused by, or in connection with, the use of our website or services.</p>
|
||||
</div>
|
||||
<div class="paragraph">
|
||||
<h2>Changes of Terms</h2>
|
||||
<p>Our Terms may change from time to time. We will post any Policy changes on this page. Please check back
|
||||
periodically to view changes to our Terms. By continuing to access or use our website or services after any
|
||||
revisions become effective, you agree to be bound by the revised Terms. If you do not agree to the new
|
||||
Terms, please stop using our website and services.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<a (click)="this.scrollService.scrollToTop()" class="scroll-up-button, active">
|
||||
<span></span>
|
||||
<p style="display: none;">Scroll Down</p>
|
||||
</a>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,12 @@
|
||||
main .container {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.paragraph p {
|
||||
margin-bottom: 15px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TermsComponent } from './terms.component';
|
||||
|
||||
describe('TermsComponent', () => {
|
||||
let component: TermsComponent;
|
||||
let fixture: ComponentFixture<TermsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TermsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TermsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-terms',
|
||||
standalone: true,
|
||||
templateUrl: './terms.component.html',
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent,
|
||||
RouterLink
|
||||
],
|
||||
styleUrl: './terms.component.scss'
|
||||
})
|
||||
export class TermsComponent {
|
||||
constructor(public scrollService: ScrollService) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 '@shared-components/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,141 @@
|
||||
<ng-container>
|
||||
<header id="top"
|
||||
[ngStyle]="{ 'background-image': getBackgroundImage(),'height': height, 'background-position': getBackgroundPosition() }">
|
||||
<nav id="nav" [ngClass]="active">
|
||||
<div class="container">
|
||||
<div id="mobile_nav">
|
||||
<div id="menu_toggle">
|
||||
<!--A fake / hidden checkbox is used as click receiver, so you can use the :checked selector on it.-->
|
||||
<input type="checkbox"/>
|
||||
<!--Some spans to act as a hamburger. They are acting like a real hamburger, not that McDonald's stuff.-->
|
||||
<span class="hamburger"></span>
|
||||
<span class="hamburger"></span>
|
||||
<span class="hamburger"></span>
|
||||
<!--Too bad the menu has to be inside of the button but hey, it's pure CSS magic.-->
|
||||
<ul id="mobile_nav_list">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li>
|
||||
<a href="#">Features</a>
|
||||
<ul class="mobile_dd">
|
||||
<li><a [routerLink]="['/map']">Map</a></li>
|
||||
<li><a [routerLink]="['/economy']">Economy</a></li>
|
||||
<!-- <li><a [routerLink]="['/events']">Events</a></li> -->
|
||||
<li><a [routerLink]="['/claiming']">Land Claiming</a></li>
|
||||
<li><a [routerLink]="['/mypet']">MyPet</a></li>
|
||||
<li><a [routerLink]="['/warps']">Warps</a></li>
|
||||
<li><a [routerLink]="['/skyblock']">Skyblock</a></li>
|
||||
<li><a [routerLink]="['/customfeatures']">Custom Features</a></li>
|
||||
<li><a
|
||||
href="https://kanboard.alttd.com/public/board/ef0c28f3d022f7e98801bbc97278e9a523c7c6dc2a3b0c58c47e2ebdbd1e">Suggestion
|
||||
Board</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">Reference</a>
|
||||
<ul class="mobile_dd">
|
||||
<li><a [routerLink]="['/guide']">Guide Book</a></li>
|
||||
<!-- <li><a [routerLink]="['/faq']">FAQ</a></li> -->
|
||||
<li><a [routerLink]="['/ranks']">Ranks</a></li>
|
||||
<li><a [routerLink]="['/rules']">Rules</a></li>
|
||||
<li><a [routerLink]="['/commandlist']">Command List</a></li>
|
||||
<li><a [routerLink]="['/mapart']">Mapart</a></li>
|
||||
<li><a [routerLink]="['/lag']">Reducing Lag</a></li>
|
||||
<li><a [routerLink]="['/staffpowers']">Staff Powers</a></li>
|
||||
<li><a [routerLink]="['/nicknames']">Nicknames</a></li>
|
||||
<li><a [routerLink]="['/bans']">Minecraft Bans</a></li>
|
||||
<li><a [routerLink]="['/discord-bans']">Discord Bans</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="https://discordapp.com/invite/TGqpzCJ">Discord</a></li>
|
||||
<li><a [routerLink]="['/vote']">Vote</a></li>
|
||||
<li><a href="https://store.alttd.com">Store</a></li>
|
||||
<li>
|
||||
<a href="#">Altitude</a>
|
||||
<ul class="mobile_dd">
|
||||
<li><a [routerLink]="['/about']">About Us</a></li>
|
||||
<li><a [routerLink]="['/socials']">Socials</a></li>
|
||||
<li><a [routerLink]="['/team']">Team</a></li>
|
||||
<li><a [routerLink]="['/birthdays']">Famous Birthdays</a></li>
|
||||
<li><a [routerLink]="['/community']">Community</a></li>
|
||||
<li><a [routerLink]="['/contact']">Contact Us</a></li>
|
||||
<li><a href="https://alttd.com/appeal">Ban Appeal</a></li>
|
||||
<li><a href="https://alttd.com/blog/">Blog</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/"><img ngSrc="/public/img/logos/logo.png" priority alt="Altitude Server Logo" height="319"
|
||||
width="550"></a>
|
||||
<ul>
|
||||
<li class="nav_li"><a [id]="getCurrentPageId(['home'])" class="nav_link" href="/" [ngClass]="active">Home</a>
|
||||
</li>
|
||||
<li class="nav_li">
|
||||
<span [id]="getCurrentPageId(['map', 'customfeatures', 'economy', 'events', 'mypet', 'warps',
|
||||
'skyblock', 'claiming'])" class="nav_link fake_link" [ngClass]="active">Features</span>
|
||||
<ul class="dropdown">
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/map']">Map</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/economy']">Economy</a></li>
|
||||
<!-- <li class="navli"><a class="nav_link2" [routerLink]="['/events']">Events</a></li> -->
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/claiming']">Land Claiming</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/mypet']">MyPet</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/warps']">Warps</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/skyblock']">Skyblock</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/customfeatures']">Custom Features</a></li>
|
||||
<li class="nav_li"><a class="nav_link2"
|
||||
href="https://kanboard.alttd.com/public/board/ef0c28f3d022f7e98801bbc97278e9a523c7c6dc2a3b0c58c47e2ebdbd1e">Suggestion
|
||||
Board</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav_li">
|
||||
<span
|
||||
[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>
|
||||
<!-- <li class="navli"><a class="nav_link2" [routerLink]="['/faq']">FAQ</a></li> -->
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/ranks']">Ranks</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/rules']">Rules</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/commandlist']">Command List</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/mapart']">Mapart</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/lag']">Reducing Lag</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/staffpowers']">Staff Powers</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/nicknames']">Nicknames</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/bans']">Minecraft Bans</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/discord-bans']">Discord Bans</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav_li"><a class="nav_link" target="_blank" rel="noopener"
|
||||
href="https://discordapp.com/invite/TGqpzCJ" [ngClass]="active">Discord</a></li>
|
||||
<li class="nav_li"><a [id]="getCurrentPageId(['vote'])" class="nav_link" [routerLink]="['/vote']"
|
||||
[ngClass]="active">Vote</a>
|
||||
</li>
|
||||
<li class="nav_li">
|
||||
<a class="nav_link" target="_blank" rel="noopener" href="https://store.alttd.com"
|
||||
[ngClass]="active">Store</a>
|
||||
</li>
|
||||
<li class="nav_li">
|
||||
<a [id]="getCurrentPageId(['about', 'team', 'community', 'birthdays'])"
|
||||
class="nav_link fake_link" [ngClass]="active">Altitude</a>
|
||||
<ul class="dropdown">
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/about']">About Us</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/socials']">Socials</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/team']">Team</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/birthdays']">Famous Birthdays</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/community']">Community</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" target="_blank" rel="noopener" [routerLink]="['/contact']">Contact
|
||||
Us</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" target="_blank" rel="noopener" href="https://alttd.com/appeal">Ban
|
||||
Appeal</a></li>
|
||||
<li class="nav_li"><a class="nav_link2" target="_blank" href="https://alttd.com/blog/">Blog</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<app-theme></app-theme>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<ng-content select="[header-content]"></ng-content>
|
||||
|
||||
</header>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,353 @@
|
||||
header {
|
||||
background-size: cover;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
header .container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#header-img {
|
||||
max-width: 550px;
|
||||
padding-bottom: 15px;
|
||||
margin-top: -120px;
|
||||
}
|
||||
|
||||
#nav {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 110px;
|
||||
z-index: 3;
|
||||
transition: height 0.3s, background 0.5s ease;
|
||||
}
|
||||
|
||||
#nav.active {
|
||||
height: 80px;
|
||||
background-color: var(--color-primary);
|
||||
box-shadow: 0 2px 3px -1px rgba(30, 30, 30, 0.1);
|
||||
}
|
||||
|
||||
.nav_link.active {
|
||||
color: var(--navlink-color);
|
||||
}
|
||||
|
||||
.nav_link.active:hover {
|
||||
color: var(--navlink-color);
|
||||
}
|
||||
|
||||
nav img {
|
||||
width: 90px;
|
||||
height: 52.2px;
|
||||
}
|
||||
|
||||
.nav_li {
|
||||
display: inline;
|
||||
font-family: 'minecraft-title', sans-serif;
|
||||
color: #333333;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav_link {
|
||||
text-decoration: none;
|
||||
padding: 15px;
|
||||
color: #FFF;
|
||||
font-size: 0.9em;
|
||||
transition: 0.5s ease;
|
||||
}
|
||||
|
||||
.nav_link:hover {
|
||||
text-decoration: none;
|
||||
color: #DBDBDB;
|
||||
}
|
||||
|
||||
.nav_link:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
bottom: -14px;
|
||||
left: 0;
|
||||
background-color: var(--link-color);
|
||||
visibility: hidden;
|
||||
transform: scaleX(0);
|
||||
transition: all 0.3s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.nav_link:hover:before {
|
||||
visibility: visible;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.nav_link2 {
|
||||
color: var(--navlink-color);
|
||||
padding: 16px 16px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
transition: background-color 0.3s, 0.5s ease;
|
||||
}
|
||||
|
||||
.nav_link2:hover {
|
||||
background-color: var(--dropdown-color);
|
||||
color: var(--navlink-color);
|
||||
}
|
||||
|
||||
.nav_li:hover .dropdown {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: visibility 0.3s, opacity 0.3s;
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
#menu_toggle {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: 50px;
|
||||
left: 50px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#menu_toggle input {
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: -5px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#menu_toggle span:first-child {
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
#menu_toggle span:nth-last-child(2) {
|
||||
transform-origin: 0 100%;
|
||||
}
|
||||
|
||||
#menu_toggle input:checked ~ span {
|
||||
opacity: 1;
|
||||
transform: rotate(45deg) translate(-2px, -1px);
|
||||
background: var(--hamburger);
|
||||
}
|
||||
|
||||
#menu_toggle input:checked ~ span:nth-last-child(3) {
|
||||
opacity: 0;
|
||||
transform: rotate(0deg) scale(0.2, 0.2);
|
||||
}
|
||||
|
||||
#menu_toggle input:checked ~ span:nth-last-child(2) {
|
||||
transform: rotate(-45deg) translate(0, -1px);
|
||||
}
|
||||
|
||||
#menu_toggle input:checked ~ ul {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.hamburger {
|
||||
display: block;
|
||||
width: 33px;
|
||||
height: 4px;
|
||||
margin-bottom: 5px;
|
||||
position: relative;
|
||||
background: #cdcdcd;
|
||||
border-radius: 3px;
|
||||
z-index: 1;
|
||||
transform-origin: 4px 0;
|
||||
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), opacity 0.55s ease;
|
||||
}
|
||||
|
||||
#mobile_nav_list {
|
||||
position: fixed;
|
||||
top: -70px;
|
||||
left: 45px;
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
margin: -100px 0 0 -50px;
|
||||
padding: 125px 50px 50px;
|
||||
background: var(--color-primary);
|
||||
list-style-type: none;
|
||||
transform-origin: 0 0;
|
||||
transform: translate(-100%, 0);
|
||||
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0);
|
||||
}
|
||||
|
||||
#mobile_nav_list li {
|
||||
padding: 10px 0;
|
||||
font-size: 1.2em;
|
||||
font-family: 'opensans-bold', sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#mobile_nav_list li a {
|
||||
color: var(--navlink-color);
|
||||
}
|
||||
|
||||
#mobile_nav_list li a:hover, #mobile_nav_list li span:hover {
|
||||
color: var(--navlink-color);
|
||||
border-bottom: 2px solid var(--navlink-color);
|
||||
}
|
||||
|
||||
#mobile_nav_list .mobile_dd {
|
||||
display: none;
|
||||
margin: 8px 0 -5px 20px;
|
||||
}
|
||||
|
||||
#mobile_nav_list .mobile_dd2 {
|
||||
display: none;
|
||||
margin: 8px 0 -5px 20px;
|
||||
}
|
||||
|
||||
#mobile_nav_list .mobile_dd li {
|
||||
font-size: 1em;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#mobile_nav_list li:hover .mobile_dd {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#mobile_nav_list .mobile_dd li:hover .mobile_dd2 {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav_li {
|
||||
display: inline;
|
||||
font-family: 'minecraft-title', sans-serif;
|
||||
color: #333333;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#current_page {
|
||||
text-decoration: none;
|
||||
color: var(--white);
|
||||
background: var(--link-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.fake_link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 28px;
|
||||
background-color: var(--color-primary);
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
border-radius: 5px;
|
||||
transition: visibility 0.3s, opacity 0.3s;
|
||||
transition-delay: 0.1s;
|
||||
font-size: 0.9em;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#sub_drop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
.dropdown2 {
|
||||
top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (-ms-ime-align:auto) {
|
||||
.dropdown2 {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
#scroll-button {
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1150px) {
|
||||
.dropdown {
|
||||
top: -10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#nav {
|
||||
height: 150px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#nav.active {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
#nav_list {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
#nav img, #nav ul {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
header .container {
|
||||
justify-content: center;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.switch-div {
|
||||
top: 65px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 690px) {
|
||||
#nav_list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#nav {
|
||||
height: 90px;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
#nav.active {
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
#nav img {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
#mobile_nav {
|
||||
display: block;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: -15px;
|
||||
}
|
||||
|
||||
#current_page {
|
||||
background: none;
|
||||
border-bottom: 3px solid var(--pureblack);
|
||||
}
|
||||
|
||||
.switch-div {
|
||||
top: 33px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 690px) {
|
||||
#mobile_nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {HeaderComponent} from './header.component';
|
||||
|
||||
describe('HeaderComponent', () => {
|
||||
let component: HeaderComponent;
|
||||
let fixture: ComponentFixture<HeaderComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HeaderComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import {Component, HostListener, Input} from '@angular/core';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {ThemeComponent} from '@shared-components/theme/theme.component';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ThemeComponent,
|
||||
RouterLink,
|
||||
NgOptimizedImage
|
||||
],
|
||||
selector: 'app-header',
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss']
|
||||
})
|
||||
export class HeaderComponent {
|
||||
@Input() current_page: string = '';
|
||||
@Input() background_image: string = '';
|
||||
@Input() height: string = '';
|
||||
@Input() overlay_gradient: number = 0;
|
||||
|
||||
public active: string = '';
|
||||
public inverseYPos: number = 0;
|
||||
|
||||
@HostListener('window:scroll', [])
|
||||
onWindowScroll(): void {
|
||||
const scrollPosition = window.scrollY;
|
||||
if (scrollPosition > 5) {
|
||||
this.active = 'active';
|
||||
this.inverseYPos = scrollPosition - (scrollPosition * 0.2);
|
||||
} else {
|
||||
this.inverseYPos = 0;
|
||||
this.active = '';
|
||||
}
|
||||
}
|
||||
|
||||
public getBackgroundImage() {
|
||||
if (this.overlay_gradient) {
|
||||
return `linear-gradient(rgba(0, 0, 0, ${this.overlay_gradient}), rgba(0, 0, 0, ${this.overlay_gradient})), url(${this.background_image})`;
|
||||
} else {
|
||||
return `url(${this.background_image})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public getCurrentPageId(options: string[]) {
|
||||
if (options.includes(this.current_page)) {
|
||||
return 'current_page'
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getBackgroundPosition() {
|
||||
if (this.current_page === 'home') {
|
||||
return 'center ' + this.inverseYPos + 'px'
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'home'" height="100vh"
|
||||
[background_image]="'/public/img/backgrounds/120spawn-min.png'">
|
||||
<div class="title" header-content>
|
||||
<h1 style="display: none;">Altitude</h1>
|
||||
<img id="header-img" ngSrc="/public/img/logos/logo.png" alt="The Altitude Minecraft Server" height="319"
|
||||
width="550">
|
||||
<h2 style="font-size: 2.5em;" id="homeh2">Altitude now on {{ ALTITUDE_VERSION }}!</h2>
|
||||
<a id="scroll-button" (click)="scrollToSection()">
|
||||
<span></span>
|
||||
<p style="display: none;">Scroll Down</p>
|
||||
</a>
|
||||
</div>
|
||||
</app-header>
|
||||
<main>
|
||||
<section id="scrollingpoint" style="background: #202020; text-align: center; padding: 80px 0;">
|
||||
<!-- TODO load player count from old api or backend?-->
|
||||
<h2 style="color: white;"><span class="player-count">Loading...</span></h2>
|
||||
<h2 style="color: white;">Server IP: play.alttd.com</h2>
|
||||
<div style="padding-top: 35px;">
|
||||
<app-copy-ip></app-copy-ip>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="paragraph">
|
||||
<h2>Adventure Begins</h2>
|
||||
<p>You awake in a strange town, where are you? There are residents running about trading with each other and
|
||||
stories of distant realms with more towns. It's time to write your story. Welcome to Altitude, the laid-back
|
||||
community-oriented server that hosts your home for Minecraft.</p>
|
||||
</div>
|
||||
<img ngSrc="/public/img/items/bookquill.png" style="width: 150px; align-self: center; margin: 0 auto;"
|
||||
alt="Alternative Altitude Server Logo"
|
||||
height="150" width="150">
|
||||
</div>
|
||||
</section>
|
||||
<!--
|
||||
<section id="section1">
|
||||
<div class="container" id="video">
|
||||
<h2 style="display: none;">YouTube Trailer</h2>
|
||||
<div style="border-radius:5px;overflow:hidden;position:relative;width:100%">
|
||||
<img style="width: 100%" src="https://img.youtube.com/vi/Nzbj9Dbv5Wk/maxresdefault.jpg" alt="Altitude YouTube Trailer">
|
||||
<div id="youtube">
|
||||
<div class="play" onclick="playVideo()"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
-->
|
||||
<section class="darkmodeSection">
|
||||
<div class="container" style="padding: 10px 0 80px 0">
|
||||
<iframe id="discord-widget" src="https://discordapp.com/widget?id=141644560005595136&theme=dark"></iframe>
|
||||
<div class="paragraph" id="discordp">
|
||||
<h2>Meet the Community</h2>
|
||||
<p>Altitude is home to players young and old from all around the globe, and here, everyone is family. Altitude
|
||||
is your place to get together with friends and relax - and maybe enjoy some survival too. Altitude is
|
||||
intended for older players, but all are welcome!</p>
|
||||
<p>Don't have Discord? Keep up with news and announcements at the <a href="alttd.com/blog">blog</a>.</p>
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<a target="_blank" rel="noopener" href="https://discordapp.com/invite/TGqpzCJ">
|
||||
<div style="margin-top: 30px;" class="button-outer">
|
||||
<span class="button-inner">Join Our Discord</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="section2">
|
||||
<div class="customContainer">
|
||||
<h2 style="color: white; padding-bottom: 35px; font-size: 2.8em;">Survival Shaped by You</h2>
|
||||
<p style="color: white; padding-bottom: 35px; font-size: 1.1em; margin: auto;">Altitude is built by the
|
||||
community, for the community. We've added features requested by our members and several custom plugins to
|
||||
create our "perfect" survival experience.</p>
|
||||
<div class="survivalShapedContainerPlugins">
|
||||
<div class="pluginColumn">
|
||||
<h2>McMMO & MyPet</h2>
|
||||
<p>Two of the most requested plugins on Altitude, level up yourself and your pet with these MMO-based
|
||||
plugins!</p>
|
||||
<a [routerLink]="['/mypet']">
|
||||
<div class="button-outer">
|
||||
<span class="button-inner">MyPet Info</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pluginColumn">
|
||||
<h2>Dynamic map</h2>
|
||||
<p>See the world and the players around it in real-time! The map shows the entire survival world with claims
|
||||
and warps.</p>
|
||||
<a [routerLink]="['/map']">
|
||||
<div class="button-outer">
|
||||
<span class="button-inner">Visit Map</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pluginColumn">
|
||||
<h2>Player Shops</h2>
|
||||
<p>Our economy is built on player shops, encouraging player interaction and putting the control in your
|
||||
hands.</p>
|
||||
<a [routerLink]="['/economy']">
|
||||
<div class="button-outer">
|
||||
<span class="button-inner">Shop Guide</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="removemobile darkmodeSection">
|
||||
<div class="customContainer">
|
||||
<h2 style="padding-bottom: 35px; font-size: 2.8em;">Community Builds</h2>
|
||||
<p style="padding-bottom: 35px; font-size: 1.1em; margin: auto;">Thank you to our brilliant players for sharing
|
||||
their impressive builds. Take a look at some of them here!<br>
|
||||
If you know of any builds that should be featured, please let us know!</p>
|
||||
<div class="sliderWrapper">
|
||||
<div class="sliderContent">
|
||||
<div class="indexSlider">
|
||||
<div id="go-left" (click)="previousSlide()" class="circleBehind goLeft"></div>
|
||||
<div id="go-right" (click)="nextSlide()" class="circleBehind goRight"></div>
|
||||
<div class="display">
|
||||
<span class="slide"
|
||||
[style.background-image]="'url(' + slide + ')'"
|
||||
[style.opacity]="carouselOpacity"
|
||||
[style.display]="'block'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dots">
|
||||
<ng-container *ngFor="let slide of getSlideIndices();">
|
||||
<span class="dot" (click)="setSlide(slide)" [ngClass]="getDotClass(slide)"></span>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section style="background: #202020;">
|
||||
<div class="customContainer">
|
||||
<h2 id="quote" style="color: white; font-family: 'minecraft-text',sans-serif; line-height: 1.3em;">"Great
|
||||
community, great people, great server all round . . . If it can bring back my love for minecraft, it could
|
||||
work wonders for you."</h2>
|
||||
<p style="color: white; margin-top:30px;">- /u/seanhanley1993</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="customContainer">
|
||||
<img ngSrc="/public/img/logos/log.png" alt="Alternative Altitude Server Logo" height="129"
|
||||
width="120">
|
||||
<h2 style="margin: 20px 0; font-size: 1.8em; padding-bottom: 0 !important;">play.alttd.com</h2>
|
||||
<app-copy-ip></app-copy-ip>
|
||||
</div>
|
||||
</section>
|
||||
<a (click)="this.scrollService.scrollToTop()" class="scroll-up-button, active">
|
||||
<span></span>
|
||||
<p style="display: none;">Scroll Down</p>
|
||||
</a>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,359 @@
|
||||
#section1 {
|
||||
background: -webkit-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/town2.jpg");
|
||||
background: -o-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/town2.jpg");
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/town2.jpg");
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#section2 {
|
||||
background: -webkit-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/windmill.png");
|
||||
background: -o-linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/windmill.png");
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url("/public/img/backgrounds/windmill.png");
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
main .container {
|
||||
padding: 80px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.customContainer {
|
||||
width: 80%;
|
||||
max-width: 1020px;
|
||||
margin: auto;
|
||||
padding: 80px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Discord Widget
|
||||
#discord-widget {
|
||||
margin: 0 auto;
|
||||
width: 40%;
|
||||
max-width: 380px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
//Scroll button/anchorpoint
|
||||
#scroll-button {
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
left: 50%;
|
||||
z-index: 2;
|
||||
display: inline-block;
|
||||
-webkit-transform: translate(0, -50%);
|
||||
transform: translate(0, -50%);
|
||||
color: #fff;
|
||||
transition: opacity .3s;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
#scroll-button:hover {
|
||||
opacity: .5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#scroll-button span {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
margin-left: -23px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#scroll-button span::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: -12px 0 0 -8px;
|
||||
border-left: 1px solid #fff;
|
||||
border-bottom: 1px solid #fff;
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#scroll-button span::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
content: '';
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, .1);
|
||||
border-radius: 100%;
|
||||
opacity: 0;
|
||||
-webkit-animation: sdb03 3s infinite;
|
||||
animation: sdb03 3s infinite;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sdb03 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
60% {
|
||||
box-shadow: 0 0 0 60px rgba(255, 255, 255, .1);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sdb03 {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
60% {
|
||||
box-shadow: 0 0 0 60px rgba(255, 255, 255, .1);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Survival Shaped by You section
|
||||
.survivalShapedContainerPlugins {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pluginColumn {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.pluginColumn div {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.pluginColumn h2 {
|
||||
color: var(--white);
|
||||
font-size: 1.4em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pluginColumn p {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
// Community builds section, removed if on mobile
|
||||
.sliderWrapper, .sliderContent {
|
||||
display: flex;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.sliderContent {
|
||||
position: relative;
|
||||
box-shadow: 8px 8px 25px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.indexSlider {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
font-size: 21px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.goLeft, .goRight {
|
||||
display: block;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.goRight:hover, .goLeft:hover {
|
||||
background: rgba(155, 155, 155, 0.3);
|
||||
box-shadow: -2px 0 15px rgba(200, 200, 200, 0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.goLeft {
|
||||
height: 100%;
|
||||
color: white;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.goRight {
|
||||
height: 100%;
|
||||
color: white;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.slide {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.display > span {
|
||||
width: 700px;
|
||||
height: 400px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.dots {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: 3%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dot {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border: 1px solid rgb(255, 255, 255);
|
||||
opacity: 0.5;
|
||||
margin: 0 5px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dot:hover {
|
||||
box-shadow: -2px 0 10px 5px rgba(200, 200, 200, 0.3);
|
||||
background-color: rgba(200, 200, 200, 0.8);
|
||||
border: 1px solid rgb(255, 255, 255);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.active-dot {
|
||||
background-color: var(--link-color);
|
||||
}
|
||||
|
||||
.inactive-dot {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
// Different resolution changes
|
||||
@media (max-width: 1150px) {
|
||||
.container {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.title h2 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#homeh2 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.title {
|
||||
height: calc(100% - 150px);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.container, .customContainer {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
#header-img {
|
||||
width: 45%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.removemobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 690px) {
|
||||
#discordwidget {
|
||||
margin: 0 auto;
|
||||
width: 60%;
|
||||
max-width: 380px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
#discordp {
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
.title h2 {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#homeh2 {
|
||||
font-size: 1.4em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#header-img {
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
.title {
|
||||
height: calc(100% - 110px);
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.columnContainer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main .container {
|
||||
text-align: center;
|
||||
width: 90%;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
width: 80%;
|
||||
margin: 0 auto 30px auto;
|
||||
}
|
||||
|
||||
.survivalShapedContainerPlugins {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pluginColumn {
|
||||
margin: 30px auto;
|
||||
}
|
||||
|
||||
#quote {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {HomeComponent} from './home.component';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HomeComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
import {ALTITUDE_VERSION} from '@custom-types/constant';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {CopyIpComponent} from '@shared-components/copy-ip/copy-ip.component';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent,
|
||||
CopyIpComponent,
|
||||
RouterLink,
|
||||
NgOptimizedImage
|
||||
],
|
||||
selector: 'app-home',
|
||||
templateUrl: './home.component.html',
|
||||
styleUrl: './home.component.scss'
|
||||
})
|
||||
export class HomeComponent implements OnInit {
|
||||
constructor(private titleService: Title, public scrollService: ScrollService) {
|
||||
}
|
||||
|
||||
private slides: string[] = ["/public/img/backgrounds/caruselimage2.png",
|
||||
"/public/img/backgrounds/caruselimage4.png",
|
||||
"/public/img/backgrounds/caruselimage1.png",
|
||||
"/public/img/backgrounds/caruselimage5.png",
|
||||
"/public/img/backgrounds/caruselimage3.png"
|
||||
];
|
||||
|
||||
private slideIndex = 0;
|
||||
public carouselOpacity = 1;
|
||||
public triggerAnimation = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.titleService.setTitle('Survival Minecraft Server on ' + ALTITUDE_VERSION + ' | Altitude Community');
|
||||
this.randomizeSlides()
|
||||
}
|
||||
|
||||
private randomizeSlides(): void {
|
||||
const array = [...this.slides];
|
||||
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
|
||||
this.slides = array;
|
||||
}
|
||||
|
||||
get slide(): string {
|
||||
return this.slides[this.slideIndex];
|
||||
}
|
||||
|
||||
public previousSlide(): void {
|
||||
if (this.slideIndex > 0) {
|
||||
this.setSlide(this.slideIndex - 1);
|
||||
} else {
|
||||
this.setSlide(this.slides.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
public nextSlide(): void {
|
||||
this.setSlide((this.slideIndex + 1) % this.slides.length);
|
||||
}
|
||||
|
||||
public getSlideIndices(): number[] {
|
||||
return Array.from({length: this.slides.length}, (_, i) => i);
|
||||
}
|
||||
|
||||
public setSlide(slide: number): void {
|
||||
this.slideIndex = slide;
|
||||
this.triggerAnimation = true
|
||||
this.carouselOpacity = 0;
|
||||
|
||||
const startTime = Date.now();
|
||||
const duration = 1500;
|
||||
|
||||
const animate = () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
|
||||
this.carouselOpacity = Math.min(elapsed / duration, 1);
|
||||
|
||||
if (this.carouselOpacity < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
public getDotClass(slide: number): string {
|
||||
return slide == this.slideIndex ? 'active-dot' : 'inactive-dot';
|
||||
}
|
||||
|
||||
public scrollToSection(): void {
|
||||
const element = document.getElementById('scrollingpoint');
|
||||
if (element) {
|
||||
element.scrollIntoView({behavior: 'smooth', block: 'start'});
|
||||
} else {
|
||||
console.error('Element with id "scrollingpoint" not found.');
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly ALTITUDE_VERSION = ALTITUDE_VERSION;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<div class="card-div">
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Frames</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="frames-container">
|
||||
<mat-tab-group [selectedIndex]="frames.indexOf(currentFrame)"
|
||||
(selectedIndexChange)="switchFrame(frames[$event])">
|
||||
<mat-tab *ngFor="let frameId of frames" [label]="frameId">
|
||||
<div class="frame-content">
|
||||
<h3>Particles in {{ frameId }}</h3>
|
||||
<div class="particles-list">
|
||||
<div *ngFor="let particle of particleData.frames[frameId]; let i = index" class="particle-item">
|
||||
<span class="particle-item-text">
|
||||
Particle {{ i + 1 }}: ({{ particle.x.toFixed(2) }}, {{ particle.y.toFixed(2) }}
|
||||
, {{ particle.z.toFixed(2) }})
|
||||
</span>
|
||||
<button mat-icon-button (click)="removeParticle(frameId, i)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="highlightParticle(frameId, i)">
|
||||
<mat-icon>lightbulb</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="!particleData.frames[frameId] || particleData.frames[frameId].length === 0"
|
||||
class="no-particles">
|
||||
No particles in this frame. Click on the plane to add particles.
|
||||
</div>
|
||||
</div>
|
||||
<div class="frame-actions">
|
||||
<button mat-raised-button color="warn" (click)="removeFrame(frameId)"
|
||||
[disabled]="frames.length <= 1">
|
||||
Remove Frame
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
<div class="add-frame">
|
||||
<button mat-raised-button color="primary" (click)="addFrame()">
|
||||
Add New Frame
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
.frames-container {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.frame-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.particles-list {
|
||||
height: 550px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.particle-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.particle-item-text {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.particle-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.no-particles {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--color-primairy);
|
||||
}
|
||||
|
||||
.frame-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.add-frame {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card-div {
|
||||
mat-card {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--font-color);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--font-color);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FramesComponent } from './frames.component';
|
||||
|
||||
describe('FramesComponent', () => {
|
||||
let component: FramesComponent;
|
||||
let fixture: ComponentFixture<FramesComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [FramesComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FramesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MatButton, MatIconButton} from "@angular/material/button";
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
|
||||
import {MatTab, MatTabGroup} from "@angular/material/tabs";
|
||||
import {NgForOf, NgIf} from "@angular/common";
|
||||
import {ParticleData} from '../../models/particle.model';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {ParticleManagerService} from '../../services/particle-manager.service';
|
||||
import {FrameManagerService} from '../../services/frame-manager.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-frames',
|
||||
imports: [
|
||||
MatButton,
|
||||
MatCard,
|
||||
MatCardContent,
|
||||
MatCardHeader,
|
||||
MatCardTitle,
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatTab,
|
||||
MatTabGroup,
|
||||
NgForOf,
|
||||
NgIf
|
||||
],
|
||||
templateUrl: './frames.component.html',
|
||||
styleUrl: './frames.component.scss'
|
||||
})
|
||||
export class FramesComponent {
|
||||
|
||||
constructor(
|
||||
private particleManagerService: ParticleManagerService,
|
||||
private frameManagerService: FrameManagerService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the particle data
|
||||
*/
|
||||
public get particleData(): ParticleData {
|
||||
return this.particleManagerService.getParticleData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current frame
|
||||
*/
|
||||
public get currentFrame(): string {
|
||||
return this.particleManagerService.getCurrentFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all frames
|
||||
*/
|
||||
public get frames(): string[] {
|
||||
return this.particleManagerService.getFrames();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new frame
|
||||
*/
|
||||
public addFrame(): void {
|
||||
this.frameManagerService.addFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a different frame
|
||||
*/
|
||||
public switchFrame(frameId: string): void {
|
||||
this.frameManagerService.switchFrame(frameId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a particle
|
||||
*/
|
||||
public removeParticle(frameId: string, index: number): void {
|
||||
this.particleManagerService.removeParticle(frameId, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a frame
|
||||
*/
|
||||
public removeFrame(frameId: string): void {
|
||||
this.frameManagerService.removeFrame(frameId);
|
||||
}
|
||||
|
||||
|
||||
public highlightParticle(frameId: string, i: number) {
|
||||
this.particleManagerService.highlightParticle(frameId, i);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<div class="card-div">
|
||||
<mat-card class="particle-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Particle Properties</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="particle-properties">
|
||||
<div class="property-row">
|
||||
<div class="color-picker">
|
||||
<input type="color" [(ngModel)]="selectedColor">
|
||||
<span>Current color: {{ selectedColor }}</span>
|
||||
</div>
|
||||
<mat-form-field appearance="fill" class="type-field">
|
||||
<mat-label>Select Particle Type</mat-label>
|
||||
<input type="text"
|
||||
placeholder="Search for a particle type"
|
||||
matInput
|
||||
[formControl]="particleTypeControl"
|
||||
[matAutocomplete]="auto">
|
||||
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
|
||||
<mat-option *ngFor="let type of filteredParticleTypes | async" [value]="type">
|
||||
{{ type }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="size-slider">
|
||||
<mat-slider min="0.1" max="4" step="0.1" class="full-width">
|
||||
<input matSliderThumb [(ngModel)]="selectedSize">
|
||||
</mat-slider>
|
||||
<span>Size: {{ selectedSize }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
.particle-card {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.particle-properties {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.property-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.type-field {
|
||||
flex: 1;
|
||||
min-width: 20ch;
|
||||
max-width: 40ch;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.color-picker input[type="color"] {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-div {
|
||||
mat-card {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--font-color);
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.size-slider {
|
||||
width: 95%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--font-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {ParticleComponent} from './particle.component';
|
||||
|
||||
describe('ParticleComponent', () => {
|
||||
let component: ParticleComponent;
|
||||
let fixture: ComponentFixture<ParticleComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ParticleComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ParticleComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {ParticleManagerService} from '../../services/particle-manager.service';
|
||||
import {Particle} from '../../models/particle.model';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
import {MatSelectModule} from '@angular/material/select';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {MatAutocompleteModule} from '@angular/material/autocomplete';
|
||||
import {Observable} from 'rxjs';
|
||||
import {map, startWith} from 'rxjs/operators';
|
||||
import {AsyncPipe, NgForOf} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-particle',
|
||||
imports: [
|
||||
MatCard,
|
||||
MatCardContent,
|
||||
MatCardHeader,
|
||||
MatCardTitle,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MatSliderModule,
|
||||
MatSelectModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatAutocompleteModule,
|
||||
NgForOf,
|
||||
AsyncPipe
|
||||
],
|
||||
templateUrl: './particle.component.html',
|
||||
styleUrl: './particle.component.scss'
|
||||
})
|
||||
export class ParticleComponent implements OnInit {
|
||||
// Available particle types from the enum
|
||||
particleTypes = Object.values(Particle);
|
||||
|
||||
// Form control for the particle type dropdown with filtering
|
||||
particleTypeControl = new FormControl();
|
||||
filteredParticleTypes: Observable<string[]>;
|
||||
|
||||
constructor(
|
||||
private particleManagerService: ParticleManagerService,
|
||||
) {
|
||||
this.filteredParticleTypes = this.particleTypeControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map(value => this._filterParticleTypes(value || ''))
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Initialize the particle type control with the current value
|
||||
this.particleTypeControl.setValue(this.selectedParticle);
|
||||
|
||||
// Update the selected particle when the control value changes
|
||||
this.particleTypeControl.valueChanges.subscribe(value => {
|
||||
if (value && Object.values(Particle).includes(value)) {
|
||||
this.selectedParticle = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _filterParticleTypes(value: string): string[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
return this.particleTypes.filter(type => type.toLowerCase().includes(filterValue));
|
||||
}
|
||||
|
||||
// Display function for the autocomplete
|
||||
displayFn(particle: string): string {
|
||||
return particle ? particle : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected color
|
||||
*/
|
||||
public get selectedColor(): string {
|
||||
return this.particleManagerService.getSelectedColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected color
|
||||
*/
|
||||
public set selectedColor(color: string) {
|
||||
this.particleManagerService.setSelectedColor(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected particle type
|
||||
*/
|
||||
public get selectedParticle(): Particle {
|
||||
return this.particleManagerService.particle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected particle type
|
||||
*/
|
||||
public set selectedParticle(particle: Particle) {
|
||||
this.particleManagerService.particle = particle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected particle size
|
||||
*/
|
||||
public get selectedSize(): number {
|
||||
return this.particleManagerService.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected particle size
|
||||
*/
|
||||
public set selectedSize(size: number) {
|
||||
this.particleManagerService.size = size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<div class="card-div">
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Particle Properties</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Particle Name</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.particle_name" placeholder="Enter particle name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Display Name</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.display_name" placeholder="Enter display name">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Particle Type</mat-label>
|
||||
<mat-select [(ngModel)]="particleData.particle_type">
|
||||
<mat-option *ngFor="let type of particleTypes" [value]="type">{{ type }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Lore</mat-label>
|
||||
<textarea matInput [(ngModel)]="particleData.lore" placeholder="Enter lore"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Display Item</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.display_item" placeholder="Enter display item">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Permission</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.permission" placeholder="Enter permission">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Package Permission</mat-label>
|
||||
<input matInput [(ngModel)]="particleData.package_permission" placeholder="Enter package permission">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Frame Delay</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.frame_delay" placeholder="Enter frame delay">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Repeat</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.repeat" placeholder="Enter repeat count">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Repeat Delay</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.repeat_delay"
|
||||
placeholder="Enter repeat delay">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Random Offset</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="particleData.random_offset"
|
||||
placeholder="Enter random offset">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<mat-checkbox [(ngModel)]="particleData.stationary"><span>Stationary</span></mat-checkbox>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
.form-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card-div {
|
||||
mat-card {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--font-color);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--font-color);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PropertiesComponent } from './properties.component';
|
||||
|
||||
describe('PropertiesComponent', () => {
|
||||
let component: PropertiesComponent;
|
||||
let fixture: ComponentFixture<PropertiesComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PropertiesComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PropertiesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
|
||||
import {MatCheckbox} from "@angular/material/checkbox";
|
||||
import {MatFormField, MatInput, MatLabel} from "@angular/material/input";
|
||||
import {NgForOf} from "@angular/common";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {ParticleData, ParticleType} from '../../models/particle.model';
|
||||
import {MatSelect} from '@angular/material/select';
|
||||
import {MatOption} from '@angular/material/core';
|
||||
import {ParticleManagerService} from '../../services/particle-manager.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-particle-properties',
|
||||
imports: [
|
||||
MatCard,
|
||||
MatCardContent,
|
||||
MatCardHeader,
|
||||
MatCardTitle,
|
||||
MatCheckbox,
|
||||
MatFormField,
|
||||
MatInput,
|
||||
MatLabel,
|
||||
MatOption,
|
||||
MatSelect,
|
||||
NgForOf,
|
||||
ReactiveFormsModule,
|
||||
FormsModule
|
||||
],
|
||||
templateUrl: './properties.component.html',
|
||||
styleUrl: './properties.component.scss'
|
||||
})
|
||||
export class PropertiesComponent {
|
||||
public particleTypes = Object.values(ParticleType);
|
||||
|
||||
constructor(
|
||||
private particleManagerService: ParticleManagerService,
|
||||
) {
|
||||
}
|
||||
|
||||
public get particleData(): ParticleData {
|
||||
return this.particleManagerService.getParticleData();
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<div #rendererContainer class="renderer-container">
|
||||
<div class="plane-controls-overlay">
|
||||
<div>
|
||||
<mat-form-field appearance="outline" style="width: 10ch">
|
||||
<mat-label>Opacity</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="opacity" min="0" max="1" step="0.01" placeholder="">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button mat-mini-fab color="primary" (click)="resetCamera()"
|
||||
matTooltip="Reset camera">
|
||||
<mat-icon>location_searching</mat-icon>
|
||||
</button>
|
||||
|
||||
<button mat-mini-fab color="primary" (click)="togglePlaneLock()"
|
||||
[matTooltip]="isPlaneLocked ? 'Unlock Plane' : 'Lock Plane'">
|
||||
<mat-icon>{{ isPlaneLocked ? 'lock' : 'lock_open' }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isPlaneLocked" class="plane-orientation-buttons">
|
||||
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_ABOVE)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_ABOVE"
|
||||
matTooltip="Vertical Above">
|
||||
<mat-icon>arrow_upward</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab color="warn" (click)="setPlaneOrientation(planeOrientations.VERTICAL_BELOW)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.VERTICAL_BELOW"
|
||||
matTooltip="Vertical Below">
|
||||
<mat-icon>arrow_downward</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_FRONT)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_FRONT"
|
||||
matTooltip="Horizontal Front">
|
||||
<mat-icon>arrow_forward</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab color="primary" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_BEHIND)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_BEHIND"
|
||||
matTooltip="Horizontal Behind">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_RIGHT)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_RIGHT"
|
||||
matTooltip="Horizontal Right">
|
||||
<mat-icon>arrow_right</mat-icon>
|
||||
</button>
|
||||
<button mat-mini-fab color="accent" (click)="setPlaneOrientation(planeOrientations.HORIZONTAL_LEFT)"
|
||||
[class.active]="currentPlaneOrientation === planeOrientations.HORIZONTAL_LEFT"
|
||||
matTooltip="Horizontal Left">
|
||||
<mat-icon>arrow_left</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
.renderer-container {
|
||||
height: 1000px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-primairy);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative; /* Added for absolute positioning of overlay */
|
||||
}
|
||||
|
||||
.plane-controls-overlay {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.plane-orientation-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.plane-orientation-buttons button {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.plane-orientation-buttons button:hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.plane-orientation-buttons button.active {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 10px var(--color-tertiary);
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RenderContainerComponent } from './render-container.component';
|
||||
|
||||
describe('RenderContainerComponent', () => {
|
||||
let component: RenderContainerComponent;
|
||||
let fixture: ComponentFixture<RenderContainerComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RenderContainerComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RenderContainerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import {AfterViewInit, Component, ElementRef, OnDestroy, ViewChild} from '@angular/core';
|
||||
import {MatMiniFabButton} from '@angular/material/button';
|
||||
import {NgIf} from '@angular/common';
|
||||
import {IntersectionPlaneService, PlaneOrientation} from '../../services/intersection-plane.service';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {MatTooltip} from '@angular/material/tooltip';
|
||||
import {RendererService} from '../../services/renderer.service';
|
||||
import {PlayerModelService} from '../../services/player-model.service';
|
||||
import {InputHandlerService} from '../../services/input-handler.service';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||
|
||||
@Component({
|
||||
selector: 'app-render-container',
|
||||
imports: [
|
||||
MatIcon,
|
||||
MatMiniFabButton,
|
||||
MatTooltip,
|
||||
NgIf,
|
||||
FormsModule,
|
||||
MatInput,
|
||||
MatFormField,
|
||||
MatLabel
|
||||
],
|
||||
templateUrl: './render-container.component.html',
|
||||
styleUrl: './render-container.component.scss'
|
||||
})
|
||||
export class RenderContainerComponent implements AfterViewInit, OnDestroy {
|
||||
@ViewChild('rendererContainer') rendererContainer!: ElementRef;
|
||||
|
||||
constructor(
|
||||
private intersectionPlaneService: IntersectionPlaneService,
|
||||
private playerModelService: PlayerModelService,
|
||||
private inputHandlerService: InputHandlerService,
|
||||
private rendererService: RendererService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.initializeScene();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when component is destroyed
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (this.rendererService.renderer) {
|
||||
this.inputHandlerService.cleanup(this.rendererService.renderer.domElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the 3D scene and all related components
|
||||
*/
|
||||
private initializeScene(): void {
|
||||
this.rendererService.initializeRenderer(this.rendererContainer);
|
||||
this.playerModelService.loadSkinTexture('/public/img/skins/steve.png')
|
||||
.then(() => {
|
||||
// Then create the player model with the texture applied
|
||||
this.playerModelService.createPlayerModel();
|
||||
});
|
||||
this.intersectionPlaneService.createIntersectionPlane();
|
||||
this.inputHandlerService.initializeInputHandlers(this.rendererService.renderer.domElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation loop
|
||||
*/
|
||||
private animate(): void {
|
||||
requestAnimationFrame(this.animate.bind(this));
|
||||
this.intersectionPlaneService.updatePlaneOrientation(this.rendererService.camera);
|
||||
this.rendererService.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the plane is locked
|
||||
*/
|
||||
public get isPlaneLocked(): boolean {
|
||||
return this.intersectionPlaneService.isPlaneLocked();
|
||||
}
|
||||
|
||||
public set opacity(opacity: number) {
|
||||
this.intersectionPlaneService.currentOpacity = opacity;
|
||||
}
|
||||
|
||||
public get opacity(): number {
|
||||
return this.intersectionPlaneService.currentOpacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the plane locked state
|
||||
*/
|
||||
public togglePlaneLock(): void {
|
||||
const newLockedState = !this.isPlaneLocked;
|
||||
this.intersectionPlaneService.setPlaneLocked(newLockedState);
|
||||
}
|
||||
|
||||
public resetCamera(): void {
|
||||
this.rendererService.resetCamera();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current plane orientation
|
||||
*/
|
||||
public get currentPlaneOrientation(): PlaneOrientation {
|
||||
return this.intersectionPlaneService.getCurrentOrientation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the plane orientation
|
||||
*/
|
||||
public setPlaneOrientation(orientation: PlaneOrientation): void {
|
||||
this.intersectionPlaneService.setPlaneOrientation(orientation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available plane orientations
|
||||
*/
|
||||
public get planeOrientations(): typeof PlaneOrientation {
|
||||
return PlaneOrientation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Defines the types of particles available in the system
|
||||
*/
|
||||
export enum Particle {
|
||||
DUST = 'DUST',
|
||||
DUST_COLOR_TRANSITION = 'DUST_COLOR_TRANSITION',
|
||||
TINTED_LEAVES = 'TINTED_LEAVES'
|
||||
// Other particle types can be added later
|
||||
}
|
||||
|
||||
export enum ParticleType {
|
||||
HEAD = 'HEAD',
|
||||
TRAIL = 'TRAIL',
|
||||
BREAK_PLACE_BLOCK = 'BREAK_PLACE_BLOCK',
|
||||
DEATH = 'DEATH',
|
||||
KILL = 'KILL',
|
||||
CLICK_BLOCK = 'CLICK_BLOCK',
|
||||
TELEPORT_ARRIVE = 'TELEPORT',
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single particle's information
|
||||
*/
|
||||
export interface ParticleInfo {
|
||||
particle_type: Particle;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
extra?: number;
|
||||
color?: string;
|
||||
color_gradient_end?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the complete particle data structure
|
||||
*/
|
||||
export interface ParticleData {
|
||||
particle_name: string;
|
||||
display_name: string;
|
||||
particle_type: string;
|
||||
lore: string;
|
||||
display_item: string;
|
||||
permission: string;
|
||||
package_permission: string;
|
||||
frame_delay: number;
|
||||
repeat: number;
|
||||
repeat_delay: number;
|
||||
random_offset: number;
|
||||
stationary: boolean;
|
||||
frames: {
|
||||
[frameId: string]: ParticleInfo[];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<app-header current_page="particles" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Particle Creator</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="column">
|
||||
<div class="renderer-section column">
|
||||
<div class="flex row">
|
||||
<div class="flex side-column">
|
||||
<app-particle-properties></app-particle-properties>
|
||||
</div>
|
||||
<div class="flex middle-column">
|
||||
<app-render-container></app-render-container>
|
||||
<div class="plane-controls">
|
||||
<label>Plane Position (Z-axis):</label>
|
||||
<mat-slider [min]="minOffset" [max]="maxOffset" step="1" #planeSlider>
|
||||
<input matSliderThumb [(ngModel)]="planePosition" (input)="updatePlanePosition($event)">
|
||||
</mat-slider>
|
||||
<span>{{ planePosition }} offset from center</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex side-column">
|
||||
<app-particle></app-particle>
|
||||
<app-frames></app-frames>
|
||||
<div>
|
||||
<button mat-fab extended (click)="copyJson()">
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
Copy JSON to clipboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
@@ -0,0 +1,125 @@
|
||||
.renderer-section {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.side-column {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.middle-column {
|
||||
flex: 2;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
label, span {
|
||||
color: var(--font-color);
|
||||
}
|
||||
|
||||
.plane-controls {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: var(--color-primairy);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.plane-controls mat-slider {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:host ::ng-deep {
|
||||
.mdc-text-field--outlined {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.mdc-text-field--outlined .mdc-floating-label,
|
||||
.mdc-text-field--outlined .mdc-text-field__input,
|
||||
.mat-mdc-form-field-label,
|
||||
.mat-mdc-select-value-text,
|
||||
.mat-mdc-select-arrow,
|
||||
.mat-mdc-checkbox-label,
|
||||
.mat-mdc-card-header,
|
||||
.mat-mdc-card-title,
|
||||
.mat-mdc-card-content {
|
||||
color: var(--font-color) !important;
|
||||
}
|
||||
|
||||
.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__leading,
|
||||
.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__notch,
|
||||
.mdc-text-field--outlined .mdc-notched-outline .mdc-notched-outline__trailing {
|
||||
border-color: var(--font-color) !important;
|
||||
}
|
||||
|
||||
// Fix for dropdown menu
|
||||
.mat-mdc-select-panel {
|
||||
background-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-option {
|
||||
color: var(--font-color) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-option:hover:not(.mdc-list-item--disabled) {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-option.mat-mdc-option-active {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-select-panel {
|
||||
background-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-option {
|
||||
color: var(--font-color) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-option:hover:not(.mdc-list-item--disabled) {
|
||||
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-option.mat-mdc-option-active {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab {
|
||||
color: var(--font-color) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-header {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.mat-mdc-tab-label-container {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.mdc-tab__content .mdc-tab__text-label {
|
||||
color: var(--font-color) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-label, .mat-mdc-tab-link {
|
||||
color: var(--font-color) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-group.mat-primary .mat-mdc-tab:not(.mat-mdc-tab-disabled) .mdc-tab-indicator__content--underline {
|
||||
border-color: var(--font-color) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-tab-body-content {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
color: var(--font-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,131 @@
|
||||
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {MatButtonModule} from '@angular/material/button';
|
||||
import {MatInputModule} from '@angular/material/input';
|
||||
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatSelectModule} from '@angular/material/select';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
import {MatCheckboxModule} from '@angular/material/checkbox';
|
||||
import {MatTabsModule} from '@angular/material/tabs';
|
||||
import {MatCardModule} from '@angular/material/card';
|
||||
import {MatIconModule} from '@angular/material/icon';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
// Services
|
||||
import {IntersectionPlaneService} from './services/intersection-plane.service';
|
||||
import {ParticleManagerService} from './services/particle-manager.service';
|
||||
|
||||
// Models
|
||||
import {PropertiesComponent} from './components/properties/properties.component';
|
||||
import {ParticleComponent} from './components/particle/particle.component';
|
||||
import {FramesComponent} from './components/frames/frames.component';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {RenderContainerComponent} from './components/render-container/render-container.component';
|
||||
import {ParticlesService} from '@api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-particles',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
MatButtonModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatSliderModule,
|
||||
MatCheckboxModule,
|
||||
MatTabsModule,
|
||||
MatCardModule,
|
||||
MatIconModule,
|
||||
HeaderComponent,
|
||||
PropertiesComponent,
|
||||
ParticleComponent,
|
||||
FramesComponent,
|
||||
RenderContainerComponent,
|
||||
],
|
||||
templateUrl: './particles.component.html',
|
||||
styleUrl: './particles.component.scss'
|
||||
})
|
||||
export class ParticlesComponent {
|
||||
@ViewChild('planeSlider') planeSlider!: ElementRef;
|
||||
|
||||
constructor(
|
||||
private intersectionPlaneService: IntersectionPlaneService,
|
||||
private particleManagerService: ParticleManagerService,
|
||||
private matSnackBar: MatSnackBar,
|
||||
private particlesService: ParticlesService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update plane position based on slider
|
||||
*/
|
||||
public updatePlanePosition(event: Event): void {
|
||||
const slider = event.target as HTMLInputElement;
|
||||
const value = Number(slider.value);
|
||||
this.intersectionPlaneService.updatePlanePosition(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current plane position
|
||||
*/
|
||||
public get planePosition(): number {
|
||||
return this.intersectionPlaneService.getPlanePosition();
|
||||
}
|
||||
|
||||
public set planePosition(newPlanePosition: number) {
|
||||
this.intersectionPlaneService.updatePlanePosition(newPlanePosition);
|
||||
}
|
||||
|
||||
public get maxOffset(): number {
|
||||
return this.intersectionPlaneService.getMaxOffset();
|
||||
}
|
||||
|
||||
public get minOffset(): number {
|
||||
return this.intersectionPlaneService.getMinOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JSON output
|
||||
*/
|
||||
public generateJson(): string {
|
||||
return this.particleManagerService.generateJson();
|
||||
}
|
||||
|
||||
public getJsonFile(): Blob {
|
||||
const jsonContent = this.generateJson();
|
||||
return new Blob([jsonContent], {type: 'application/json'});
|
||||
}
|
||||
|
||||
public saveJsonToFile(): void {
|
||||
const jsonContent = this.generateJson();
|
||||
const blob = new Blob([jsonContent], {type: 'application/json'});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary link element
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'particle-data.json';
|
||||
|
||||
// Append to the document, click it, and remove it
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Clean up
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
this.matSnackBar.open('JSON file downloaded', '', {duration: 2000});
|
||||
}
|
||||
|
||||
public copyJson() {
|
||||
navigator.clipboard.writeText(this.generateJson()).then(() => {
|
||||
this.matSnackBar.open('Copied to clipboard', '', {duration: 2000})
|
||||
});
|
||||
//TODO validation
|
||||
this.particlesService.saveFile(this.particleManagerService.getParticleData().particle_name, this.getJsonFile());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ParticleManagerService } from './particle-manager.service';
|
||||
|
||||
/**
|
||||
* Service responsible for managing animation frames
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class FrameManagerService {
|
||||
|
||||
constructor(private particleManager: ParticleManagerService) {}
|
||||
|
||||
/**
|
||||
* Adds a new frame
|
||||
*/
|
||||
addFrame(): void {
|
||||
const frames = this.particleManager.getFrames();
|
||||
const frameId = `frame${frames.length + 1}`;
|
||||
|
||||
frames.push(frameId);
|
||||
this.particleManager.setFrames(frames);
|
||||
|
||||
const particleData = this.particleManager.getParticleData();
|
||||
particleData.frames[frameId] = [];
|
||||
this.particleManager.setParticleData(particleData);
|
||||
|
||||
this.switchFrame(frameId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to a different frame
|
||||
*/
|
||||
switchFrame(frameId: string): void {
|
||||
this.particleManager.setCurrentFrame(frameId);
|
||||
this.particleManager.clearParticleVisuals();
|
||||
this.particleManager.renderFrameParticles(frameId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a frame
|
||||
*/
|
||||
removeFrame(frameId: string): void {
|
||||
const frames = this.particleManager.getFrames();
|
||||
const index = frames.indexOf(frameId);
|
||||
|
||||
if (index !== -1) {
|
||||
frames.splice(index, 1);
|
||||
this.particleManager.setFrames(frames);
|
||||
|
||||
const particleData = this.particleManager.getParticleData();
|
||||
delete particleData.frames[frameId];
|
||||
this.particleManager.setParticleData(particleData);
|
||||
|
||||
// Switch to first frame if we removed the current one
|
||||
if (frameId === this.particleManager.getCurrentFrame() && frames.length > 0) {
|
||||
this.switchFrame(frames[0]);
|
||||
} else if (frames.length === 0) {
|
||||
// If no frames left, add one
|
||||
this.addFrame();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Injectable, ElementRef } from '@angular/core';
|
||||
import * as THREE from 'three';
|
||||
import { RendererService } from './renderer.service';
|
||||
import { IntersectionPlaneService } from './intersection-plane.service';
|
||||
import { ParticleManagerService } from './particle-manager.service';
|
||||
|
||||
/**
|
||||
* Service responsible for handling user input interactions
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class InputHandlerService {
|
||||
private raycaster = new THREE.Raycaster();
|
||||
private mouse = new THREE.Vector2();
|
||||
private isDragging = false;
|
||||
private mouseDownTime = 0;
|
||||
|
||||
constructor(
|
||||
private rendererService: RendererService,
|
||||
private intersectionPlaneService: IntersectionPlaneService,
|
||||
private particleManagerService: ParticleManagerService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initializes input event listeners
|
||||
*/
|
||||
initializeInputHandlers(rendererElement: HTMLElement): void {
|
||||
rendererElement.addEventListener('mousedown', this.onMouseDown.bind(this));
|
||||
rendererElement.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||
rendererElement.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||
window.addEventListener('resize', this.onWindowResize.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse down event
|
||||
*/
|
||||
private onMouseDown(event: MouseEvent): void {
|
||||
this.isDragging = false;
|
||||
this.mouseDownTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse up event
|
||||
*/
|
||||
private onMouseUp(event: MouseEvent): void {
|
||||
// If mouse was down for less than 200ms and didn't move much, consider it a click, not a drag
|
||||
if (Date.now() - this.mouseDownTime < 200 && !this.isDragging) {
|
||||
this.handlePlaneClick(event);
|
||||
}
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse move event
|
||||
*/
|
||||
private onMouseMove(event: MouseEvent): void {
|
||||
// If mouse moves while button is pressed, it's a drag
|
||||
if (event.buttons > 0) {
|
||||
this.isDragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouse click on the plane
|
||||
*/
|
||||
private handlePlaneClick(event: MouseEvent): void {
|
||||
// Calculate mouse position in normalized device coordinates
|
||||
const rect = this.rendererService.renderer.domElement.getBoundingClientRect();
|
||||
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
// Update the picking ray with the camera and mouse position
|
||||
this.raycaster.setFromCamera(this.mouse, this.rendererService.camera);
|
||||
|
||||
// Calculate objects intersecting the picking ray
|
||||
const intersects = this.raycaster.intersectObject(this.intersectionPlaneService.getIntersectionPlane());
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const point = intersects[0].point;
|
||||
this.particleManagerService.addParticle(point.x, point.y, point.z);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles window resize event
|
||||
*/
|
||||
private onWindowResize(): void {
|
||||
// This is delegated to the renderer service
|
||||
const container = this.rendererService.renderer.domElement.parentElement;
|
||||
if (container) {
|
||||
this.rendererService.onWindowResize(new ElementRef(container));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes event listeners
|
||||
*/
|
||||
cleanup(rendererElement: HTMLElement): void {
|
||||
rendererElement.removeEventListener('mousedown', this.onMouseDown.bind(this));
|
||||
rendererElement.removeEventListener('mouseup', this.onMouseUp.bind(this));
|
||||
rendererElement.removeEventListener('mousemove', this.onMouseMove.bind(this));
|
||||
window.removeEventListener('resize', this.onWindowResize.bind(this));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import * as THREE from 'three';
|
||||
import {RendererService} from './renderer.service';
|
||||
|
||||
/**
|
||||
* Represents the possible orientations of the intersection plane
|
||||
*/
|
||||
export enum PlaneOrientation {
|
||||
VERTICAL_ABOVE,
|
||||
VERTICAL_BELOW,
|
||||
HORIZONTAL_FRONT,
|
||||
HORIZONTAL_BEHIND,
|
||||
HORIZONTAL_RIGHT,
|
||||
HORIZONTAL_LEFT
|
||||
}
|
||||
|
||||
/**
|
||||
* Service responsible for managing the intersection plane
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class IntersectionPlaneService {
|
||||
private intersectionPlane!: THREE.Mesh;
|
||||
private planePosition: number = 0; // Position in 1/16th of a block
|
||||
private currentOrientation: PlaneOrientation = PlaneOrientation.HORIZONTAL_FRONT;
|
||||
private planeLocked: boolean = false;
|
||||
private opacity: number = 0.05;
|
||||
|
||||
constructor(private rendererService: RendererService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the intersection plane and adds it to the scene
|
||||
*/
|
||||
createIntersectionPlane(): THREE.Mesh {
|
||||
const planeGeometry = new THREE.PlaneGeometry(3, 3);
|
||||
const planeMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0x00AA00,
|
||||
transparent: true,
|
||||
opacity: 0.05,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
this.intersectionPlane = new THREE.Mesh(planeGeometry, planeMaterial);
|
||||
this.intersectionPlane.position.z = 0;
|
||||
// Center the plane vertically with the player (player is about 2 blocks tall)
|
||||
this.intersectionPlane.position.y = 1;
|
||||
this.rendererService.scene.add(this.intersectionPlane);
|
||||
this.intersectionPlane.renderOrder = 1;
|
||||
|
||||
return this.intersectionPlane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the plane orientation based on camera position
|
||||
*/
|
||||
private determinePlaneOrientation(camera: THREE.Camera): PlaneOrientation {
|
||||
// Check if camera is looking from above or below first
|
||||
const verticalAngle = Math.atan2(
|
||||
camera.position.y,
|
||||
Math.sqrt(camera.position.x * camera.position.x + camera.position.z * camera.position.z)
|
||||
);
|
||||
|
||||
// Threshold angle for considering the camera to be above/below (about 45 degrees)
|
||||
const verticalThreshold = Math.PI / 4;
|
||||
|
||||
if (verticalAngle > verticalThreshold) {
|
||||
return PlaneOrientation.VERTICAL_ABOVE;
|
||||
} else if (verticalAngle < -verticalThreshold) {
|
||||
return PlaneOrientation.VERTICAL_BELOW;
|
||||
} else {
|
||||
// Calculate the angle between camera and player (in the XZ plane)
|
||||
const cameraAngle = Math.atan2(
|
||||
camera.position.x,
|
||||
camera.position.z
|
||||
);
|
||||
|
||||
// Determine which quadrant the camera is in with a 45-degree offset
|
||||
const quadrant = Math.floor((cameraAngle + Math.PI + Math.PI / 4) / (Math.PI / 2)) % 4;
|
||||
|
||||
switch (quadrant) {
|
||||
case 0:
|
||||
return PlaneOrientation.HORIZONTAL_FRONT;
|
||||
case 1:
|
||||
return PlaneOrientation.HORIZONTAL_RIGHT;
|
||||
case 2:
|
||||
return PlaneOrientation.HORIZONTAL_BEHIND;
|
||||
case 3:
|
||||
return PlaneOrientation.HORIZONTAL_LEFT;
|
||||
default:
|
||||
return PlaneOrientation.HORIZONTAL_FRONT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the plane orientation based on camera position
|
||||
*/
|
||||
public updatePlaneOrientation(camera: THREE.Camera): void {
|
||||
if (!this.intersectionPlane) return;
|
||||
|
||||
if (!this.planeLocked) {
|
||||
this.currentOrientation = this.determinePlaneOrientation(camera);
|
||||
}
|
||||
|
||||
this.updateIntersectionPlaneOrientation()
|
||||
|
||||
//Restrict plane position to the new bounds and update it
|
||||
this.planePosition = Math.max(this.getMinOffset(), Math.min(this.getMaxOffset(), this.planePosition));
|
||||
this.updatePlanePosition(this.planePosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the plane position based on slider value
|
||||
*/
|
||||
public updatePlanePosition(value: number): void {
|
||||
this.planePosition = value;
|
||||
// Convert from 1/16th block to Three.js units
|
||||
const position = (this.planePosition / 16)
|
||||
|
||||
this.intersectionPlane.position.y = 0.8;
|
||||
this.intersectionPlane.position.x = 0;
|
||||
this.intersectionPlane.position.z = 0;
|
||||
|
||||
// Position based on the current orientation
|
||||
switch (this.currentOrientation) {
|
||||
case PlaneOrientation.VERTICAL_ABOVE:
|
||||
this.intersectionPlane.position.y = 0.8 - position;
|
||||
break;
|
||||
case PlaneOrientation.VERTICAL_BELOW:
|
||||
this.intersectionPlane.position.y = 0.8 + position;
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||
this.intersectionPlane.position.z = position;
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||
this.intersectionPlane.position.z = -position;
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||
this.intersectionPlane.position.x = position;
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||
this.intersectionPlane.position.x = -position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the plane material color
|
||||
*/
|
||||
private updatePlaneMaterial(color: number): void {
|
||||
this.intersectionPlane.material = new THREE.MeshBasicMaterial({
|
||||
color: color,
|
||||
transparent: true,
|
||||
opacity: this.opacity,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
}
|
||||
|
||||
public set currentOpacity(opacity: number) {
|
||||
this.opacity = opacity;
|
||||
}
|
||||
|
||||
public get currentOpacity(): number {
|
||||
return this.opacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the intersection plane
|
||||
*/
|
||||
public getIntersectionPlane(): THREE.Mesh {
|
||||
return this.intersectionPlane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current plane position
|
||||
*/
|
||||
public getPlanePosition(): number {
|
||||
return this.planePosition;
|
||||
}
|
||||
|
||||
public getMaxOffset(): number {
|
||||
switch (this.currentOrientation) {
|
||||
case PlaneOrientation.VERTICAL_ABOVE:
|
||||
case PlaneOrientation.VERTICAL_BELOW:
|
||||
return 16;
|
||||
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||
return 8;
|
||||
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
public getMinOffset(): number {
|
||||
return this.getMaxOffset() * -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current plane orientation
|
||||
*/
|
||||
public getCurrentOrientation(): PlaneOrientation {
|
||||
return this.currentOrientation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the plane orientation manually
|
||||
*/
|
||||
public setPlaneOrientation(orientation: PlaneOrientation): void {
|
||||
this.currentOrientation = orientation;
|
||||
this.updateIntersectionPlaneOrientation();
|
||||
this.updatePlanePosition(this.planePosition);
|
||||
}
|
||||
|
||||
private updateIntersectionPlaneOrientation() {
|
||||
switch (this.currentOrientation) {
|
||||
case PlaneOrientation.VERTICAL_ABOVE:
|
||||
this.intersectionPlane.rotation.x = -Math.PI / 2;
|
||||
this.intersectionPlane.rotation.y = 0;
|
||||
this.updatePlaneMaterial(0xAA0000);
|
||||
break;
|
||||
case PlaneOrientation.VERTICAL_BELOW:
|
||||
this.intersectionPlane.rotation.x = Math.PI / 2;
|
||||
this.intersectionPlane.rotation.y = 0;
|
||||
this.updatePlaneMaterial(0xAA0000);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_FRONT:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = 0;
|
||||
this.updatePlaneMaterial(0x00AA00);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_BEHIND:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = Math.PI;
|
||||
this.updatePlaneMaterial(0x00AA00);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_RIGHT:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = Math.PI / 2;
|
||||
this.updatePlaneMaterial(0x0000AA);
|
||||
break;
|
||||
case PlaneOrientation.HORIZONTAL_LEFT:
|
||||
this.intersectionPlane.rotation.x = 0;
|
||||
this.intersectionPlane.rotation.y = -Math.PI / 2;
|
||||
this.updatePlaneMaterial(0x0000AA);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the plane orientation is locked
|
||||
*/
|
||||
public isPlaneLocked(): boolean {
|
||||
return this.planeLocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the plane orientation is locked
|
||||
*/
|
||||
public setPlaneLocked(locked: boolean): void {
|
||||
this.planeLocked = locked;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import * as THREE from 'three';
|
||||
import {RendererService} from './renderer.service';
|
||||
import {Particle, ParticleData, ParticleInfo, ParticleType} from '../models/particle.model';
|
||||
|
||||
/**
|
||||
* Service responsible for managing particles in the scene
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ParticleManagerService {
|
||||
private particles: THREE.Mesh[] = [];
|
||||
private particleData: ParticleData = {
|
||||
particle_name: '',
|
||||
display_name: '',
|
||||
particle_type: ParticleType.TRAIL,
|
||||
lore: '',
|
||||
display_item: 'DIRT',
|
||||
permission: '',
|
||||
package_permission: '',
|
||||
frame_delay: 1,
|
||||
repeat: 1,
|
||||
repeat_delay: 0,
|
||||
random_offset: 0,
|
||||
stationary: true,
|
||||
frames: {
|
||||
'frame1': []
|
||||
}
|
||||
};
|
||||
private currentFrame: string = 'frame1';
|
||||
private frames: string[] = ['frame1'];
|
||||
private selectedColor: string = '#ff0000';
|
||||
private selectedParticle: Particle = Particle.DUST;
|
||||
private selectedSize: number = 1;
|
||||
|
||||
constructor(private rendererService: RendererService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a particle at the specified position
|
||||
*/
|
||||
addParticle(x: number, y: number, z: number): void {
|
||||
// Create a visual representation of the particle
|
||||
const particleGeometry = new THREE.SphereGeometry(0.03 * this.selectedSize, 16, 16);
|
||||
const particleMaterial = new THREE.MeshBasicMaterial({color: this.selectedColor});
|
||||
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
|
||||
|
||||
particleMesh.position.set(x, y, z);
|
||||
this.rendererService.scene.add(particleMesh);
|
||||
this.particles.push(particleMesh);
|
||||
|
||||
// Add to particle data
|
||||
const hexColor = this.selectedColor.replace('#', '');
|
||||
|
||||
//TODO make this work for more than just type DUST
|
||||
const particleInfo: ParticleInfo = {
|
||||
particle_type: this.selectedParticle,
|
||||
x: x,
|
||||
y: y,
|
||||
z: z,
|
||||
color: hexColor,
|
||||
// color_gradient_end: hexColor2,
|
||||
extra: 1,
|
||||
size: this.selectedSize
|
||||
};
|
||||
|
||||
if (!this.particleData.frames[this.currentFrame]) {
|
||||
this.particleData.frames[this.currentFrame] = [];
|
||||
}
|
||||
|
||||
this.particleData.frames[this.currentFrame].push(particleInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all particle visuals from the scene
|
||||
*/
|
||||
clearParticleVisuals(): void {
|
||||
for (const particle of this.particles) {
|
||||
this.rendererService.scene.remove(particle);
|
||||
}
|
||||
this.particles = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders particles for a specific frame
|
||||
*/
|
||||
renderFrameParticles(frameId: string): void {
|
||||
if (!this.particleData.frames[frameId]) return;
|
||||
|
||||
for (const particleInfo of this.particleData.frames[frameId]) {
|
||||
const particleGeometry = new THREE.SphereGeometry(0.03 * (particleInfo.size ?? 1), 16, 16);
|
||||
|
||||
const color = this.getColor(particleInfo);
|
||||
const particleMaterial = new THREE.MeshBasicMaterial({color});
|
||||
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
|
||||
|
||||
particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z);
|
||||
this.rendererService.scene.add(particleMesh);
|
||||
this.particles.push(particleMesh);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a particle from a specific frame
|
||||
*/
|
||||
removeParticle(frameId: string, index: number): void {
|
||||
if (this.particleData.frames[frameId] && this.particleData.frames[frameId].length > index) {
|
||||
this.particleData.frames[frameId].splice(index, 1);
|
||||
|
||||
// Update visuals if this is the current frame
|
||||
if (frameId === this.currentFrame) {
|
||||
this.clearParticleVisuals();
|
||||
this.renderFrameParticles(frameId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
highlightParticle(frameId: string, index: number): void {
|
||||
if (!(this.particleData.frames[frameId] && this.particleData.frames[frameId].length > index)) {
|
||||
return;
|
||||
}
|
||||
const particleInfo = this.particleData.frames[frameId][index];
|
||||
const color = this.getColor(particleInfo);
|
||||
const particleMaterial = new THREE.MeshBasicMaterial({color});
|
||||
const particleGeometry = new THREE.SphereGeometry(0.03 * (particleInfo.size ?? 1), 16, 16);
|
||||
const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
|
||||
particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z);
|
||||
this.rendererService.scene.add(particleMesh);
|
||||
this.particles.push(particleMesh);
|
||||
this.animatePulse(particleMesh, 3, () => {
|
||||
this.rendererService.scene.remove(particleMesh);
|
||||
this.clearParticleVisuals();
|
||||
this.renderFrameParticles(this.currentFrame);
|
||||
});
|
||||
}
|
||||
|
||||
private animatePulse(mesh: THREE.Mesh, cycles: number, onComplete: () => void): void {
|
||||
const duration = 300;
|
||||
const maxScale = 0.08 / 0.03;
|
||||
const startTime = performance.now();
|
||||
|
||||
const animate = (time: number) => {
|
||||
const elapsed = (time - startTime) % duration;
|
||||
const t = elapsed / (duration / 2);
|
||||
const scaleFactor = t <= 1 ? 1 + (maxScale - 1) * t : maxScale - (maxScale - 1) * (t - 1);
|
||||
mesh.scale.setScalar(scaleFactor);
|
||||
|
||||
if (time - startTime >= duration * cycles) {
|
||||
onComplete();
|
||||
} else {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
private getColor(particleInfo: ParticleInfo) {
|
||||
if (particleInfo.color) {
|
||||
const r = parseInt(particleInfo.color.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(particleInfo.color.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(particleInfo.color.substring(4, 6), 16) / 255;
|
||||
return new THREE.Color(r, g, b);
|
||||
} else {
|
||||
return new THREE.Color(255, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selected color for new particles
|
||||
*/
|
||||
setSelectedColor(color: string): void {
|
||||
this.selectedColor = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the selected color
|
||||
*/
|
||||
getSelectedColor(): string {
|
||||
return this.selectedColor;
|
||||
}
|
||||
|
||||
public get particle(): Particle {
|
||||
return this.selectedParticle;
|
||||
}
|
||||
|
||||
public set particle(selectedParticle: Particle) {
|
||||
this.selectedParticle = selectedParticle;
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.selectedSize;
|
||||
}
|
||||
|
||||
public set size(selectedSize: number) {
|
||||
this.selectedSize = selectedSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the particle data
|
||||
*/
|
||||
getParticleData(): ParticleData {
|
||||
return this.particleData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the particle data
|
||||
*/
|
||||
setParticleData(data: ParticleData): void {
|
||||
this.particleData = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current frame
|
||||
*/
|
||||
getCurrentFrame(): string {
|
||||
return this.currentFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current frame
|
||||
*/
|
||||
setCurrentFrame(frameId: string): void {
|
||||
this.currentFrame = frameId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all frames
|
||||
*/
|
||||
getFrames(): string[] {
|
||||
return this.frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all frames
|
||||
*/
|
||||
setFrames(frames: string[]): void {
|
||||
this.frames = frames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates JSON output of the particle data
|
||||
*/
|
||||
generateJson(): string {
|
||||
return JSON.stringify(this.particleData, null, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import * as THREE from 'three';
|
||||
import {RendererService} from './renderer.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PlayerModelService {
|
||||
private playerModel!: THREE.Group;
|
||||
private skinTexture!: THREE.Texture;
|
||||
private textureLoaded = false;
|
||||
|
||||
constructor(private rendererService: RendererService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a Minecraft skin texture from a URL
|
||||
* @param textureUrl The URL of the skin texture to load
|
||||
* @returns A promise that resolves when the texture is loaded
|
||||
*/
|
||||
loadSkinTexture(textureUrl: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const loader = new THREE.TextureLoader();
|
||||
loader.load(textureUrl, (texture) => {
|
||||
// Set texture parameters
|
||||
texture.magFilter = THREE.NearestFilter;
|
||||
texture.minFilter = THREE.NearestFilter;
|
||||
this.skinTexture = texture;
|
||||
this.textureLoaded = true;
|
||||
|
||||
// If the player model already exists, rebuild it with textures
|
||||
if (this.playerModel) {
|
||||
// Remove old model
|
||||
this.rendererService.scene.remove(this.playerModel);
|
||||
// Create new model with textures
|
||||
this.createPlayerModel();
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a player model with Minecraft-style textures and adds it to the scene
|
||||
*/
|
||||
createPlayerModel(): THREE.Group {
|
||||
this.playerModel = new THREE.Group();
|
||||
|
||||
if (this.textureLoaded) {
|
||||
// Create textured model if texture is loaded
|
||||
this.createTexturedPlayerModel();
|
||||
} else {
|
||||
// Create simple colored model if no texture is loaded
|
||||
this.createSimplePlayerModel();
|
||||
}
|
||||
|
||||
this.playerModel.renderOrder = 0;
|
||||
this.rendererService.scene.add(this.playerModel);
|
||||
return this.playerModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple colored player model (without textures)
|
||||
*/
|
||||
private createSimplePlayerModel(): void {
|
||||
// Head
|
||||
const headGeometry = new THREE.BoxGeometry(0.5, 0.5, 0.5);
|
||||
const headMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
|
||||
const head = new THREE.Mesh(headGeometry, headMaterial);
|
||||
head.position.y = 1.35;
|
||||
this.playerModel.add(head);
|
||||
|
||||
// Body
|
||||
const bodyGeometry = new THREE.BoxGeometry(0.5, 0.7, 0.25);
|
||||
const bodyMaterial = new THREE.MeshLambertMaterial({color: 0x0000ff});
|
||||
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
||||
body.position.y = 0.75;
|
||||
this.playerModel.add(body);
|
||||
|
||||
// Arms
|
||||
const armGeometry = new THREE.BoxGeometry(0.2, 0.7, 0.25);
|
||||
const armMaterial = new THREE.MeshLambertMaterial({color: 0xffccaa});
|
||||
|
||||
const leftArm = new THREE.Mesh(armGeometry, armMaterial);
|
||||
leftArm.position.set(-0.35, 0.75, 0);
|
||||
this.playerModel.add(leftArm);
|
||||
|
||||
const rightArm = new THREE.Mesh(armGeometry, armMaterial);
|
||||
rightArm.position.set(0.35, 0.75, 0);
|
||||
this.playerModel.add(rightArm);
|
||||
|
||||
// Legs
|
||||
const legGeometry = new THREE.BoxGeometry(0.25, 0.7, 0.25);
|
||||
const legMaterial = new THREE.MeshLambertMaterial({color: 0x000000});
|
||||
|
||||
const leftLeg = new THREE.Mesh(legGeometry, legMaterial);
|
||||
leftLeg.position.set(-0.125, 0.15, 0);
|
||||
this.playerModel.add(leftLeg);
|
||||
|
||||
const rightLeg = new THREE.Mesh(legGeometry, legMaterial);
|
||||
rightLeg.position.set(0.125, 0.15, 0);
|
||||
this.playerModel.add(rightLeg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a textured player model using the Minecraft skin
|
||||
*/
|
||||
private createTexturedPlayerModel(): void {
|
||||
// Create the player with properly mapped textures
|
||||
|
||||
// Head - 8x8x8 pixels in the texture
|
||||
this.playerModel.add(this.createBoxWithUvMapping(
|
||||
0.5, 0.5, 0.5, // width, height, depth
|
||||
[
|
||||
{x: 8, y: 0, w: 8, h: 8}, // top
|
||||
{x: 16, y: 0, w: 8, h: 8}, // bottom
|
||||
{x: 16, y: 8, w: 8, h: 8}, // right
|
||||
{x: 0, y: 8, w: 8, h: 8}, // left
|
||||
{x: 8, y: 8, w: 8, h: 8}, // front
|
||||
{x: 24, y: 8, w: 8, h: 8} // back
|
||||
],
|
||||
{x: 0, y: 1.35, z: 0} // position
|
||||
));
|
||||
|
||||
// Body - 8x12x4 pixels in the texture
|
||||
this.playerModel.add(this.createBoxWithUvMapping(
|
||||
0.5, 0.7, 0.25, // width, height, depth
|
||||
[
|
||||
{x: 20, y: 16, w: 8, h: 4}, // top
|
||||
{x: 28, y: 16, w: 8, h: 4}, // bottom
|
||||
{x: 28, y: 20, w: 4, h: 12}, // right
|
||||
{x: 16, y: 20, w: 4, h: 12}, // left
|
||||
{x: 20, y: 20, w: 8, h: 12}, // front
|
||||
{x: 32, y: 20, w: 8, h: 12} // back
|
||||
],
|
||||
{x: 0, y: 0.75, z: 0} // position
|
||||
));
|
||||
|
||||
// Left Arm - 4x12x4 pixels in the texture
|
||||
this.playerModel.add(this.createBoxWithUvMapping(
|
||||
0.2, 0.7, 0.25, // width, height, depth
|
||||
[
|
||||
{x: 44, y: 16, w: 4, h: 4}, // top
|
||||
{x: 48, y: 16, w: 4, h: 4}, // bottom
|
||||
{x: 48, y: 20, w: 4, h: 12}, // right
|
||||
{x: 40, y: 20, w: 4, h: 12}, // left
|
||||
{x: 44, y: 20, w: 4, h: 12}, // front
|
||||
{x: 52, y: 20, w: 4, h: 12} // back
|
||||
],
|
||||
{x: -0.35, y: 0.75, z: 0} // position
|
||||
));
|
||||
|
||||
// Right Arm - 4x12x4 pixels in the texture
|
||||
this.playerModel.add(this.createBoxWithUvMapping(
|
||||
0.2, 0.7, 0.25, // width, height, depth
|
||||
[
|
||||
{x: 44, y: 16, w: 4, h: 4}, // top - mirror of left arm
|
||||
{x: 48, y: 16, w: 4, h: 4}, // bottom - mirror of left arm
|
||||
{x: 40, y: 20, w: 4, h: 12}, // right - mirror of left arm's left
|
||||
{x: 48, y: 20, w: 4, h: 12}, // left - mirror of left arm's right
|
||||
{x: 44, y: 20, w: 4, h: 12}, // front - same as left arm
|
||||
{x: 52, y: 20, w: 4, h: 12} // back - same as left arm
|
||||
],
|
||||
{x: 0.35, y: 0.75, z: 0} // position
|
||||
));
|
||||
|
||||
// Left Leg - 4x12x4 pixels in the texture
|
||||
this.playerModel.add(this.createBoxWithUvMapping(
|
||||
0.26, 0.7, 0.26, // width, height, depth
|
||||
[
|
||||
{x: 4, y: 16, w: 4, h: 4}, // top
|
||||
{x: 8, y: 16, w: 4, h: 4}, // bottom
|
||||
{x: 8, y: 20, w: 4, h: 12}, // right
|
||||
{x: 0, y: 20, w: 4, h: 12}, // left
|
||||
{x: 4, y: 20, w: 4, h: 12}, // front
|
||||
{x: 12, y: 20, w: 4, h: 12} // back
|
||||
],
|
||||
{x: -0.125, y: 0.15, z: 0} // position
|
||||
));
|
||||
|
||||
// Right Leg - 4x12x4 pixels in the texture
|
||||
this.playerModel.add(this.createBoxWithUvMapping(
|
||||
0.26, 0.7, 0.26, // width, height, depth
|
||||
[
|
||||
{x: 4, y: 16, w: 4, h: 4}, // top - mirror of left leg
|
||||
{x: 8, y: 16, w: 4, h: 4}, // bottom - mirror of left leg
|
||||
{x: 0, y: 20, w: 4, h: 12}, // right - mirror of left leg's left
|
||||
{x: 8, y: 20, w: 4, h: 12}, // left - mirror of left leg's right
|
||||
{x: 4, y: 20, w: 4, h: 12}, // front - same as left leg
|
||||
{x: 12, y: 20, w: 4, h: 12} // back - same as left leg
|
||||
],
|
||||
{x: 0.125, y: 0.15, z: 0} // position
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a box with proper UV mapping for a Minecraft character part
|
||||
* @param width Width of the box
|
||||
* @param height Height of the box
|
||||
* @param depth Depth of the box
|
||||
* @param uvMapping Array of UV coordinates for each face (top, bottom, right, left, front, back)
|
||||
* @param position Position of the box
|
||||
* @returns THREE.Mesh with properly mapped textures
|
||||
*/
|
||||
private createBoxWithUvMapping(
|
||||
width: number,
|
||||
height: number,
|
||||
depth: number,
|
||||
uvMapping: Array<{ x: number, y: number, w: number, h: number }>,
|
||||
position: { x: number, y: number, z: number }
|
||||
): THREE.Mesh {
|
||||
const geometry = new THREE.BoxGeometry(width, height, depth);
|
||||
|
||||
// Remap the custom face order to BoxGeometry face order: px, nx, py, ny, pz, nz
|
||||
const faceOrder = [2, 3, 0, 1, 4, 5]; // right, left, top, bottom, front, back
|
||||
const textureWidth = 64;
|
||||
const textureHeight = 64;
|
||||
const uv = geometry.attributes['uv'];
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const face = uvMapping[faceOrder[i]];
|
||||
const x1 = face.x / textureWidth;
|
||||
const y1 = 1 - face.y / textureHeight;
|
||||
const x2 = (face.x + face.w) / textureWidth;
|
||||
const y2 = 1 - (face.y + face.h) / textureHeight;
|
||||
|
||||
let uvs: [number, number][] = [
|
||||
[x1, y1], // top-left
|
||||
[x2, y1], // top-right
|
||||
[x1, y2], // bottom-left
|
||||
[x2, y2] // bottom-right
|
||||
];
|
||||
|
||||
const uvOffset = i * 8;
|
||||
for (let j = 0; j < 4; j++) {
|
||||
uv.array[uvOffset + j * 2] = uvs[j][0];
|
||||
uv.array[uvOffset + j * 2 + 1] = uvs[j][1];
|
||||
}
|
||||
}
|
||||
|
||||
uv.needsUpdate = true;
|
||||
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: this.skinTexture,
|
||||
transparent: true,
|
||||
});
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.position.set(position.x, position.y, position.z);
|
||||
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import {ElementRef, Injectable} from '@angular/core';
|
||||
import * as THREE from 'three';
|
||||
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import {ThemeService} from '@shared-components/theme/theme.service';
|
||||
import {THEME_MODE} from '@custom-types/constant';
|
||||
|
||||
/**
|
||||
* Service responsible for managing the Three.js rendering environment
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RendererService {
|
||||
scene!: THREE.Scene;
|
||||
camera!: THREE.PerspectiveCamera;
|
||||
renderer!: THREE.WebGLRenderer;
|
||||
controls!: OrbitControls;
|
||||
private currentTheme: THEME_MODE = THEME_MODE.LIGHT;
|
||||
|
||||
constructor(private themeService: ThemeService) {
|
||||
this.themeService.theme$.subscribe(theme => {
|
||||
this.currentTheme = theme;
|
||||
if (this.scene) {
|
||||
this.setBackgroundColor(theme);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Three.js scene, camera, renderer, and controls
|
||||
*/
|
||||
initializeRenderer(container: ElementRef): void {
|
||||
// Create scene
|
||||
this.scene = new THREE.Scene();
|
||||
this.setBackgroundColor(this.currentTheme);
|
||||
|
||||
// Get container dimensions
|
||||
const containerWidth = container.nativeElement.clientWidth;
|
||||
const containerHeight = container.nativeElement.clientHeight;
|
||||
|
||||
// Create camera
|
||||
this.camera = new THREE.PerspectiveCamera(75, containerWidth / containerHeight, 0.1, 1000);
|
||||
this.camera.position.set(-1, 2, 3);
|
||||
this.camera.lookAt(0, 1, 0);
|
||||
|
||||
// Create renderer
|
||||
this.renderer = new THREE.WebGLRenderer({antialias: true});
|
||||
this.renderer.setSize(containerWidth, containerHeight);
|
||||
|
||||
// Center the canvas in the container
|
||||
this.renderer.domElement.style.display = 'block';
|
||||
this.renderer.domElement.style.margin = 'auto';
|
||||
|
||||
container.nativeElement.appendChild(this.renderer.domElement);
|
||||
|
||||
// Initialize orbit controls
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.05;
|
||||
this.controls.minDistance = 0.5;
|
||||
this.controls.maxDistance = 4;
|
||||
this.controls.target.set(0, 1, 0);
|
||||
this.controls.update();
|
||||
|
||||
// Add lights
|
||||
this.addLights();
|
||||
}
|
||||
|
||||
private setBackgroundColor(theme: THEME_MODE) {
|
||||
this.scene.background = new THREE.Color(this.currentTheme === THEME_MODE.DARK ? 0x242526 : 0xFBFBFE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the camera to its default position and orientation.
|
||||
*/
|
||||
public resetCamera(): void {
|
||||
this.camera.position.set(-1, 2, 3);
|
||||
this.camera.lookAt(0, 1, 0);
|
||||
this.controls.target.set(0, 1, 0);
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds lighting to the scene
|
||||
*/
|
||||
private addLights(): void {
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
directionalLight.position.set(1, 1, 1);
|
||||
this.scene.add(directionalLight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles window resize events
|
||||
*/
|
||||
onWindowResize(container: ElementRef): void {
|
||||
const containerWidth = container.nativeElement.clientWidth;
|
||||
const containerHeight = 400; // Fixed height as defined in CSS
|
||||
|
||||
this.camera.aspect = containerWidth / containerHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(containerWidth, containerHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the scene
|
||||
*/
|
||||
render(): void {
|
||||
if (this.controls) {
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user