diff --git a/packages/client/components/game.astro b/packages/client/components/game.astro
index de272407a5..4abaac99f3 100644
--- a/packages/client/components/game.astro
+++ b/packages/client/components/game.astro
@@ -3,6 +3,7 @@
+
diff --git a/packages/client/data/sprites.json b/packages/client/data/sprites.json
index 35dc0d0f08..7795c79106 100755
--- a/packages/client/data/sprites.json
+++ b/packages/client/data/sprites.json
@@ -502,6 +502,7 @@
},
"offsetY": -24
},
+
{ "id": "mobs/angel", "width": 32, "height": 32, "offsetX": -8, "offsetY": -13 },
{ "id": "mobs/card", "width": 32, "height": 32, "offsetX": -8, "offsetY": -13 },
{ "id": "mobs/card2", "width": 32, "height": 32, "offsetX": -8, "offsetY": -13 },
@@ -1164,6 +1165,27 @@
"offsetX": 0,
"offsetY": 0
},
+ {
+ "id": "objects/sprucetree",
+ "width": 64,
+ "height": 80,
+ "animations": {
+ "idle": {
+ "length": 1,
+ "row": 0
+ },
+ "shake": {
+ "length": 5,
+ "row": 1
+ },
+ "stump": {
+ "length": 1,
+ "row": 2
+ }
+ },
+ "offsetX": -24,
+ "offsetY": -56
+ },
{
"id": "mobs/clam",
"width": 32,
diff --git a/packages/client/public/img/sprites/objects/sprucetree.png b/packages/client/public/img/sprites/objects/sprucetree.png
new file mode 100644
index 0000000000..62422357d7
Binary files /dev/null and b/packages/client/public/img/sprites/objects/sprucetree.png differ
diff --git a/packages/client/public/img/tilesets/tilesheet-2.png b/packages/client/public/img/tilesets/tilesheet-2.png
index 291c184d00..e876e86931 100644
Binary files a/packages/client/public/img/tilesets/tilesheet-2.png and b/packages/client/public/img/tilesets/tilesheet-2.png differ
diff --git a/packages/client/scss/game/impl/_container.scss b/packages/client/scss/game/impl/_container.scss
index 350696854c..1a376908ec 100644
--- a/packages/client/scss/game/impl/_container.scss
+++ b/packages/client/scss/game/impl/_container.scss
@@ -15,6 +15,7 @@
#background,
#entities,
+#entities-fore,
#foreground,
#cursor,
#text-canvas,
diff --git a/packages/client/src/controllers/entities.ts b/packages/client/src/controllers/entities.ts
index ff25c98d9f..234c367ea9 100644
--- a/packages/client/src/controllers/entities.ts
+++ b/packages/client/src/controllers/entities.ts
@@ -7,6 +7,7 @@ import Pet from '../entity/character/pet/pet';
import Player from '../entity/character/player/player';
import Projectile from '../entity/objects/projectile';
import Effect from '../entity/objects/effect';
+import Tree from '../entity/objects/tree';
import { Modules } from '@kaetram/common/network';
@@ -127,13 +128,17 @@ export default class EntitiesController {
prefix = 'effectentity';
break;
}
+
+ case Modules.EntityType.Tree: {
+ entity = this.createTree(info as EntityData);
+
+ prefix = 'objects';
+ break;
+ }
}
// Something went wrong creating the entity.
- if (!entity) {
- console.log(info);
- return log.error(`Failed to create entity ${info.instance}`);
- }
+ if (!entity) return log.error(`Failed to create entity ${info.instance}`);
let sprite = entity.sprite || this.game.sprites.get(`${prefix}/${info.key}`);
@@ -336,6 +341,16 @@ export default class EntitiesController {
return new Effect(info.instance);
}
+ /**
+ * Creates a new tree object based on the info provided.
+ * @param info Contains the key and instance of the tree.
+ * @returns A new tree object.
+ */
+
+ private createTree(info: EntityData): Entity {
+ return new Tree(info.instance);
+ }
+
/**
* Checks if the instance provided is the same as the main player.
* @param instance The instance we are checking.
@@ -446,6 +461,9 @@ export default class EntitiesController {
*/
public registerPosition(entity: Entity): void {
+ // Tree entities are registered as colliding on the rendering grid.
+ if (entity.isTree()) this.game.map.grid[entity.gridY][entity.gridX] = 2;
+
this.grids.addToRenderingGrid(entity);
}
diff --git a/packages/client/src/controllers/input.ts b/packages/client/src/controllers/input.ts
index 1ceb8c6a07..f87f04ba3b 100644
--- a/packages/client/src/controllers/input.ts
+++ b/packages/client/src/controllers/input.ts
@@ -560,6 +560,12 @@ export default class InputController {
break;
}
+ case Modules.EntityType.Tree: {
+ this.setCursor(this.cursors.axe);
+ this.hovering = Modules.Hovering.Tree;
+ break;
+ }
+
case Modules.EntityType.Player: {
if (this.game.pvp) {
this.setCursor(this.getAttackCursor());
@@ -750,7 +756,7 @@ export default class InputController {
*/
private isTargetable(entity: Entity): boolean {
- return this.isAttackable(entity) || entity.isNPC() || entity.isChest();
+ return this.isAttackable(entity) || entity.isNPC() || entity.isChest() || entity.isTree();
}
/**
diff --git a/packages/client/src/entity/character/player/handler.ts b/packages/client/src/entity/character/player/handler.ts
index 664293a7ce..026d4df01e 100644
--- a/packages/client/src/entity/character/player/handler.ts
+++ b/packages/client/src/entity/character/player/handler.ts
@@ -44,10 +44,11 @@ export default class Handler extends CharacterHandler {
// Prevent calculating pathing when we target a mob that is within range.
if (this.character.canAttackTarget() && !this.character.trading) return [];
- let isObject = this.map.isObject(x, y);
+ let isObject = this.map.isObject(x, y),
+ isTree = this.character.target?.isTree();
// Ignore requests into colliding tiles but allow targetable objects.
- if (this.map.isColliding(x, y) && !isObject) return [];
+ if (this.map.isColliding(x, y) && !isObject && !isTree) return [];
// Sends the packet to the server with the request.
this.game.socket.send(Packets.Movement, {
@@ -74,6 +75,8 @@ export default class Handler extends CharacterHandler {
ignores.push({ x: x + 1, y }, { x: x - 1, y }, { x, y: y + 1 }, { x, y: y - 1 });
}
+ if (isTree) ignores.push({ x, y });
+
return this.game.findPath(this.character, x, y, ignores, cursor);
}
diff --git a/packages/client/src/entity/entity.ts b/packages/client/src/entity/entity.ts
index 48b53c1583..940b00173f 100644
--- a/packages/client/src/entity/entity.ts
+++ b/packages/client/src/entity/entity.ts
@@ -225,7 +225,8 @@ export default abstract class Entity {
name: string,
speed = this.sprite.idleSpeed,
count = 1,
- onEndCount?: () => void
+ onEndCount?: () => void,
+ withStop = false
): void {
// Prevent setting animation if no sprite or it's the same animation.
if (this.animation?.name === name) return;
@@ -234,7 +235,7 @@ export default abstract class Entity {
let { length, row, width, height } = this.sprite.animations[name];
// Create a new animation instance to prevent pointer issues.
- this.animation = new Animation(name, length, row, width, height);
+ this.animation = new Animation(name, length, row, width, height, withStop);
// Restart the attack animation if it's already playing.
if (name.startsWith('atk')) this.animation.reset();
@@ -432,6 +433,14 @@ export default abstract class Entity {
return this.type === Modules.EntityType.Object;
}
+ /**
+ * @returns Whether or not the entity is a tree type.
+ */
+
+ public isTree(): boolean {
+ return this.type === Modules.EntityType.Tree;
+ }
+
/**
* Default implementation for `isModerator()`
* @returns False by default.
diff --git a/packages/client/src/entity/objects/effect.ts b/packages/client/src/entity/objects/effect.ts
index 9558ab6169..95d18447e8 100644
--- a/packages/client/src/entity/objects/effect.ts
+++ b/packages/client/src/entity/objects/effect.ts
@@ -6,4 +6,16 @@ export default class Effect extends Entity {
public constructor(instance: string) {
super(instance, Modules.EntityType.Effect);
}
+
+ public override idle(): void {
+ this.setAnimation(
+ 'idle',
+ 150,
+ 1,
+ () => {
+ //
+ },
+ true
+ );
+ }
}
diff --git a/packages/client/src/entity/objects/tree.ts b/packages/client/src/entity/objects/tree.ts
new file mode 100644
index 0000000000..3ff10f2129
--- /dev/null
+++ b/packages/client/src/entity/objects/tree.ts
@@ -0,0 +1,17 @@
+import Entity from '../entity';
+
+import { Modules } from '@kaetram/common/network';
+
+export default class Tree extends Entity {
+ public cut = false;
+
+ public constructor(instance: string) {
+ super(instance, Modules.EntityType.Tree);
+ }
+
+ public override idle(): void {
+ this.setAnimation(this.cut ? 'stump' : 'idle', 150, 1, () => {
+ //
+ });
+ }
+}
diff --git a/packages/client/src/entity/sprite.ts b/packages/client/src/entity/sprite.ts
index fac2b300c6..c934041b57 100644
--- a/packages/client/src/entity/sprite.ts
+++ b/packages/client/src/entity/sprite.ts
@@ -103,7 +103,7 @@ export default class Sprite {
public loadHurtSprite(): void {
// Hurt sprite already exists or there's not hurt effect, no need to load.
- if (this.hurtSprite || !this.hasHurtSprite()) return;
+ if (this.hurtSprite || !this.hasHurtSprite() || !this.loaded) return;
this.hurtSprite = Utils.getHurtSprite(this);
}
@@ -115,7 +115,7 @@ export default class Sprite {
public loadSilhouetteSprite(): void {
// Silhouette sprite already exists or there's no silhouette, no need to load.
- if (this.silhouetteSprite || !this.hasSilhouette() || isMobile()) return;
+ if (this.silhouetteSprite || !this.hasSilhouette() || isMobile() || !this.loaded) return;
this.silhouetteSprite = Utils.getSilhouetteSprite(this);
}
@@ -197,7 +197,7 @@ export default class Sprite {
public hasSilhouette(): boolean {
let type = this.getType();
- return type === 'mobs' || type === 'player' || type === 'npcs';
+ return type === 'mobs' || type === 'player' || type === 'npcs' || type === 'objects';
}
/**
diff --git a/packages/client/src/menu/guilds.ts b/packages/client/src/menu/guilds.ts
index 42869c2e93..9f40ad404f 100644
--- a/packages/client/src/menu/guilds.ts
+++ b/packages/client/src/menu/guilds.ts
@@ -20,7 +20,6 @@ export default class Guilds extends Menu {
// Container for creating a new guild.
private create: HTMLElement = document.querySelector('#guilds-create')!;
-
private createError: HTMLElement = document.querySelector('#guilds-create-error')!;
private backButton: HTMLElement = document.querySelector('#guilds-back-button')!;
@@ -904,6 +903,6 @@ export default class Guilds extends Menu {
*/
private setError(text = ''): void {
- this.createError.innerHTML = text;
+ this.createError.innerHTML = Util.parseMessage(Util.formatNotification(text));
}
}
diff --git a/packages/client/src/renderer/renderer.ts b/packages/client/src/renderer/renderer.ts
index 0b47924b37..1fbcb812b9 100644
--- a/packages/client/src/renderer/renderer.ts
+++ b/packages/client/src/renderer/renderer.ts
@@ -56,6 +56,7 @@ export default class Renderer {
protected overlay = document.querySelector('#overlay')!;
protected textCanvas = document.querySelector('#text-canvas')!;
protected entities = document.querySelector('#entities')!;
+ protected entitiesFore = document.querySelector('#entities-fore')!;
protected cursor = document.querySelector('#cursor')!;
protected entitiesMask = document.querySelector('#entities-mask')!;
@@ -66,12 +67,14 @@ export default class Renderer {
this.overlay,
this.textCanvas,
this.entities,
+ this.entitiesFore,
this.cursor,
this.entitiesMask
];
// Create the contexts based on the canvases.
protected entitiesContext: CanvasRenderingContext2D = this.entities.getContext('2d')!;
+ protected entitiesForeContext: CanvasRenderingContext2D = this.entitiesFore.getContext('2d')!;
protected overlayContext: CanvasRenderingContext2D = this.overlay.getContext('2d')!;
protected textContext: CanvasRenderingContext2D = this.textCanvas.getContext('2d')!;
protected cursorContext: CanvasRenderingContext2D = this.cursor.getContext('2d')!;
@@ -79,18 +82,20 @@ export default class Renderer {
protected allContexts = [
this.entitiesContext,
+ this.entitiesForeContext,
+ this.entitiesMaskContext,
this.overlayContext,
this.textContext,
- this.cursorContext,
- this.entitiesMaskContext
+ this.cursorContext
];
// We split contexts into two arrays, one for tilemap rendering and one for the rest.
protected contexts = [
this.entitiesContext,
+ this.entitiesForeContext,
+ this.entitiesMaskContext,
this.textContext,
- this.overlayContext,
- this.entitiesMaskContext
+ this.overlayContext
];
// Zooming buttons
@@ -385,6 +390,7 @@ export default class Renderer {
if (this.game.player.dead) return;
this.setCameraView(this.entitiesContext);
+ this.setCameraView(this.entitiesForeContext);
this.forEachVisibleEntity((entity: Entity) => {
// Skip entities that aren't properly loaded or are invisible.
@@ -393,7 +399,7 @@ export default class Renderer {
this.drawEntity(entity);
});
- this.entitiesMaskContext.globalAlpha = 0.3;
+ this.entitiesMaskContext.globalAlpha = 0.2;
this.entitiesMaskContext.drawImage(this.entities, 0, 0);
}
@@ -570,36 +576,37 @@ export default class Renderer {
dx = ~~(entity.x * this.camera.zoomFactor),
dy = ~~(entity.y * this.camera.zoomFactor),
flipX = dx + this.actualTileSize,
- flipY = dy + entity.sprite.height;
+ flipY = dy + entity.sprite.height,
+ context = entity.isTree() ? this.entitiesForeContext : this.entitiesContext;
- this.entitiesContext.save();
+ context.save();
// Update the entity fading onto the context.
- if (entity.fading) this.entitiesContext.globalAlpha = entity.fadingAlpha;
+ if (entity.fading) context.globalAlpha = entity.fadingAlpha;
// Handle flipping since we use the same sprite for right/left.
if (entity.spriteFlipX) {
- this.entitiesContext.translate(flipX, dy);
- this.entitiesContext.scale(-1, 1);
+ context.translate(flipX, dy);
+ context.scale(-1, 1);
} else if (entity.spriteFlipY) {
- this.entitiesContext.translate(dx, flipY);
- this.entitiesContext.scale(1, -1);
- } else this.entitiesContext.translate(dx, dy);
+ context.translate(dx, flipY);
+ context.scale(1, -1);
+ } else context.translate(dx, dy);
// Scale the entity to the current zoom factor.
- this.entitiesContext.scale(this.camera.zoomFactor, this.camera.zoomFactor);
+ context.scale(this.camera.zoomFactor, this.camera.zoomFactor);
// Scale the entity again if it has a custom scaling associated with it.
- if (entity.customScale) this.entitiesContext.scale(entity.customScale, entity.customScale);
+ if (entity.customScale) context.scale(entity.customScale, entity.customScale);
// Rotate using the entity's angle.
- if (entity.angle !== 0) this.entitiesContext.rotate(entity.angle);
+ if (entity.angle !== 0) context.rotate(entity.angle);
// Draw the entity shadowf
if (entity.hasShadow()) {
let shadowSprite = this.game.sprites.get('shadow')!;
- this.entitiesContext.drawImage(
+ context.drawImage(
shadowSprite.image,
0,
0,
@@ -612,7 +619,7 @@ export default class Renderer {
);
}
- this.entitiesContext.drawImage(
+ context.drawImage(
entity.getSprite().image,
frame!.x,
frame!.y,
@@ -626,10 +633,12 @@ export default class Renderer {
this.drawEntityFore(entity);
- this.entitiesContext.restore();
+ context.restore();
this.drawHealth(entity as Character);
+ if (!entity.isPlayer() && !entity.isMob() && !entity.isNPC() && !entity.isItem()) return;
+
if (!this.game.overlays.hasOverlay())
if (this.game.player.instance === entity.instance && this.camera.isCentered())
this.drawPlayerName(entity as Player);
@@ -924,8 +933,6 @@ export default class Renderer {
*/
private drawName(entity: Character | Item): void {
- if (entity.isPet() || entity.isProjectile()) return;
-
let x = entity.x + 8, // Default offsets
y = entity.y - 5,
colour = 'white',
diff --git a/packages/client/src/utils/util.ts b/packages/client/src/utils/util.ts
index 11631f4234..b72ccca266 100644
--- a/packages/client/src/utils/util.ts
+++ b/packages/client/src/utils/util.ts
@@ -389,7 +389,7 @@ export default {
for (let i = 0; i < cloneData.data.length; i += 4) {
// Non-empty pixels are skipped.
- if (cloneData.data[i + 3] !== 0) continue;
+ if (cloneData.data[i + 3] > 24) continue;
// Extract the x and y coordinates of the pixel.
let x = (i / 4) % sprite.image.width,
diff --git a/packages/common/network/modules.ts b/packages/common/network/modules.ts
index aba97fb0c2..f0029aa773 100644
--- a/packages/common/network/modules.ts
+++ b/packages/common/network/modules.ts
@@ -39,7 +39,8 @@ export enum EntityType {
Object,
Pet,
LootBag,
- Effect
+ Effect,
+ Tree
}
export enum Interfaces {
@@ -181,7 +182,8 @@ export enum Hovering {
Item,
NPC,
Chest,
- Object
+ Object,
+ Tree
}
export enum AudioTypes {
diff --git a/packages/common/util/utils.ts b/packages/common/util/utils.ts
index e480ad23c9..c8e977e8b2 100644
--- a/packages/common/util/utils.ts
+++ b/packages/common/util/utils.ts
@@ -27,7 +27,7 @@ export default {
*/
createInstance(identifier = 0): string {
- return `${identifier}${this.randomInt(1000, 100_000)}${++this.counter}`;
+ return `${identifier}-${this.randomInt(1000, 100_000)}${++this.counter}`;
},
/**
@@ -137,7 +137,7 @@ export default {
*/
getEntityType(instance: string): number {
- return parseInt(instance.slice(0, 1));
+ return parseInt(instance.split('-')[0]);
},
/**
diff --git a/packages/server/data/trees.json b/packages/server/data/trees.json
index 0d445d5df6..5b87d6ec8e 100644
--- a/packages/server/data/trees.json
+++ b/packages/server/data/trees.json
@@ -188,5 +188,12 @@
"experience": 350,
"difficulty": 240,
"item": "bloodwoodlogs"
+ },
+
+ "sprucetree": {
+ "levelRequirement": 45,
+ "experience": 400,
+ "difficulty": 250,
+ "item": "logs"
}
}
diff --git a/packages/server/src/controllers/entities.ts b/packages/server/src/controllers/entities.ts
index c8430a82dc..cd8da4ba46 100644
--- a/packages/server/src/controllers/entities.ts
+++ b/packages/server/src/controllers/entities.ts
@@ -1,6 +1,7 @@
import mobData from '../../data/mobs.json';
import itemData from '../../data/items.json';
import npcData from '../../data/npcs.json';
+import treeData from '../../data/trees.json';
import NPC from '../game/entity/npc/npc';
import Item from '../game/entity/objects/item';
import Chest from '../game/entity/objects/chest';
@@ -10,6 +11,7 @@ import Projectile from '../game/entity/objects/projectile';
import LootBag from '../game/entity/objects/lootbag';
import Pet from '../game/entity/character/pet/pet';
import Effect from '../game/entity/objects/effect';
+import Tree from '../game/entity/objects/tree';
import log from '@kaetram/common/util/log';
import { Modules } from '@kaetram/common/network';
@@ -40,6 +42,7 @@ export default class Entities {
private pets: { [instance: string]: Pet } = {};
private lootBags: { [instance: string]: LootBag } = {};
private effects: { [instance: string]: Effect } = {};
+ private trees: { [instance: string]: Tree } = {};
public constructor(private world: World) {
this.map = world.map;
@@ -71,10 +74,17 @@ export default class Entities {
case Modules.EntityType.Mob: {
return this.spawnMob(key, position.x, position.y);
}
+
+ case Modules.EntityType.Tree: {
+ return this.spawnTree(key, position.x, position.y);
+ }
}
});
- log.info(`Spawned ${Object.keys(this.entities).length} entities!`);
+ log.info(`Spawned ${Object.keys(this.items).length} items!`);
+ log.info(`Spawned ${Object.keys(this.npcs).length} NPCs!`);
+ log.info(`Spawned ${Object.keys(this.mobs).length} mobs!`);
+ log.info(`Spawned ${Object.keys(this.trees).length} trees!`);
// Spawns the static chests throughout the world.
@@ -154,6 +164,24 @@ export default class Entities {
return mob;
}
+ /**
+ * Spawns a tree in the world and adds it to the world.
+ * @param key The key of the tree, used to determine its sprite.
+ * @param x The x grid coordinate of the tree spawn.
+ * @param y The y grid coordinate of the tree spawn.
+ * @returns A new tree object.
+ */
+
+ public spawnTree(key: string, x: number, y: number): Tree {
+ let tree = new Tree(key, x, y);
+
+ this.add(tree);
+
+ this.trees[tree.instance] = tree;
+
+ return tree;
+ }
+
/**
* Spawns a chest in the world at the given position.
* @param items The items that the chest might drop.
@@ -587,6 +615,7 @@ export default class Entities {
if (key in itemData) return Modules.EntityType.Item;
if (key in npcData) return Modules.EntityType.NPC;
if (key in mobData) return Modules.EntityType.Mob;
+ if (key in treeData) return Modules.EntityType.Tree;
return -1;
}
diff --git a/packages/server/src/game/entity/objects/tree.ts b/packages/server/src/game/entity/objects/tree.ts
new file mode 100644
index 0000000000..7b7c4cd288
--- /dev/null
+++ b/packages/server/src/game/entity/objects/tree.ts
@@ -0,0 +1,10 @@
+import Entity from '../entity';
+
+import Utils from '@kaetram/common/util/utils';
+import { Modules } from '@kaetram/common/network';
+
+export default class Tree extends Entity {
+ public constructor(key: string, x: number, y: number) {
+ super(Utils.createInstance(Modules.EntityType.Tree), key, x, y);
+ }
+}