├── res
├── assets
│ ├── img
│ │ ├── air.png
│ │ ├── crab.png
│ │ ├── cracks.png
│ │ ├── blocks
│ │ │ ├── tnt.png
│ │ │ ├── dirt.png
│ │ │ ├── random.png
│ │ │ ├── sand.png
│ │ │ ├── snow.png
│ │ │ ├── stone.png
│ │ │ ├── torch.png
│ │ │ ├── bedrock.png
│ │ │ ├── coal_ore.png
│ │ │ ├── furnace.png
│ │ │ ├── gold_ore.png
│ │ │ ├── iron_ore.png
│ │ │ ├── oak_log.png
│ │ │ ├── obsidian.png
│ │ │ ├── diamond_ore.png
│ │ │ ├── furnace_on.png
│ │ │ ├── grass_block.png
│ │ │ ├── oak_leaves.png
│ │ │ ├── oak_planks.png
│ │ │ ├── oak_sapling.png
│ │ │ ├── smooth_stone.png
│ │ │ ├── stone_bricks.png
│ │ │ ├── crafting_table.png
│ │ │ └── grass_block_snow.png
│ │ ├── gui
│ │ │ ├── table1x2.png
│ │ │ ├── table2x2.png
│ │ │ ├── table3x3.png
│ │ │ ├── hotbar_edge.png
│ │ │ ├── hotbar_mid.png
│ │ │ ├── slot_border.png
│ │ │ └── block_border.png
│ │ ├── player
│ │ │ ├── player.png
│ │ │ ├── player_iron_axe.png
│ │ │ ├── player_stone_axe.png
│ │ │ ├── player_wood_axe.png
│ │ │ ├── player_diamond_axe.png
│ │ │ ├── player_iron_shovel.png
│ │ │ ├── player_wood_shovel.png
│ │ │ ├── player_diamond_shovel.png
│ │ │ ├── player_iron_pickaxe.png
│ │ │ ├── player_stone_pickaxe.png
│ │ │ ├── player_stone_shovel.png
│ │ │ ├── player_wood_pickaxe.png
│ │ │ └── player_diamond_pickaxe.png
│ │ ├── blocks_icon
│ │ │ ├── tnt.png
│ │ │ ├── dirt.png
│ │ │ ├── random.png
│ │ │ ├── sand.png
│ │ │ ├── snow.png
│ │ │ ├── stone.png
│ │ │ ├── torch.png
│ │ │ ├── bedrock.png
│ │ │ ├── coal_ore.png
│ │ │ ├── furnace.png
│ │ │ ├── gold_ore.png
│ │ │ ├── iron_ore.png
│ │ │ ├── oak_log.png
│ │ │ ├── obsidian.png
│ │ │ ├── diamond_ore.png
│ │ │ ├── furnace_on.png
│ │ │ ├── grass_block.png
│ │ │ ├── oak_leaves.png
│ │ │ ├── oak_planks.png
│ │ │ ├── oak_sapling.png
│ │ │ ├── smooth_stone.png
│ │ │ ├── stone_bricks.png
│ │ │ ├── crafting_table.png
│ │ │ └── grass_block_snow.png
│ │ └── items_icon
│ │ │ ├── coal.png
│ │ │ ├── bread.png
│ │ │ ├── bucket.png
│ │ │ ├── diamond.png
│ │ │ ├── stick.png
│ │ │ ├── iron_axe.png
│ │ │ ├── raw_gold.png
│ │ │ ├── raw_iron.png
│ │ │ ├── snowball.png
│ │ │ ├── stone_axe.png
│ │ │ ├── diamond_axe.png
│ │ │ ├── gold_ingot.png
│ │ │ ├── iron_ingot.png
│ │ │ ├── iron_pickaxe.png
│ │ │ ├── iron_shovel.png
│ │ │ ├── stone_shovel.png
│ │ │ ├── water_bucket.png
│ │ │ ├── wooden_axe.png
│ │ │ ├── diamond_shovel.png
│ │ │ ├── stone_pickaxe.png
│ │ │ ├── wooden_pickaxe.png
│ │ │ ├── wooden_shovel.png
│ │ │ └── diamond_pickaxe.png
│ └── font
│ │ └── arkpixel10.ttf
└── resources.go
├── .gitignore
├── items
├── id.go
├── util.go
├── craftingtable.go
├── recipes.go
├── inventory.go
└── property.go
├── sys_platform.go
├── go.mod
├── cmd
└── kar
│ └── main.go
├── sys_effect.go
├── LICENSE.md
├── sys_enemy.go
├── util.go
├── sys_spawn.go
├── components.go
├── sys_menu.go
├── sys_mainmenu.go
├── sys_projectile.go
├── sys_item.go
├── game.go
├── ecs_resources.go
├── README.md
├── tilemap
├── tilemap.go
└── generator.go
├── sys_camera.go
├── archetypes.go
├── singleton.go
├── sys_debug.go
├── go.sum
├── collision.go
├── sys_ui.go
└── sys_player.go
/res/assets/img/air.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/air.png
--------------------------------------------------------------------------------
/res/assets/img/crab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/crab.png
--------------------------------------------------------------------------------
/res/assets/img/cracks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/cracks.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/tnt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/tnt.png
--------------------------------------------------------------------------------
/res/assets/font/arkpixel10.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/font/arkpixel10.ttf
--------------------------------------------------------------------------------
/res/assets/img/blocks/dirt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/dirt.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/random.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/random.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/sand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/sand.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/snow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/snow.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/stone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/stone.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/torch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/torch.png
--------------------------------------------------------------------------------
/res/assets/img/gui/table1x2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/gui/table1x2.png
--------------------------------------------------------------------------------
/res/assets/img/gui/table2x2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/gui/table2x2.png
--------------------------------------------------------------------------------
/res/assets/img/gui/table3x3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/gui/table3x3.png
--------------------------------------------------------------------------------
/res/assets/img/player/player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/bedrock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/bedrock.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/coal_ore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/coal_ore.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/furnace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/furnace.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/gold_ore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/gold_ore.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/iron_ore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/iron_ore.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/oak_log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/oak_log.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/obsidian.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/obsidian.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/tnt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/tnt.png
--------------------------------------------------------------------------------
/res/assets/img/gui/hotbar_edge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/gui/hotbar_edge.png
--------------------------------------------------------------------------------
/res/assets/img/gui/hotbar_mid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/gui/hotbar_mid.png
--------------------------------------------------------------------------------
/res/assets/img/gui/slot_border.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/gui/slot_border.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/coal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/coal.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/diamond_ore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/diamond_ore.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/furnace_on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/furnace_on.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/grass_block.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/grass_block.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/oak_leaves.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/oak_leaves.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/oak_planks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/oak_planks.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/oak_sapling.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/oak_sapling.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/dirt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/dirt.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/random.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/random.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/sand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/sand.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/snow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/snow.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/stone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/stone.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/torch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/torch.png
--------------------------------------------------------------------------------
/res/assets/img/gui/block_border.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/gui/block_border.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/bread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/bread.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/bucket.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/bucket.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/diamond.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/diamond.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/stick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/stick.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/smooth_stone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/smooth_stone.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/stone_bricks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/stone_bricks.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/bedrock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/bedrock.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/coal_ore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/coal_ore.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/furnace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/furnace.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/gold_ore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/gold_ore.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/iron_ore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/iron_ore.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/oak_log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/oak_log.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/obsidian.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/obsidian.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/iron_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/iron_axe.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/raw_gold.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/raw_gold.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/raw_iron.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/raw_iron.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/snowball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/snowball.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/stone_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/stone_axe.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/crafting_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/crafting_table.png
--------------------------------------------------------------------------------
/res/assets/img/blocks/grass_block_snow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks/grass_block_snow.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/diamond_ore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/diamond_ore.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/furnace_on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/furnace_on.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/grass_block.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/grass_block.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/oak_leaves.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/oak_leaves.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/oak_planks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/oak_planks.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/oak_sapling.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/oak_sapling.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/diamond_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/diamond_axe.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/gold_ingot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/gold_ingot.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/iron_ingot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/iron_ingot.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/iron_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/iron_pickaxe.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/iron_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/iron_shovel.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/stone_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/stone_shovel.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/water_bucket.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/water_bucket.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/wooden_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/wooden_axe.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_iron_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_iron_axe.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_stone_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_stone_axe.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_wood_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_wood_axe.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/smooth_stone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/smooth_stone.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/stone_bricks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/stone_bricks.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/diamond_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/diamond_shovel.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/stone_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/stone_pickaxe.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/wooden_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/wooden_pickaxe.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/wooden_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/wooden_shovel.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_diamond_axe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_diamond_axe.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_iron_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_iron_shovel.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_wood_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_wood_shovel.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/crafting_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/crafting_table.png
--------------------------------------------------------------------------------
/res/assets/img/blocks_icon/grass_block_snow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/blocks_icon/grass_block_snow.png
--------------------------------------------------------------------------------
/res/assets/img/items_icon/diamond_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/items_icon/diamond_pickaxe.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_diamond_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_diamond_shovel.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_iron_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_iron_pickaxe.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_stone_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_stone_pickaxe.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_stone_shovel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_stone_shovel.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_wood_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_wood_pickaxe.png
--------------------------------------------------------------------------------
/res/assets/img/player/player_diamond_pickaxe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/setanarut/kar/HEAD/res/assets/img/player/player_diamond_pickaxe.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Sadece .go uzantılı alt tire ile başlayan dosyaları yoksay
12 | _*.go
13 |
14 | /internal
15 | /cmd/dene
16 | /cmd/wasm
17 | /wasm
18 |
19 | # Test binary, built with `go test -c`
20 | *.test
21 |
22 | # Output of the go coverage tool, specifically when used with LiteIDE
23 | *.out
24 |
25 | # Dependency directories (remove the comment below to include it)
26 | # vendor/
27 |
28 | # Go workspace file
29 | go.work
30 | .DS_Store
31 | .vscode/tasks.json
32 | /.vscode
33 |
--------------------------------------------------------------------------------
/items/id.go:
--------------------------------------------------------------------------------
1 | package items
2 |
3 | // Item ID
4 | const (
5 | Air uint8 = iota
6 | Bedrock
7 | Bread
8 | Bucket
9 | Coal
10 | CoalOre
11 | CraftingTable
12 | Diamond
13 | DiamondAxe
14 | DiamondOre
15 | DiamondPickaxe
16 | DiamondShovel
17 | Dirt
18 | Furnace
19 | GoldIngot
20 | GoldOre
21 | GrassBlock
22 | GrassBlockSnow
23 | IronAxe
24 | IronIngot
25 | IronOre
26 | IronPickaxe
27 | IronShovel
28 | OakLeaves
29 | OakLog
30 | OakPlanks
31 | OakSapling
32 | Obsidian
33 | RawGold
34 | RawIron
35 | Sand
36 | SmoothStone
37 | Snow
38 | Snowball
39 | Stick
40 | Stone
41 | StoneAxe
42 | StoneBricks
43 | StonePickaxe
44 | StoneShovel
45 | Tnt
46 | Torch
47 | WaterBucket
48 | WoodenAxe
49 | WoodenPickaxe
50 | WoodenShovel
51 | Random
52 | )
53 |
--------------------------------------------------------------------------------
/sys_platform.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | type Platform struct {
4 | hit *HitInfo
5 | }
6 |
7 | func (p *Platform) Init() {
8 | p.hit = &HitInfo{}
9 | }
10 |
11 | func (p *Platform) Update() {
12 | q := filterPlatform.Query()
13 | for q.Next() {
14 | aabb, vel, _ := q.Get()
15 | delta := tileCollider.Collide(*aabb, *(*Vec)(vel), nil)
16 | aabb.Pos = aabb.Pos.Add(delta)
17 | if vel.X != delta.X {
18 | vel.X *= -1
19 | }
20 |
21 | }
22 | }
23 | func (p *Platform) Draw() {
24 | q := filterPlatform.Query()
25 | for q.Next() {
26 | aabb, _, _ := q.Get()
27 | // topLeftPos := aabb.TopLeft()
28 | // colorMDIO.GeoM.Reset()
29 | // colorMDIO.GeoM.Translate(topLeftPos.X, topLeftPos.Y)
30 | // cameraRes.DrawWithColorM(res.BlockCrackFrames[items.Stone][0], colorM, colorMDIO, Screen)
31 | drawAABB(aabb)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/setanarut/kar
2 |
3 | go 1.24.5
4 |
5 | require (
6 | github.com/anthonynsimon/bild v0.14.0
7 | github.com/hajimehoshi/ebiten/v2 v2.9.4
8 | github.com/mlange-42/ark v0.6.4
9 | github.com/mlange-42/ark-serde v0.3.0
10 | github.com/quasilyte/gdata v0.8.1
11 | github.com/setanarut/anim v1.4.0
12 | github.com/setanarut/fastnoise v1.1.1
13 | github.com/setanarut/kamera/v2 v2.97.2
14 | github.com/setanarut/v v1.2.1
15 | golang.org/x/image v0.33.0
16 | golang.org/x/text v0.31.0
17 | )
18 |
19 | require (
20 | github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 // indirect
21 | github.com/ebitengine/hideconsole v1.0.0 // indirect
22 | github.com/ebitengine/purego v0.9.1 // indirect
23 | github.com/go-text/typesetting v0.3.0 // indirect
24 | github.com/goccy/go-json v0.10.5 // indirect
25 | github.com/jezek/xgb v1.1.1 // indirect
26 | github.com/rivo/uniseg v0.4.7 // indirect
27 | golang.org/x/sync v0.18.0 // indirect
28 | golang.org/x/sys v0.38.0 // indirect
29 | )
30 |
--------------------------------------------------------------------------------
/cmd/kar/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/setanarut/kar"
7 |
8 | "github.com/hajimehoshi/ebiten/v2"
9 | )
10 |
11 | var game = &kar.Game{
12 | Spawn: kar.Spawn{},
13 | Platform: kar.Platform{},
14 | Enemy: kar.Enemy{},
15 | Player: kar.Player{},
16 | Item: kar.Item{},
17 | Effects: kar.Effects{},
18 | Camera: kar.Camera{},
19 | Ui: kar.UI{},
20 | MainMenu: kar.MainMenu{},
21 | Menu: kar.Menu{},
22 | Debug: kar.Debug{},
23 | Projectile: kar.Projectile{},
24 | }
25 | var opts = &ebiten.RunGameOptions{
26 | DisableHiDPI: true,
27 | GraphicsLibrary: ebiten.GraphicsLibraryAuto,
28 | InitUnfocused: false,
29 | }
30 |
31 | func main() {
32 | game.Init()
33 | // ebiten.SetTPS(8)
34 | ebiten.SetScreenClearedEveryFrame(false)
35 | ebiten.SetWindowSize(int(kar.ScreenSize.X*kar.WindowScale), int(kar.ScreenSize.Y*kar.WindowScale))
36 | if err := ebiten.RunGameWithOptions(game, opts); err != nil {
37 | log.Fatal(err)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/sys_effect.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "math"
5 |
6 | "github.com/setanarut/kar/res"
7 |
8 | "github.com/hajimehoshi/ebiten/v2"
9 | )
10 |
11 | // block breaking effect system
12 | type Effects struct {
13 | g float64
14 | }
15 |
16 | func (e *Effects) Init() {
17 | e.g = 0.2
18 |
19 | }
20 | func (e *Effects) Update() {
21 |
22 | q := filterEffect.Query()
23 |
24 | for q.Next() {
25 | _, pos, vel, angle := q.Get()
26 | vel.Y += e.g
27 | pos.Y += vel.Y
28 |
29 | if math.Signbit(float64(*angle)) {
30 | *angle -= 0.2
31 | } else {
32 | *angle += 0.2
33 | }
34 |
35 | pos.X += vel.X * 2
36 |
37 | if pos.Y > cameraRes.Y+cameraRes.Height {
38 | toRemove = append(toRemove, q.Entity())
39 | }
40 | }
41 | }
42 | func (e *Effects) Draw() {
43 | q := filterEffect.Query()
44 | for q.Next() {
45 | id, pos, _, angle := q.Get()
46 | colorMDIO.GeoM = ebiten.GeoM{}
47 | colorMDIO.GeoM.Translate(-4, -4)
48 | colorMDIO.GeoM.Rotate(float64(*angle))
49 | colorMDIO.GeoM.Translate(4, 4)
50 | colorMDIO.GeoM.Translate(pos.X, pos.Y)
51 | colorM.Scale(1.3, 1.3, 1.3, 1)
52 | if *id != 0 {
53 | cameraRes.DrawWithColorM(res.Icon8[uint8(*id)], colorM, colorMDIO, Screen)
54 | }
55 | colorM.Reset()
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Barış
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 | ---
24 |
25 | The Go gopher mascot was designed by Renee French.
26 | The design is licensed under the [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) license
--------------------------------------------------------------------------------
/sys_enemy.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "math"
5 |
6 | "github.com/setanarut/kar/items"
7 |
8 | "github.com/setanarut/kar/res"
9 | )
10 |
11 | type Enemy struct {
12 | hit *HitInfo
13 | }
14 |
15 | func (p *Enemy) Init() {
16 | p.hit = &HitInfo{}
17 | }
18 |
19 | func (p *Enemy) Update() {
20 | q := filterEnemy.Query()
21 | for q.Next() {
22 | aabb, vel, mobileID, tick := q.Get()
23 | *tick += 0.09
24 | if *tick >= 2 {
25 | *tick = 0
26 | }
27 | switch *mobileID {
28 | case CrabID:
29 | enemyVel := *(*Vec)(vel)
30 | tileCollider.Collide(*aabb, enemyVel, func(hitInfos []HitTileInfo, delta Vec) {
31 | aabb.Pos = aabb.Pos.Add(delta)
32 | for _, hit := range hitInfos {
33 | spawnData := spawnEffectData{
34 | Pos: tileMapRes.TileToWorld(hit.TileCoords),
35 | Id: tileMapRes.GetIDUnchecked(hit.TileCoords),
36 | }
37 | toSpawnEffect = append(toSpawnEffect, spawnData)
38 | tileMapRes.SetUnchecked(hit.TileCoords, items.Air)
39 | }
40 | })
41 | }
42 | }
43 | }
44 | func (p *Enemy) Draw() {
45 | q := filterEnemy.Query()
46 | for q.Next() {
47 | aabb, _, mobileID, idx := q.Get()
48 | switch *mobileID {
49 | case CrabID:
50 | tl := aabb.TopLeft()
51 | colorMDIO.GeoM.Reset()
52 | colorMDIO.GeoM.Translate(tl.X, tl.Y)
53 | cameraRes.DrawWithColorM(res.Crab[int(math.Floor(float64(*idx)))], colorM, colorMDIO, Screen)
54 | }
55 |
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/util.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "fmt"
5 | "image/color"
6 | "math"
7 | "time"
8 |
9 | "github.com/hajimehoshi/ebiten/v2/vector"
10 | )
11 |
12 | func mapRange(v, a, b, c, d float64) float64 {
13 | return (v-a)/(b-a)*(d-c) + c
14 | }
15 |
16 | func linspace(min, max float64, n int) []float64 {
17 | if n == 1 {
18 | return []float64{min}
19 | }
20 | d := max - min
21 | l := float64(n) - 1
22 | res := make([]float64, n)
23 | for i := range res {
24 | res[i] = (min + (float64(i)*d)/l)
25 | }
26 | return res
27 | }
28 |
29 | // sinspace returns n points between start and end based on a sinusoidal function with a given amplitude
30 | //
31 | // start := 0.0 // Start of range
32 | // end := 2 * math.Pi // End of range (one full sine wave)
33 | // amplitude := 2.0 // Amplitude of the sine wave
34 | // n := 10 // Number of points
35 | func sinspace(start, end, amplitude float64, n int) []float64 {
36 | tValues := linspace(start, end, n)
37 | for i, t := range tValues {
38 | tValues[i] = amplitude * math.Sin(t)
39 | }
40 | return tValues
41 | }
42 |
43 | func drawAABB(aabb *AABB) {
44 | x, y := cameraRes.ApplyCameraTransformToPoint(aabb.Pos.X, aabb.Pos.Y)
45 | vector.DrawFilledRect(
46 | Screen,
47 | float32(x-aabb.Half.X),
48 | float32(y-aabb.Half.Y),
49 | float32(aabb.Half.X*2),
50 | float32(aabb.Half.Y*2),
51 | color.RGBA{128, 0, 0, 10},
52 | false,
53 | )
54 | }
55 |
56 | func formatDuration(d time.Duration) string {
57 | s := int(d.Seconds())
58 | return fmt.Sprintf("%02d:%02d:%02d:%02d", (s/3600)/24, (s/3600)%24, (s/60)%60, s%60)
59 | }
60 |
--------------------------------------------------------------------------------
/sys_spawn.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/mlange-42/ark/ecs"
7 | )
8 |
9 | // spawnItemData is a helper for delaying spawn events
10 | type spawnItemData struct {
11 | Pos Vec
12 | Id uint8
13 | Durability int
14 | }
15 | type spawnEffectData struct {
16 | Pos Vec
17 | Id uint8
18 | }
19 |
20 | var (
21 | toSpawnItem = []spawnItemData{}
22 | toSpawnEffect = []spawnEffectData{}
23 | toRemove []ecs.Entity
24 | )
25 |
26 | type Spawn struct {
27 | spawnInterval time.Duration
28 | }
29 |
30 | func (s *Spawn) Init() {
31 | // s.spawnInterval = time.Second * 4
32 | }
33 | func (s *Spawn) Update() {
34 |
35 | // gameDataRes.Duration += Tick
36 | // gameDataRes.SpawnElapsed += Tick
37 |
38 | // if gameDataRes.SpawnElapsed > s.spawnInterval {
39 | // gameDataRes.SpawnElapsed = 0
40 | // }
41 |
42 | // if gameDataRes.SpawnElapsed == 0 {
43 | // if world.Alive(currentPlayer) {
44 | // p := mapAABB.GetUnchecked(currentPlayer).Pos
45 | // mapEnemy.NewEntity(
46 | // &AABB{
47 | // Pos: p.Sub(Vec{50, 50}),
48 | // Half: Vec{8, 4.5},
49 | // },
50 | // &Velocity{0.4, 0},
51 | // ptr(CrabID),
52 | // ptr(AnimationTick(0)),
53 | // )
54 | // }
55 | // }
56 |
57 | // Spawn item
58 | for _, data := range toSpawnItem {
59 | SpawnItem(data.Pos, data.Id, data.Durability)
60 | }
61 | // Spawn effect
62 | for _, data := range toSpawnEffect {
63 | SpawnEffect(data.Pos, data.Id)
64 | }
65 |
66 | toSpawnItem = toSpawnItem[:0]
67 | toSpawnEffect = toSpawnEffect[:0]
68 |
69 | for _, e := range toRemove {
70 | world.RemoveEntity(e)
71 | }
72 | toRemove = toRemove[:0]
73 | }
74 | func (s *Spawn) Draw() {
75 | }
76 |
--------------------------------------------------------------------------------
/components.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type ItemID uint8
8 | type Rotation float64
9 | type Facing Vec
10 | type Velocity Vec
11 | type Position Vec
12 | type MobileID int // Mobile character id
13 | type PlatformType string
14 | type Durability int
15 | type AnimationIndex int // timing-related data for item animations.
16 | type AnimationTick float64 // timing-related data for item animations.
17 | type CollisionDelayer time.Duration
18 |
19 | type Health struct {
20 | Current int
21 | Max int
22 | }
23 |
24 | type Controller struct {
25 | Acceleration float64
26 | AirSkiddingDecel float64
27 | CurrentState string
28 | FallingDamageTempPosY float64
29 | Gravity float64
30 | JumpBoost float64
31 | JumpBoostMultiplier float64
32 | JumpHoldTime float64
33 | JumpPower float64
34 | JumpReleaseTimer float64
35 | JumpTimer float64
36 | MaxFallSpeed float64
37 | MaxRunSpeed float64
38 | MaxWalkSpeed float64
39 | MinSpeedThresForJumpBoostMultiplier float64
40 | PreviousState string
41 | RunAcceleration float64
42 | RunDeceleration float64
43 | ShortJumpVelocity float64
44 | SkiddingFriction float64
45 | SkiddingJumpEnabled bool
46 | SpeedJumpFactor float64
47 | WalkAcceleration float64
48 | WalkDeceleration float64
49 | }
50 |
51 | func ptr[T any](v T) *T { return &v }
52 |
--------------------------------------------------------------------------------
/sys_menu.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "image/color"
5 |
6 | "github.com/setanarut/kar/res"
7 |
8 | "github.com/hajimehoshi/ebiten/v2"
9 | "github.com/hajimehoshi/ebiten/v2/inpututil"
10 | "github.com/hajimehoshi/ebiten/v2/text/v2"
11 | "github.com/hajimehoshi/ebiten/v2/vector"
12 | )
13 |
14 | type Menu struct {
15 | drawOpt *text.DrawOptions
16 | line int
17 | text string
18 | menuOffset Vec
19 | }
20 |
21 | func (m *Menu) Init() {
22 | m.drawOpt = &text.DrawOptions{
23 | DrawImageOptions: ebiten.DrawImageOptions{},
24 | LayoutOptions: text.LayoutOptions{
25 | LineSpacing: 18,
26 | },
27 | }
28 |
29 | m.text = "SAVE\nMAIN MENU"
30 | m.menuOffset = ScreenSize.Scale(0.5).Sub(Vec{20, 30})
31 | m.drawOpt.ColorScale.ScaleWithColor(color.Gray{200})
32 | }
33 |
34 | func (m *Menu) Update() {
35 | if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) {
36 | switch m.line {
37 | case 0:
38 | SaveGame()
39 | // previousGameState = "menu"
40 | currentGameState = "playing"
41 | colorM.Reset()
42 | textDO.ColorScale.Reset()
43 | case 1:
44 | // previousGameState = "menu"
45 | currentGameState = "mainmenu"
46 | colorM.Reset()
47 | textDO.ColorScale.Reset()
48 | }
49 | }
50 |
51 | if inpututil.IsKeyJustPressed(ebiten.KeyW) {
52 | m.line = (m.line - 1 + 2) % 2
53 |
54 | }
55 | if inpututil.IsKeyJustPressed(ebiten.KeyS) {
56 | m.line = (m.line + 1) % 2
57 | }
58 | }
59 | func (m *Menu) Draw() {
60 | m.drawOpt.GeoM.Reset()
61 | m.drawOpt.GeoM.Translate(m.menuOffset.X, m.menuOffset.Y)
62 |
63 | // draw menu text
64 | text.Draw(Screen, m.text, res.Font, m.drawOpt)
65 |
66 | // draw selection box
67 | vector.DrawFilledRect(
68 | Screen,
69 | float32(m.menuOffset.X-8),
70 | float32(m.menuOffset.Y+float64(m.line*18))+5,
71 | 3,
72 | 7,
73 | color.White,
74 | false,
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/items/util.go:
--------------------------------------------------------------------------------
1 | package items
2 |
3 | import (
4 | "image/color"
5 | "math/rand/v2"
6 | )
7 |
8 | var Blocks []uint8
9 |
10 | func init() {
11 | for id := range Property {
12 | if HasTag(id, Block) {
13 | Blocks = append(Blocks, id)
14 | }
15 | }
16 | }
17 |
18 | func HasTag(id uint8, tag tag) bool {
19 | return Property[id].Tags&tag != 0
20 | }
21 |
22 | func IsBestTool(blockID, toolID uint8) bool {
23 | return Property[blockID].BestToolTag&Property[toolID].Tags != 0
24 | }
25 | func IsStackable(id uint8) bool {
26 | return Property[id].MaxStackSize > 1
27 | }
28 | func GetDefaultDurability(id uint8) int {
29 | if HasTag(id, Tool) {
30 | if HasTag(id, MaterialWooden) {
31 | return 25
32 | }
33 | if HasTag(id, MaterialStone) {
34 | return 50
35 | }
36 | if HasTag(id, MaterialGold) {
37 | return 100
38 | }
39 | if HasTag(id, MaterialIron) {
40 | return 400
41 | }
42 | if HasTag(id, MaterialDiamond) {
43 | return 800
44 | }
45 | }
46 | return 0
47 | }
48 |
49 | func RandomBlock() uint8 {
50 | return Blocks[rand.IntN(len(Blocks))]
51 | }
52 | func DisplayName(id uint8) string {
53 | return Property[id].DisplayName
54 | }
55 | func RandomItem() uint8 {
56 | max := len(Property) - 1
57 | return uint8(1 + rand.IntN(max-1+1))
58 | }
59 |
60 | var ColorMap = map[uint8]color.RGBA{
61 | Air: rgb(0, 62, 161),
62 | CraftingTable: rgb(194, 137, 62),
63 | GrassBlock: rgb(133, 75, 54),
64 | Dirt: rgb(133, 75, 54),
65 | Sand: rgb(199, 193, 158),
66 | Stone: rgb(120, 120, 120),
67 | CoalOre: rgb(0, 0, 0),
68 | GoldOre: rgb(255, 221, 0),
69 | IronOre: rgb(151, 176, 205),
70 | DiamondOre: rgb(0, 247, 255),
71 | OakLog: rgb(227, 131, 104),
72 | OakPlanks: rgb(224, 153, 145),
73 | OakLeaves: rgb(0, 160, 16),
74 | }
75 |
76 | func rgb(r, g, b uint8) color.RGBA {
77 | return color.RGBA{r, g, b, 255}
78 | }
79 |
--------------------------------------------------------------------------------
/sys_mainmenu.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "image/color"
5 |
6 | "github.com/setanarut/kar/res"
7 |
8 | "github.com/hajimehoshi/ebiten/v2"
9 | "github.com/hajimehoshi/ebiten/v2/inpututil"
10 | "github.com/hajimehoshi/ebiten/v2/text/v2"
11 | "github.com/hajimehoshi/ebiten/v2/vector"
12 | "golang.org/x/image/colornames"
13 | )
14 |
15 | type MainMenu struct {
16 | drawOpt text.DrawOptions
17 | line int
18 | text string
19 | menuOffset Vec
20 | }
21 |
22 | func (m *MainMenu) Init() {
23 | m.drawOpt = text.DrawOptions{
24 | DrawImageOptions: ebiten.DrawImageOptions{},
25 | LayoutOptions: text.LayoutOptions{
26 | LineSpacing: 18,
27 | },
28 | }
29 |
30 | m.text = "NEW GAME\nLOAD"
31 | m.menuOffset = ScreenSize.Scale(0.5).Sub(Vec{20, 30})
32 | m.drawOpt.ColorScale.ScaleWithColor(color.Gray{200})
33 | }
34 |
35 | func (m *MainMenu) Update() {
36 | if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) {
37 | switch m.line {
38 | case 0:
39 | NewGame()
40 | // previousGameState = "mainmenu"
41 | currentGameState = "playing"
42 | m.text = "NEW GAME\nLOAD"
43 | case 1:
44 | LoadGame()
45 | // previousGameState = "mainmenu"
46 | if dataManager.ItemExists("01save") {
47 | currentGameState = "playing"
48 | m.text = "NEW GAME\nLOAD"
49 | } else {
50 | m.text = "NEW GAME\nNO SAVED GAME!"
51 | }
52 | }
53 |
54 | colorM.Reset()
55 | textDO.ColorScale.Reset()
56 | }
57 |
58 | if inpututil.IsKeyJustPressed(ebiten.KeyW) {
59 | m.line = (m.line - 1 + 2) % 2
60 |
61 | }
62 | if inpututil.IsKeyJustPressed(ebiten.KeyS) {
63 | m.line = (m.line + 1) % 2
64 | }
65 | }
66 | func (m *MainMenu) Draw() {
67 | m.drawOpt.GeoM.Reset()
68 | m.drawOpt.GeoM.Translate(m.menuOffset.X, m.menuOffset.Y)
69 |
70 | // draw menu text
71 | text.Draw(Screen, m.text, res.Font, &m.drawOpt)
72 |
73 | // draw selection box
74 | vector.DrawFilledRect(
75 | Screen,
76 | float32(m.menuOffset.X-8),
77 | float32(m.menuOffset.Y+float64(m.line*18))+5,
78 | 3,
79 | 7,
80 | colornames.White,
81 | false,
82 | )
83 | }
84 |
--------------------------------------------------------------------------------
/sys_projectile.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "math"
5 |
6 | "github.com/setanarut/kar/items"
7 |
8 | "github.com/setanarut/kar/res"
9 | )
10 |
11 | type Projectile struct {
12 | snowBallBox AABB
13 | bounceVelocity float64
14 | hitInfo HitInfo
15 | }
16 |
17 | func (p *Projectile) Init() {
18 | p.hitInfo = HitInfo{}
19 | p.snowBallBox = AABB{Half: Vec{4, 4}}
20 | p.bounceVelocity = -math.Sqrt(2 * SnowballGravity * SnowballBounceHeight)
21 | }
22 | func (p *Projectile) Update() {
23 | // projectile physics
24 | q := filterProjectile.Query()
25 | for q.Next() {
26 | itemID, projectilePos, projectileVel := q.Get()
27 | // snowball physics
28 | if uint8(*itemID) == items.Snowball {
29 | projectileVel.Y += SnowballGravity
30 | projectileVel.Y = min(projectileVel.Y, SnowballMaxFallVelocity)
31 | p.snowBallBox.Pos.X = projectilePos.X
32 | p.snowBallBox.Pos.Y = projectilePos.Y
33 | tileCollider.Collide(p.snowBallBox, *(*Vec)(projectileVel), func(ci []HitTileInfo, delta Vec) {
34 | projectilePos.X += delta.X
35 | projectilePos.Y += delta.Y
36 | isHorizontalCollision := false
37 | for _, cinfo := range ci {
38 | if cinfo.Normal.Y == -1 {
39 |
40 | projectileVel.Y = p.bounceVelocity
41 | }
42 | if cinfo.Normal.X == -1 && projectileVel.X > 0 && projectileVel.Y > 0 {
43 | isHorizontalCollision = true
44 | }
45 | if cinfo.Normal.X == 1 && projectileVel.X < 0 && projectileVel.Y > 0 {
46 | isHorizontalCollision = true
47 | }
48 | }
49 | if isHorizontalCollision {
50 | if world.Alive(q.Entity()) {
51 | toRemove = append(toRemove, q.Entity())
52 | }
53 | }
54 | },
55 | )
56 | // TODO snowbros mantığı yaz. çarpınca donacakç
57 | q := filterEnemy.Query()
58 | for q.Next() {
59 | aabb, _, _, _ := q.Get()
60 | if Overlap(&p.snowBallBox, aabb, nil) {
61 | toRemove = append(toRemove, q.Entity())
62 | }
63 | }
64 | }
65 | }
66 | }
67 | func (p *Projectile) Draw() {
68 | // Draw snowball
69 | q := filterProjectile.Query()
70 | for q.Next() {
71 | id, pos, _ := q.Get()
72 | colorMDIO.GeoM.Reset()
73 | colorMDIO.GeoM.Translate(pos.X-dropItemAABB.Half.X, pos.Y-dropItemAABB.Half.Y)
74 | cameraRes.DrawWithColorM(res.Icon8[uint8(*id)], colorM, colorMDIO, Screen)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/sys_item.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "github.com/setanarut/kar/items"
5 |
6 | "github.com/setanarut/kar/res"
7 |
8 | "github.com/mlange-42/ark/ecs"
9 | )
10 |
11 | type Item struct {
12 | toRemoveComponent []ecs.Entity
13 | itemHit *HitInfo
14 | }
15 |
16 | func (i *Item) Init() {
17 | i.itemHit = &HitInfo{}
18 | }
19 | func (i *Item) Update() {
20 |
21 | q := filterCollisionDelayer.Query()
22 |
23 | for q.Next() {
24 | delayer := q.Get()
25 | *delayer -= CollisionDelayer(Tick)
26 | }
27 |
28 | // dropped items collisions and animations
29 | if world.Alive(currentPlayer) {
30 | playerBox := mapAABB.GetUnchecked(currentPlayer)
31 | itemQuery := filterDroppedItem.Query()
32 | for itemQuery.Next() {
33 | itemID, itemPos, animIndex := itemQuery.Get()
34 | dropItemAABB.Pos = *(*Vec)(itemPos)
35 | itemEntity := itemQuery.Entity()
36 |
37 | if !mapCollisionDelayer.HasUnchecked(itemEntity) {
38 | // Check player-item collision
39 | if Overlap(playerBox, dropItemAABB, i.itemHit) {
40 | // if Durability component exists, get durability
41 | dur := 0
42 | if mapDurability.HasUnchecked(itemEntity) {
43 | dur = int(*mapDurability.GetUnchecked(itemEntity))
44 | }
45 | if inventoryRes.AddItemIfEmpty(uint8(*itemID), dur) {
46 | toRemove = append(toRemove, itemEntity)
47 | }
48 | onInventorySlotChanged()
49 | }
50 | } else {
51 | if *mapCollisionDelayer.GetUnchecked(itemEntity) < 0 {
52 | i.toRemoveComponent = append(i.toRemoveComponent, itemEntity)
53 | }
54 | }
55 | dropItemAABB.Pos.Y += 6
56 | // vertical item sine animation
57 | tileCollider.Collisions = tileCollider.Collisions[:0]
58 | dy := tileCollider.CollideY(dropItemAABB, ItemGravity)
59 | itemPos.Y += dy
60 | *animIndex = AnimationIndex((int(*animIndex) + 1) % len(sinspaceOffsets))
61 |
62 | }
63 | }
64 |
65 | // Remove MapCollisionDelayer components
66 | for _, entity := range i.toRemoveComponent {
67 | mapCollisionDelayer.Remove(entity)
68 | }
69 |
70 | i.toRemoveComponent = i.toRemoveComponent[:0]
71 | }
72 | func (i *Item) Draw() {
73 | // Draw drop Items
74 | itemQuery := filterDroppedItem.Query()
75 | for itemQuery.Next() {
76 | itemid, pos, animIndex := itemQuery.Get()
77 | id := uint8(*itemid)
78 | dropItemAABB.Pos = *(*Vec)(pos)
79 | colorMDIO.GeoM.Reset()
80 | colorMDIO.GeoM.Translate(dropItemAABB.Left(), dropItemAABB.Top()+sinspaceOffsets[*animIndex])
81 | if id != items.Air {
82 | cameraRes.DrawWithColorM(res.Icon8[id], colorM, colorMDIO, Screen)
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/game.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "reflect"
5 |
6 | "github.com/hajimehoshi/ebiten/v2"
7 | "github.com/hajimehoshi/ebiten/v2/inpututil"
8 | )
9 |
10 | type Game struct {
11 | Spawn Spawn
12 | Platform Platform
13 | Enemy Enemy
14 | Player Player
15 | Item Item
16 | Effects Effects
17 | Camera Camera
18 | Ui UI
19 | MainMenu MainMenu
20 | Menu Menu
21 | Debug Debug
22 | Projectile Projectile
23 | }
24 |
25 | func (g *Game) Init() {
26 | v := reflect.ValueOf(g).Elem()
27 | for i := range v.NumField() {
28 | if init := v.Field(i).Addr().MethodByName("Init"); init.IsValid() {
29 | init.Call(nil)
30 | }
31 | }
32 | colorM.ChangeHSV(1, 0, 0.5) // BW
33 | textDO.ColorScale.Scale(0.5, 0.5, 0.5, 1)
34 | }
35 |
36 | func (g *Game) Update() error {
37 | if ebiten.IsFocused() {
38 |
39 | if inpututil.IsKeyJustPressed(ebiten.KeyP) {
40 | if ebiten.IsKeyPressed(ebiten.KeyMeta) && ebiten.IsKeyPressed(ebiten.KeyShift) {
41 | debugEnabled = !debugEnabled
42 | }
43 | }
44 |
45 | // toggle menu
46 | if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
47 | switch currentGameState {
48 | case "menu":
49 | currentGameState = "playing"
50 | // previousGameState = "menu"
51 | colorM.Reset()
52 | textDO.ColorScale.Reset()
53 | case "playing":
54 | currentGameState = "menu"
55 | // previousGameState = "playing"
56 | colorM.ChangeHSV(1, 0, 0.5) // BW
57 | textDO.ColorScale.Scale(0.5, 0.5, 0.5, 1)
58 | }
59 | }
60 |
61 | // Update systems
62 | switch currentGameState {
63 | case "mainmenu":
64 | g.MainMenu.Update()
65 | case "menu":
66 | g.Menu.Update()
67 | case "playing":
68 | if gameDataRes.GameplayState == Playing {
69 | g.Camera.Update()
70 | g.Enemy.Update()
71 | g.Player.Update()
72 | g.Platform.Update()
73 | g.Item.Update()
74 | g.Effects.Update()
75 | g.Projectile.Update()
76 | }
77 | g.Ui.Update()
78 | g.Spawn.Update()
79 | }
80 | if debugEnabled {
81 | g.Debug.Update()
82 | }
83 | }
84 | return nil
85 | }
86 |
87 | func (g *Game) Draw(screen *ebiten.Image) {
88 | if ebiten.IsFocused() {
89 | Screen = screen
90 | Screen.Fill(backgroundColor)
91 |
92 | // Update systems
93 | switch currentGameState {
94 | case "mainmenu":
95 | g.MainMenu.Draw()
96 | case "menu":
97 | g.Camera.Draw()
98 | g.Menu.Draw()
99 | case "playing":
100 | g.Camera.Draw()
101 | g.Platform.Draw()
102 | g.Enemy.Draw()
103 | g.Player.Draw()
104 | g.Item.Draw()
105 | g.Projectile.Draw()
106 | g.Effects.Draw()
107 | g.Ui.Draw()
108 | }
109 | if debugEnabled {
110 | g.Debug.Draw()
111 | }
112 | }
113 | }
114 |
115 | func (g *Game) LayoutF(w, h float64) (float64, float64) {
116 | return ScreenSize.X, ScreenSize.Y
117 | }
118 |
119 | func (g *Game) Layout(w, h int) (int, int) {
120 | return 0, 0
121 | }
122 |
--------------------------------------------------------------------------------
/ecs_resources.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "image"
5 | "time"
6 |
7 | "github.com/setanarut/kar/items"
8 | "github.com/setanarut/kar/res"
9 | "github.com/setanarut/kar/tilemap"
10 |
11 | "github.com/mlange-42/ark/ecs"
12 | "github.com/setanarut/anim"
13 | "github.com/setanarut/kamera/v2"
14 | )
15 |
16 | // ECS Resources
17 | var (
18 | animPlayer = *anim.NewAnimationPlayer(
19 | anim.Atlas{"Default", res.Player},
20 | anim.Atlas{"WoodenAxe", res.PlayerWoodenAxeAtlas},
21 | anim.Atlas{"StoneAxe", res.PlayerStoneAxeAtlas},
22 | anim.Atlas{"IronAxe", res.PlayerIronAxeAtlas},
23 | anim.Atlas{"DiamondAxe", res.PlayerDiamondAxeAtlas},
24 | anim.Atlas{"WoodenPickaxe", res.PlayerWoodenPickaxeAtlas},
25 | anim.Atlas{"StonePickaxe", res.PlayerStonePickaxeAtlas},
26 | anim.Atlas{"IronPickaxe", res.PlayerIronPickaxeAtlas},
27 | anim.Atlas{"DiamondPickaxe", res.PlayerDiamondPickaxeAtlas},
28 | anim.Atlas{"WoodenShovel", res.PlayerWoodenShovelAtlas},
29 | anim.Atlas{"StoneShovel", res.PlayerStoneShovelAtlas},
30 | anim.Atlas{"IronShovel", res.PlayerIronShovelAtlas},
31 | anim.Atlas{"DiamondShovel", res.PlayerDiamondShovelAtlas},
32 | )
33 |
34 | mapResAnimPlaybackData = ecs.NewResource[anim.PlaybackData](&world)
35 | mapResCamera = ecs.NewResource[kamera.Camera](&world)
36 | mapResCraftingtable = ecs.NewResource[items.CraftTable](&world)
37 | mapResGameData = ecs.NewResource[gameData](&world)
38 | mapResInventory = ecs.NewResource[items.Inventory](&world)
39 | mapResTilemap = ecs.NewResource[tilemap.TileMap](&world)
40 |
41 | gameDataRes = gameData{GameplayState: Playing}
42 | craftingTableRes = items.NewCraftTable()
43 | inventoryRes = items.NewInventory(16)
44 | cameraRes = kamera.NewCamera(0, 0, ScreenSize.X, ScreenSize.Y)
45 | tileMapRes = tilemap.MakeTileMap(512, 512, 20, 20)
46 | animDefaultPlaybackData anim.PlaybackData
47 | )
48 |
49 | // GameplayStates
50 | const (
51 | Playing int = iota
52 | CraftingTable3x3
53 | Crafting2x2
54 | Furnace1x2
55 | )
56 |
57 | type gameData struct {
58 | GameplayState int
59 | TargetBlockCoord image.Point
60 | IsRayHit bool
61 | BlockHealth float64
62 | Duration time.Duration // Gameplay duration
63 | SpawnElapsed time.Duration // Entity spawn timer
64 | }
65 |
66 | func init() {
67 |
68 | animPlayer.NewAnim("idleRight", 0, 0, 16, 16, 1, false, false, 1)
69 | animPlayer.NewAnim("idleUp", 208, 0, 16, 16, 1, false, false, 1)
70 | animPlayer.NewAnim("idleDown", 224, 0, 16, 16, 1, false, false, 1)
71 | animPlayer.NewAnim("walkRight", 16, 0, 16, 16, 4, false, false, 15)
72 | animPlayer.NewAnim("jump", 16*5, 0, 16, 16, 1, false, false, 15)
73 | animPlayer.NewAnim("skidding", 16*6, 0, 16, 16, 1, false, false, 15)
74 | animPlayer.NewAnim("attackDown", 16*7, 0, 16, 16, 2, false, false, 8)
75 | animPlayer.NewAnim("attackRight", 144, 0, 16, 16, 2, false, false, 8)
76 | animPlayer.NewAnim("attackWalk", 0, 16, 16, 16, 4, false, false, 8)
77 | animPlayer.NewAnim("attackUp", 16*11, 0, 16, 16, 2, false, false, 8)
78 | animPlayer.SetAnim("idleRight")
79 |
80 | animDefaultPlaybackData = animPlayer.Data
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/items/craftingtable.go:
--------------------------------------------------------------------------------
1 | package items
2 |
3 | import "image"
4 |
5 | type CraftTable struct {
6 | Pos image.Point // Slot position
7 | Slots [][]Slot
8 | ResultSlot Slot
9 | }
10 |
11 | func NewCraftTable() *CraftTable {
12 | return &CraftTable{
13 | Slots: [][]Slot{{{}, {}, {}}, {{}, {}, {}}, {{}, {}, {}}},
14 | }
15 | }
16 |
17 | // returns zero (air) if no recipe is found, otherwise returns the recipe result and quantity.
18 | func (c *CraftTable) CheckRecipe(recipes map[uint8]Recipe) (uint8, uint8) {
19 | cropped := c.cropRecipe(c.Slots)
20 | for itemIDKey, recipe := range recipes {
21 | if c.Equal(cropped, c.cropRecipe(recipe)) {
22 |
23 | return itemIDKey, recipe[0][0].Quantity
24 | }
25 | }
26 | return 0, 0
27 | }
28 |
29 | // returns minimum result item quantity
30 | func (c *CraftTable) UpdateResultSlot(recipes map[uint8]Recipe) uint8 {
31 | id, quantity := c.CheckRecipe(recipes)
32 | c.ResultSlot.ID = id
33 | minimum := uint8(255)
34 | for y := range 3 {
35 | for x := range 3 {
36 | if c.Slots[y][x].Quantity != 0 {
37 | minimum = min(minimum, c.Slots[y][x].Quantity)
38 | }
39 | }
40 | }
41 | if minimum == 255 {
42 | minimum = 0
43 | }
44 |
45 | if c.ResultSlot.ID != 0 {
46 | c.ResultSlot.Quantity = minimum * quantity
47 | }
48 | return minimum * quantity
49 | }
50 | func (c *CraftTable) ClearTable() {
51 | c.Slots = [][]Slot{{{}, {}, {}}, {{}, {}, {}}, {{}, {}, {}}}
52 | }
53 |
54 | func (c *CraftTable) CurrentSlot() *Slot {
55 | return &c.Slots[c.Pos.Y][c.Pos.X]
56 | }
57 |
58 | func (c *CraftTable) ClearCurrenSlot() {
59 | c.Slots[c.Pos.Y][c.Pos.X] = Slot{}
60 | }
61 |
62 | func (c *CraftTable) RemoveItem(x, y int) {
63 | if c.Slots[y][x].Quantity == 1 {
64 | c.Slots[y][x].ID = 0
65 | c.Slots[y][x].Quantity = 0
66 | } else if c.Slots[y][x].Quantity > 0 {
67 | c.Slots[y][x].Quantity--
68 | }
69 | }
70 |
71 | func (c *CraftTable) Equal(recipeA, recipeB Recipe) bool {
72 | // First, compare their sizes
73 | if len(recipeA) != len(recipeB) {
74 | return false
75 | }
76 | for i := range recipeA {
77 | if len(recipeA[i]) != len(recipeB[i]) {
78 | return false
79 | }
80 | // Compare each cell one by one
81 | for j := range recipeA[i] {
82 | if recipeA[i][j].ID != recipeB[i][j].ID {
83 | return false
84 | }
85 | }
86 | }
87 | return true
88 | }
89 |
90 | // cropRecipe normalizes grid
91 | func (c *CraftTable) cropRecipe(reci Recipe) Recipe {
92 | minRow, maxRow := len(reci), 0
93 | minCol, maxCol := len(reci[0]), 0
94 | for i := range len(reci) {
95 | for j := range len(reci[i]) {
96 | if reci[i][j].ID != 0 {
97 | if i < minRow {
98 | minRow = i
99 | }
100 | if i > maxRow {
101 | maxRow = i
102 | }
103 | if j < minCol {
104 | minCol = j
105 | }
106 | if j > maxCol {
107 | maxCol = j
108 | }
109 | }
110 | }
111 | }
112 | if minRow > maxRow || minCol > maxCol {
113 | return reci
114 | }
115 | normalizedGrid := make([][]Slot, maxRow-minRow+1)
116 | for i := range normalizedGrid {
117 | normalizedGrid[i] = make([]Slot, maxCol-minCol+1)
118 | for j := range normalizedGrid[i] {
119 | normalizedGrid[i][j] = reci[minRow+i][minCol+j]
120 | }
121 | }
122 | return Recipe(normalizedGrid)
123 | }
124 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kar
2 |
3 | Kar is a 2D crafting/mining/platformer game (abandoned).
4 |
5 |
6 |
7 | ## Controls
8 |
9 | ### Main Menu
10 |
11 | | Key | Function |
12 | | ---------------- | --------------------------------- |
13 | | Enter | Open/Close menu |
14 | | W/S | Menu navigation (up/down) |
15 | | → | Confirm the selected menu option. |
16 |
17 | ### Gameplay
18 |
19 | | Key | Function |
20 | | ----------------------- | ----------------------------------------------------------------- |
21 | | W | Look up |
22 | | S | Look down |
23 | | A | Move left |
24 | | D | Move right |
25 | | ⇧ ShiftRight | Run |
26 | | Space | Jump |
27 | | → | Break block (hold) |
28 | | ← | Place block (adds block if selected item in inventory is block) |
29 | | ← | Throw throwable item (if selected item in inventory is throwable) |
30 | | E | Select next inventory slot |
31 | | Q | Select previous inventory slot |
32 | | R | Set Quick-Slot 1 |
33 | | T | Set Quick-Slot 2 |
34 | | Tab | Switch between Quick-Slot 1 and Quick-Slot 2 |
35 | | ↑ | Open/Close 2x2 Crafting mode |
36 | | ↑ | Open/Close 3x3 Crafting mode (if looking to Crafting Table) |
37 | | L | Switch between camera modes |
38 |
39 | #### Special
40 |
41 |
42 | | Key | Function |
43 | | ------------------------- | ---------------------------------------------------------------------------------------------- |
44 | | ⇧+→ | While running at maximum speed, hold down the right arrow key and hit the block to destroy it. |
45 |
46 |
47 | ### Crafting Mode
48 |
49 | | Key | Function |
50 | | ------------ | ----------------------------------------------------------------------------------- |
51 | | → | Move 1 item from inventory to table |
52 | | ← | Move 1 item from table to inventory |
53 | | ↓ | Apply recipe and add to inventory (if space available) |
54 | | ↑ | Exits crafting mode and adds slots to inventory, drops items into world if no space |
55 | | E | Select next inventory slot |
56 | | Q | Select previous inventory slot |
57 |
--------------------------------------------------------------------------------
/tilemap/tilemap.go:
--------------------------------------------------------------------------------
1 | package tilemap
2 |
3 | import (
4 | "bytes"
5 | "image"
6 | "image/color"
7 | "image/png"
8 | "log"
9 | "math"
10 |
11 | "github.com/setanarut/kar/items"
12 |
13 | "github.com/setanarut/v"
14 | )
15 |
16 | var (
17 | Up = image.Point{0, -1} // {0, -1}
18 | Down = image.Point{0, 1} // {0, 1}
19 | Left = image.Point{-1, 0} // {-1, 0}
20 | Right = image.Point{1, 0} // {1, 0}
21 | )
22 |
23 | type TileMap struct {
24 | Grid [][]uint8
25 | W, H int
26 | TileW, TileH int
27 | }
28 |
29 | func MakeTileMap(w, h, tileW, tileH int) TileMap {
30 | return TileMap{
31 | Grid: MakeGrid(w, h),
32 | W: w,
33 | H: h,
34 | TileW: tileW,
35 | TileH: tileH,
36 | }
37 | }
38 |
39 | func MakeGrid(width, height int) [][]uint8 {
40 | var tm [][]uint8
41 | for range height {
42 | tm = append(tm, make([]uint8, width))
43 | }
44 | return tm
45 | }
46 |
47 | func (t *TileMap) Raycast(pos image.Point, dirX, dirY, dist int) (image.Point, bool) {
48 | // True if exactly one of the components is non-zero
49 | if (dirX != 0 && dirY == 0) || (dirX == 0 && dirY != 0) {
50 | for range dist {
51 | pos.X += dirX
52 | pos.Y += dirY
53 | if t.GetID(pos.X, pos.Y) != items.Air {
54 | return pos, true
55 | }
56 | }
57 | } else {
58 | return image.Point{}, false
59 | }
60 | return image.Point{}, false
61 | }
62 |
63 | func (t *TileMap) WorldToTile(x, y float64) image.Point {
64 | return image.Point{int(math.Floor(x / float64(t.TileW))), int(math.Floor(y / float64(t.TileH)))}
65 | }
66 | func (t *TileMap) WorldToTile2(x, y float64) (int, int) {
67 | return int(math.Floor(x / float64(t.TileW))), int(math.Floor(y / float64(t.TileH)))
68 | }
69 |
70 | func (t *TileMap) FloorToBlockCenter(x, y float64) v.Vec {
71 | return t.TileToWorld(t.WorldToTile(x, y))
72 | }
73 |
74 | // Tile coords to block center
75 | func (t *TileMap) TileToWorld(p image.Point) v.Vec {
76 | return v.Vec{float64((p.X * t.TileW) + t.TileW/2), float64((p.Y * t.TileH) + t.TileH/2)}
77 | }
78 |
79 | func (t *TileMap) GetID(x, y int) uint8 {
80 | if x < 0 || x >= t.W || y < 0 || y >= t.H {
81 | return 0
82 | }
83 | return t.Grid[y][x]
84 | }
85 |
86 | func (t *TileMap) GetIDUnchecked(coords image.Point) uint8 {
87 | return t.Grid[coords.Y][coords.X]
88 | }
89 |
90 | func (t *TileMap) TileIDProperty(x, y int) items.ItemProperty {
91 | return items.Property[t.GetID(x, y)]
92 | }
93 |
94 | func (t *TileMap) Set(x, y int, id uint8) {
95 | if x < 0 || x >= t.W || y < 0 || y >= t.H {
96 | return
97 | }
98 | t.Grid[y][x] = id
99 | }
100 | func (t *TileMap) SetUnchecked(coord image.Point, id uint8) {
101 | t.Grid[coord.Y][coord.X] = id
102 | }
103 |
104 | func (t *TileMap) GetTileRect(x, y int) (rectX, rectY, rectW, rectH float64) {
105 | return float64(x * t.TileW), float64(y * t.TileH), float64(t.TileW), float64(t.TileH)
106 | }
107 |
108 | func (t *TileMap) GetTileBottom(x, y int) float64 {
109 | return float64((y + 1) * t.TileH)
110 | }
111 |
112 | func (t *TileMap) FindSpawnPosition() image.Point {
113 | x := 20 * 20
114 | for y := range t.H - 1 {
115 | upperTile := t.GetID(x, y)
116 | downTile := t.GetID(x, y+1)
117 | if downTile != items.Air && upperTile == items.Air {
118 | return image.Point{x, y}
119 | }
120 | }
121 | return image.Point{}
122 | }
123 | func (t *TileMap) FindSpawnPosition2(x, y int) image.Point {
124 | for i := range 10 {
125 | if t.GetID(x, y+i) == items.Air && t.GetID(x, (y+i)+1) != items.Air {
126 | return image.Point{x, y + i}
127 | }
128 | }
129 |
130 | return image.Point{}
131 | }
132 |
133 | func (t *TileMap) GetImageByte() []byte {
134 | buf := &bytes.Buffer{}
135 | if err := png.Encode(buf, t.GetImage()); err != nil {
136 | log.Fatal(err)
137 | }
138 | return buf.Bytes()
139 | }
140 |
141 | func (tm *TileMap) GetImage() *image.RGBA {
142 | im := image.NewRGBA(image.Rect(0, 0, tm.W, tm.H))
143 | for y := range tm.H {
144 | for x := range tm.W {
145 | id := tm.Grid[y][x]
146 | v, ok := items.ColorMap[id]
147 | if ok {
148 | im.Set(x, y, v)
149 | } else {
150 | im.Set(x, y, color.Black)
151 | }
152 | }
153 | }
154 | return im
155 | }
156 |
--------------------------------------------------------------------------------
/sys_camera.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "math"
5 |
6 | "github.com/setanarut/kar/items"
7 | "github.com/setanarut/kar/res"
8 |
9 | "github.com/hajimehoshi/ebiten/v2"
10 | "github.com/hajimehoshi/ebiten/v2/inpututil"
11 | "github.com/setanarut/kamera/v2"
12 | )
13 |
14 | type Camera struct{}
15 |
16 | func (c *Camera) Init() {
17 | cameraRes.SmoothOptions.LerpSpeedX = 0.4
18 | cameraRes.SmoothOptions.LerpSpeedY = 0.0
19 | }
20 | func (c *Camera) Update() {
21 | if world.Alive(currentPlayer) {
22 | playerAABB := mapAABB.GetUnchecked(currentPlayer)
23 | // Toggle camera follow
24 | if inpututil.IsKeyJustPressed(ebiten.KeyL) {
25 | switch cameraRes.SmoothType {
26 | case kamera.Lerp:
27 | cameraRes.SetCenter(playerAABB.Pos.X, playerAABB.Pos.Y)
28 | cameraRes.SmoothType = kamera.SmoothDamp
29 | case kamera.SmoothDamp:
30 | cameraRes.SetCenter(playerAABB.Pos.X, playerAABB.Pos.Y)
31 | cameraRes.SmoothType = kamera.None
32 | case kamera.None:
33 | cameraRes.SetCenter(playerAABB.Pos.X, playerAABB.Pos.Y)
34 | cameraRes.SmoothType = kamera.Lerp
35 | }
36 | }
37 | // Camera follow
38 | if mapHealth.GetUnchecked(currentPlayer).Current > 0 {
39 | if cameraRes.SmoothType == kamera.Lerp {
40 | // if playerCenterX < CameraRes.X {
41 | // CameraRes.X -= CameraRes.Width
42 | // }
43 | // if playerCenterX > CameraRes.Right() {
44 | // CameraRes.X += CameraRes.Width
45 | // }
46 | if playerAABB.Pos.Y < cameraRes.Y {
47 | cameraRes.SetTopLeft(cameraRes.X, cameraRes.Y-cameraRes.Height)
48 | }
49 | if playerAABB.Pos.Y > cameraRes.Bottom() {
50 | cameraRes.SetTopLeft(cameraRes.X, cameraRes.Y+cameraRes.Height)
51 | }
52 | cameraRes.LookAt(playerAABB.Pos.X, playerAABB.Pos.Y)
53 | cameraRes.X = math.Floor(cameraRes.X)
54 | cameraRes.Y = math.Floor(cameraRes.Y)
55 |
56 | } else if cameraRes.SmoothType == kamera.SmoothDamp {
57 | cameraRes.LookAt(playerAABB.Pos.X, playerAABB.Pos.Y)
58 | // cameraRes.TempTargetX = math.Floor(cameraRes.TempTargetX)
59 | // cameraRes.TempTargetY = math.Floor(cameraRes.TempTargetY)
60 | cameraRes.X = math.Floor(cameraRes.X)
61 | cameraRes.Y = math.Floor(cameraRes.Y)
62 | } else if cameraRes.SmoothType == kamera.None {
63 | cameraRes.SetCenter(playerAABB.Pos.X, playerAABB.Pos.Y)
64 | }
65 | }
66 | }
67 | }
68 | func (c *Camera) Draw() {
69 |
70 | // DRAW TILEMAP
71 |
72 | // clamp tilemap bounds
73 | camMin := tileMapRes.WorldToTile(cameraRes.X, cameraRes.Y)
74 | camMin.X = min(max(camMin.X, 0), tileMapRes.W)
75 | camMin.Y = min(max(camMin.Y, 0), tileMapRes.H)
76 | camMaxX := min(max(camMin.X+renderArea.X, 0), tileMapRes.W)
77 | camMaxY := min(max(camMin.Y+renderArea.Y, 0), tileMapRes.H)
78 |
79 | // draw tiles
80 | for y := camMin.Y; y < camMaxY; y++ {
81 | for x := camMin.X; x < camMaxX; x++ {
82 | tileID := tileMapRes.Grid[y][x]
83 | if tileID != 0 {
84 | px, py := float64(x*tileMapRes.TileW), float64(y*tileMapRes.TileH)
85 | if x == ceilBlockCoord.X && y == ceilBlockCoord.Y {
86 | if tileID == items.Bedrock {
87 | if ceilBlockTick > 0 {
88 | ceilBlockTick -= 0.1
89 | }
90 | py -= ceilBlockTick
91 | }
92 | }
93 |
94 | colorMDIO.GeoM.Reset()
95 | colorMDIO.GeoM.Translate(px, py)
96 |
97 | if items.HasTag(tileID, items.UnbreakableBlock) {
98 | cameraRes.DrawWithColorM(res.BlockUnbreakable[tileID], colorM, colorMDIO, Screen)
99 | } else {
100 | if x == gameDataRes.TargetBlockCoord.X && y == gameDataRes.TargetBlockCoord.Y {
101 | i := mapRange(gameDataRes.BlockHealth, 0, 180, 0, 5)
102 | cameraRes.DrawWithColorM(res.BlockCrackFrames[tileID][int(i)], colorM, colorMDIO, Screen)
103 | } else {
104 | cameraRes.DrawWithColorM(res.BlockCrackFrames[tileID][0], colorM, colorMDIO, Screen)
105 | }
106 | }
107 | }
108 | }
109 | }
110 |
111 | // Draw target tile border
112 | if gameDataRes.IsRayHit {
113 | colorMDIO.GeoM.Reset()
114 | colorMDIO.GeoM.Translate(
115 | float64(gameDataRes.TargetBlockCoord.X*tileMapRes.TileW)-1,
116 | float64(gameDataRes.TargetBlockCoord.Y*tileMapRes.TileH)-1,
117 | )
118 | cameraRes.DrawWithColorM(res.BlockBorder, colorM, colorMDIO, Screen)
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/items/recipes.go:
--------------------------------------------------------------------------------
1 | package items
2 |
3 | type Recipe [][]Slot
4 |
5 | var CraftingRecipes map[uint8]Recipe
6 | var FurnaceRecipes map[uint8]Recipe
7 |
8 | func init() {
9 |
10 | CraftingRecipes = make(map[uint8]Recipe)
11 | FurnaceRecipes = make(map[uint8]Recipe)
12 |
13 | CraftingRecipes[OakPlanks] = [][]Slot{{Slot{ID: OakLog}}}
14 |
15 | CraftingRecipes[Stick] = [][]Slot{
16 | {Slot{ID: OakPlanks}, Slot{}},
17 | {Slot{ID: OakPlanks}, Slot{}},
18 | }
19 | CraftingRecipes[Torch] = [][]Slot{
20 | {Slot{ID: Coal}, Slot{}},
21 | {Slot{ID: Stick}, Slot{}},
22 | }
23 |
24 | CraftingRecipes[CraftingTable] = [][]Slot{
25 | {Slot{ID: OakPlanks}, Slot{ID: OakPlanks}},
26 | {Slot{ID: OakPlanks}, Slot{ID: OakPlanks}},
27 | }
28 |
29 | // Axe
30 | CraftingRecipes[WoodenAxe] = [][]Slot{
31 | {Slot{ID: OakPlanks}, Slot{ID: OakPlanks}, Slot{ID: 0}},
32 | {Slot{ID: OakPlanks}, Slot{ID: Stick}, Slot{ID: 0}},
33 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
34 | }
35 |
36 | CraftingRecipes[StoneAxe] = [][]Slot{
37 | {Slot{ID: Stone}, Slot{ID: Stone}, Slot{ID: 0}},
38 | {Slot{ID: Stone}, Slot{ID: Stick}, Slot{ID: 0}},
39 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
40 | }
41 |
42 | CraftingRecipes[IronAxe] = [][]Slot{
43 | {Slot{ID: IronIngot}, Slot{ID: IronIngot}, Slot{ID: 0}},
44 | {Slot{ID: IronIngot}, Slot{ID: Stick}, Slot{ID: 0}},
45 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
46 | }
47 |
48 | CraftingRecipes[DiamondAxe] = [][]Slot{
49 | {Slot{ID: Diamond}, Slot{ID: Diamond}, Slot{ID: 0}},
50 | {Slot{ID: Diamond}, Slot{ID: Stick}, Slot{ID: 0}},
51 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
52 | }
53 |
54 | // Shovel
55 | CraftingRecipes[WoodenShovel] = [][]Slot{
56 | {Slot{ID: 0}, Slot{ID: OakPlanks}, Slot{ID: 0}},
57 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
58 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
59 | }
60 |
61 | CraftingRecipes[StoneShovel] = [][]Slot{
62 | {Slot{ID: 0}, Slot{ID: Stone}, Slot{ID: 0}},
63 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
64 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
65 | }
66 |
67 | CraftingRecipes[IronShovel] = [][]Slot{
68 | {Slot{ID: 0}, Slot{ID: IronIngot}, Slot{ID: 0}},
69 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
70 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
71 | }
72 |
73 | CraftingRecipes[DiamondShovel] = [][]Slot{
74 | {Slot{ID: 0}, Slot{ID: Diamond}, Slot{ID: 0}},
75 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
76 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
77 | }
78 |
79 | // Pickaxe
80 | CraftingRecipes[WoodenPickaxe] = [][]Slot{
81 | {Slot{ID: OakPlanks}, Slot{ID: OakPlanks}, Slot{ID: OakPlanks}},
82 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
83 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
84 | }
85 |
86 | CraftingRecipes[StonePickaxe] = [][]Slot{
87 | {Slot{ID: Stone}, Slot{ID: Stone}, Slot{ID: Stone}},
88 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
89 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
90 | }
91 |
92 | CraftingRecipes[IronPickaxe] = [][]Slot{
93 | {Slot{ID: IronIngot}, Slot{ID: IronIngot}, Slot{ID: IronIngot}},
94 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
95 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
96 | }
97 |
98 | CraftingRecipes[DiamondPickaxe] = [][]Slot{
99 | {Slot{ID: Diamond}, Slot{ID: Diamond}, Slot{ID: Diamond}},
100 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
101 | {Slot{ID: 0}, Slot{ID: Stick}, Slot{ID: 0}},
102 | }
103 |
104 | CraftingRecipes[Furnace] = [][]Slot{
105 | {Slot{ID: Stone}, Slot{ID: Stone}, Slot{ID: Stone}},
106 | {Slot{ID: Stone}, Slot{ID: Air}, Slot{ID: Stone}},
107 | {Slot{ID: Stone}, Slot{ID: Stone}, Slot{ID: Stone}},
108 | }
109 |
110 | // output item multiplier
111 | for _, recipe := range CraftingRecipes {
112 | recipe[0][0].Quantity = 1
113 | }
114 | CraftingRecipes[OakPlanks][0][0].Quantity = 4
115 | CraftingRecipes[Stick][0][0].Quantity = 4
116 |
117 | // --- FURNACE RECIPES ---
118 |
119 | FurnaceRecipes[IronIngot] = [][]Slot{
120 | {Slot{ID: RawIron}, Slot{}},
121 | {Slot{ID: Coal}, Slot{}},
122 | }
123 | FurnaceRecipes[GoldIngot] = [][]Slot{
124 | {Slot{ID: RawGold}, Slot{}},
125 | {Slot{ID: Coal}, Slot{}},
126 | }
127 |
128 | // output item multiplier
129 | for _, recipe := range FurnaceRecipes {
130 | recipe[0][0].Quantity = 1
131 | }
132 |
133 | }
134 |
--------------------------------------------------------------------------------
/archetypes.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "math/rand/v2"
5 |
6 | "github.com/setanarut/kar/items"
7 |
8 | "github.com/mlange-42/ark/ecs"
9 | "github.com/setanarut/v"
10 | )
11 |
12 | var (
13 | mapFacing = ecs.NewMap[Facing](&world)
14 | mapPos = ecs.NewMap[Position](&world)
15 | mapAABB = ecs.NewMap[AABB](&world)
16 | mapDurability = ecs.NewMap[Durability](&world)
17 | mapHealth = ecs.NewMap[Health](&world)
18 | mapCollisionDelayer = ecs.NewMap[CollisionDelayer](&world)
19 | mapPlatform = ecs.NewMap3[AABB, Velocity, PlatformType](&world)
20 | mapEnemy = ecs.NewMap4[AABB, Velocity, MobileID, AnimationTick](&world)
21 | mapProjectile = ecs.NewMap3[ItemID, Position, Velocity](&world)
22 | mapDroppedItem = ecs.NewMap4[ItemID, Position, AnimationIndex, CollisionDelayer](&world)
23 | mapEffect = ecs.NewMap4[ItemID, Position, Velocity, Rotation](&world)
24 | mapPlayer = ecs.NewMap5[AABB, Velocity, Health, Controller, Facing](&world)
25 | )
26 |
27 | // Query Filters
28 | var (
29 | filterCollisionDelayer = ecs.NewFilter1[CollisionDelayer](&world)
30 | filterPlatform = ecs.NewFilter3[AABB, Velocity, PlatformType](&world)
31 | filterEnemy = ecs.NewFilter4[AABB, Velocity, MobileID, AnimationTick](&world)
32 | filterProjectile = ecs.NewFilter3[ItemID, Position, Velocity](&world).Without(ecs.C[Rotation]())
33 | filterDroppedItem = ecs.NewFilter3[ItemID, Position, AnimationIndex](&world)
34 | filterEffect = ecs.NewFilter4[ItemID, Position, Velocity, Rotation](&world)
35 | filterPlayer = ecs.NewFilter5[AABB, Velocity, Health, Controller, Facing](&world)
36 | )
37 |
38 | func SpawnItem(pos Vec, id uint8, durability int) ecs.Entity {
39 | e := mapDroppedItem.NewEntity(
40 | ptr(ItemID(id)),
41 | &Position{pos.X, pos.Y},
42 | ptr(AnimationIndex(rand.IntN(len(sinspaceOffsets)-1))),
43 | ptr(CollisionDelayer(ItemCollisionDelay)),
44 | )
45 | if items.HasTag(id, items.Tool) {
46 | mapDurability.Add(e, ptr(Durability(durability)))
47 | }
48 | return e
49 | }
50 |
51 | // func SpawnEnemy(pos, vel Vec) ecs.Entity {
52 | // return mapEnemy.NewEntity(
53 | // &AABB{Pos: pos, Half: v.Vec{40, 6}},
54 | // (*Velocity)(&vel),
55 | // ptr(WormID),
56 | // )
57 | // }
58 |
59 | func SpawnEffect(pos Vec, id uint8) {
60 | mapEffect.NewEntity(ptr(ItemID(id)), &Position{pos.X - 10, pos.Y - 10}, &Velocity{-1, 0}, ptr(Rotation(-0.1)))
61 | mapEffect.NewEntity(ptr(ItemID(id)), &Position{pos.X + 2, pos.Y - 10}, &Velocity{1, 0}, ptr(Rotation(0.1)))
62 | mapEffect.NewEntity(ptr(ItemID(id)), &Position{pos.X - 10, pos.Y + 2}, &Velocity{-0.5, 0}, ptr(Rotation(-0.1)))
63 | mapEffect.NewEntity(ptr(ItemID(id)), &Position{pos.X + 2, pos.Y + 2}, &Velocity{0.5, 0}, ptr(Rotation(0.1)))
64 | }
65 |
66 | func SpawnProjectile(id uint8, pos, vel Vec) ecs.Entity {
67 | var pid ItemID = ItemID(id)
68 | return mapProjectile.NewEntity(
69 | &pid,
70 | (*Position)(&pos),
71 | (*Velocity)(&vel),
72 | )
73 | }
74 |
75 | func SpawnPlayer(pos Vec) ecs.Entity {
76 | ctrl := &Controller{
77 | CurrentState: "falling",
78 | Gravity: 0.19,
79 | JumpPower: -3.7,
80 | MaxFallSpeed: 100.0,
81 | MaxRunSpeed: 3.0,
82 | MaxWalkSpeed: 1.6,
83 | Acceleration: 0.08,
84 | SkiddingFriction: 0.08,
85 | AirSkiddingDecel: 0.1,
86 | JumpHoldTime: 20.0,
87 | JumpBoost: -0.1,
88 | MinSpeedThresForJumpBoostMultiplier: 0.1,
89 | JumpBoostMultiplier: 1.01,
90 | SpeedJumpFactor: 0.3,
91 | ShortJumpVelocity: -2.0,
92 | JumpReleaseTimer: 5,
93 | WalkAcceleration: 0.04,
94 | WalkDeceleration: 0.04,
95 | RunAcceleration: 0.04,
96 | RunDeceleration: 0.04,
97 | SkiddingJumpEnabled: true,
98 | }
99 | return mapPlayer.NewEntity(
100 | &AABB{
101 | Pos: pos,
102 | Half: Vec{8, 8},
103 | },
104 | &Velocity{},
105 | &Health{20, 20},
106 | ctrl,
107 | &Facing{v.Down.X, v.Down.Y},
108 | )
109 | }
110 |
--------------------------------------------------------------------------------
/items/inventory.go:
--------------------------------------------------------------------------------
1 | package items
2 |
3 | type Slot struct {
4 | ID uint8
5 | Quantity uint8
6 | Durability int
7 | }
8 |
9 | type Inventory struct {
10 | CurrentSlotIndex int
11 | QuickSlot1, QuickSlot2 int
12 | Slots []Slot
13 | }
14 |
15 | func NewInventory(size int) *Inventory {
16 | return &Inventory{
17 | QuickSlot1: 0,
18 | QuickSlot2: 1,
19 | CurrentSlotIndex: 0,
20 | Slots: make([]Slot, size, 40),
21 | }
22 | }
23 |
24 | // AddItemIfEmpty adds item to inventory if empty
25 | func (i *Inventory) AddItemIfEmpty(id uint8, dura int) bool {
26 | idx, ok1 := i.HasItemStackSpace(id)
27 | if ok1 {
28 | i.Slots[idx].Quantity++
29 | return true
30 | } else {
31 | i2, ok2 := i.HasEmptySlot()
32 | if ok2 {
33 | i.Slots[i2].Quantity++
34 | i.Slots[i2].ID = id
35 | i.Slots[i2].Durability = dura
36 | return true
37 | }
38 | }
39 | return false
40 | }
41 |
42 | func (i *Inventory) SetSlot(slotIndex int, id uint8, quantity uint8, dur int) {
43 | if quantity > 0 {
44 | i.Slots[slotIndex] = Slot{
45 | ID: id,
46 | Quantity: quantity,
47 | Durability: dur,
48 | }
49 | }
50 | }
51 | func (i *Inventory) SetSize(n int) {
52 | if n < 1 || n > cap(i.Slots) {
53 | return
54 | }
55 | if n <= i.CurrentSlotIndex {
56 | i.CurrentSlotIndex = n - 1
57 | }
58 | if len(i.Slots) > n {
59 | i.Slots = i.Slots[:n]
60 | i.ResetUnusedSlots()
61 | } else {
62 | i.Slots = i.Slots[:n]
63 | }
64 | }
65 |
66 | func (i *Inventory) ResetUnusedSlots() {
67 | fullSlice := i.Slots[:cap(i.Slots)]
68 | for idx := len(i.Slots); idx < cap(i.Slots); idx++ {
69 | fullSlice[idx] = Slot{}
70 | }
71 | i.Slots = fullSlice[:len(i.Slots)]
72 | }
73 |
74 | func (i *Inventory) SelectNextSlot() {
75 | i.CurrentSlotIndex = (i.CurrentSlotIndex + 1) % len(i.Slots)
76 | }
77 |
78 | func (i *Inventory) SelectPrevSlot() {
79 | i.CurrentSlotIndex = (i.CurrentSlotIndex - 1 + len(i.Slots)) % len(i.Slots)
80 | }
81 |
82 | func (i *Inventory) RemoveItem(id uint8) bool {
83 | idx, ok := i.HasItem(id)
84 | if ok {
85 | i.Slots[idx].Quantity--
86 | return true
87 | }
88 | return false
89 | }
90 | func (i *Inventory) RemoveItemFromSelectedSlot() (uint8, int) {
91 | quantity := i.CurrentSlotQuantity()
92 | id := i.CurrentSlotID()
93 | dura := i.CurrentSlot().Durability
94 | if quantity == 1 {
95 | i.ClearCurrentSlot()
96 | return id, dura
97 | }
98 | if quantity > 0 {
99 |
100 | i.Slots[i.CurrentSlotIndex].Quantity--
101 | return id, dura
102 | }
103 | return 0, 0
104 | }
105 |
106 | func (i *Inventory) CurrentSlot() *Slot {
107 | return &i.Slots[i.CurrentSlotIndex]
108 | }
109 |
110 | func (i *Inventory) CurrentSlotID() uint8 {
111 | return i.Slots[i.CurrentSlotIndex].ID
112 | }
113 |
114 | func (i *Inventory) CurrentSlotQuantity() uint8 {
115 | return i.Slots[i.CurrentSlotIndex].Quantity
116 | }
117 |
118 | func (i *Inventory) ClearSlot(index int) {
119 | i.Slots[index] = Slot{}
120 | }
121 | func (i *Inventory) IsCurrentSlotEmpty() bool {
122 | return i.Slots[i.CurrentSlotIndex].Quantity <= 0 || i.Slots[i.CurrentSlotIndex].ID == Air
123 | }
124 |
125 | func (i *Inventory) Reset() {
126 | for idx := range i.Slots {
127 | i.Slots[idx] = Slot{}
128 | }
129 | i.CurrentSlotIndex = 0
130 | i.Slots = i.Slots[:16]
131 | }
132 | func (i *Inventory) RandomFillAllSlots() {
133 | for idx := range i.Slots {
134 | randItemID := RandomItem()
135 | dur := GetDefaultDurability(randItemID)
136 | i.SetSlot(idx, randItemID, Property[randItemID].MaxStackSize, dur)
137 | }
138 | }
139 |
140 | func (i *Inventory) ClearCurrentSlot() {
141 | i.ClearSlot(i.CurrentSlotIndex)
142 | }
143 |
144 | func (i *Inventory) HasEmptySlot() (index int, ok bool) {
145 | // önce seçili slot boşsa tercih et
146 | if i.CurrentSlotQuantity() == 0 {
147 | return i.CurrentSlotIndex, true
148 | } else {
149 | for idx, v := range i.Slots {
150 | if v.Quantity == 0 {
151 | return idx, true
152 | }
153 | }
154 | }
155 | return -1, false
156 | }
157 |
158 | func (i *Inventory) HasItemStackSpace(id uint8) (index int, ok bool) {
159 | for idx, v := range i.Slots {
160 | s := Property[v.ID].MaxStackSize
161 | if v.ID == id && v.Quantity < 64 && v.Quantity > 0 && s != 1 {
162 | return idx, true
163 | }
164 | }
165 | return -1, false
166 | }
167 |
168 | func (i *Inventory) HasItem(id uint8) (index int, ok bool) {
169 | for idx, v := range i.Slots {
170 | if v.ID == id && v.Quantity > 0 {
171 | return idx, true
172 | }
173 | }
174 | return -1, false
175 | }
176 |
--------------------------------------------------------------------------------
/singleton.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "image"
5 | "image/color"
6 | "log"
7 | "math"
8 | "math/rand/v2"
9 | "time"
10 |
11 | "github.com/setanarut/kar/items"
12 | "github.com/setanarut/kar/tilemap"
13 |
14 | "github.com/hajimehoshi/ebiten/v2"
15 | "github.com/hajimehoshi/ebiten/v2/colorm"
16 | "github.com/hajimehoshi/ebiten/v2/text/v2"
17 | arkserde "github.com/mlange-42/ark-serde"
18 | "github.com/mlange-42/ark/ecs"
19 | "github.com/quasilyte/gdata"
20 | "github.com/setanarut/kamera/v2"
21 | "github.com/setanarut/v"
22 | )
23 |
24 | type Vec = v.Vec
25 |
26 | const (
27 | SnowballGravity float64 = 0.5
28 | SnowballSpeedX float64 = 3.5
29 | SnowballMaxFallVelocity float64 = 2.5
30 | SnowballBounceHeight float64 = 9.0
31 | ItemGravity float64 = 3.0
32 | PlayerBestToolDamage float64 = 5.0
33 | PlayerDefaultDamage float64 = 1.0
34 | RaycastDist int = 4 // block unit
35 | Tick time.Duration = time.Second / 60
36 | ItemCollisionDelay time.Duration = time.Second / 2
37 | )
38 |
39 | // MOB ID
40 | const (
41 | CrabID MobileID = 1
42 | )
43 |
44 | var debugEnabled bool = false
45 |
46 | var (
47 | Screen *ebiten.Image
48 | ScreenSize = Vec{500, 340}
49 | WindowScale = 2.0
50 | )
51 | var (
52 | currentGameState = "mainmenu"
53 | )
54 | var (
55 | ceilBlockCoord image.Point
56 | ceilBlockTick float64
57 | dropItemAABB = &AABB{Half: Vec{4, 4}}
58 | )
59 | var (
60 | world ecs.World = ecs.NewWorld(100)
61 | currentPlayer ecs.Entity
62 | renderArea = image.Point{(int(ScreenSize.X) / 20) + 3, (int(ScreenSize.Y) / 20) + 3}
63 | dataManager *gdata.Manager
64 | sinspaceOffsets []float64 = sinspace(0, 2*math.Pi, 3, 60)
65 | backgroundColor color.RGBA = color.RGBA{36, 36, 39, 255}
66 | gameTileMapGenerator tilemap.Generator
67 | colorMDIO *colorm.DrawImageOptions = &colorm.DrawImageOptions{}
68 | colorM colorm.ColorM = colorm.ColorM{}
69 | textDO *text.DrawOptions = &text.DrawOptions{
70 | DrawImageOptions: ebiten.DrawImageOptions{},
71 | LayoutOptions: text.LayoutOptions{
72 | LineSpacing: 10,
73 | },
74 | }
75 | tileCollider = Collider{
76 | TileMap: tileMapRes.Grid,
77 | TileSize: image.Point{tileMapRes.TileW, tileMapRes.TileH},
78 | }
79 | )
80 |
81 | func init() {
82 | var err error
83 | dataManager, err = gdata.Open(gdata.Config{AppName: "kar"})
84 | if err != nil {
85 | panic(err)
86 | }
87 | gameTileMapGenerator = tilemap.NewGenerator(tileMapRes)
88 |
89 | }
90 |
91 | func NewGame() {
92 | world.Reset()
93 | inventoryRes.Reset()
94 | gameDataRes = gameData{}
95 | animPlayer.Data = animDefaultPlaybackData
96 |
97 | mapResInventory.Add(inventoryRes)
98 | mapResCraftingtable.Add(craftingTableRes)
99 | mapResCamera.Add(cameraRes)
100 | mapResGameData.Add(&gameDataRes)
101 | mapResAnimPlaybackData.Add(&animPlayer.Data)
102 | mapResTilemap.Add(&tileMapRes)
103 |
104 | gameTileMapGenerator.SetSeed(rand.Int())
105 | gameTileMapGenerator.Generate()
106 | spawnCoord := tileMapRes.FindSpawnPosition()
107 | SpawnPos := tileMapRes.TileToWorld(spawnCoord)
108 | currentPlayer = SpawnPlayer(SpawnPos)
109 | box := mapAABB.Get(currentPlayer)
110 | box.SetBottom(tileMapRes.GetTileBottom(spawnCoord.X, spawnCoord.Y))
111 | cameraRes.SmoothType = kamera.SmoothDamp
112 | cameraRes.SetCenter(box.Pos.X, box.Pos.Y)
113 | inventoryRes.SetSlot(8, items.Snowball, 64, 0)
114 | }
115 |
116 | func SaveGame() {
117 | jsonData, err := arkserde.Serialize(&world, arkserde.Opts.Compress())
118 | if err != nil {
119 | log.Fatal(err)
120 | }
121 | dataManager.SaveItem("01save", jsonData)
122 |
123 | }
124 |
125 | func LoadGame() {
126 | if dataManager.ItemExists("01save") {
127 | world.Reset()
128 | mapResInventory.Add(inventoryRes)
129 | mapResCraftingtable.Add(craftingTableRes)
130 | mapResCamera.Add(cameraRes)
131 | mapResGameData.Add(&gameDataRes)
132 | mapResAnimPlaybackData.Add(&animPlayer.Data)
133 | mapResTilemap.Add(&tileMapRes)
134 |
135 | jsonData, err := dataManager.LoadItem("01save")
136 | if err != nil {
137 | log.Fatal(err)
138 | }
139 | err = arkserde.Deserialize(jsonData, &world, arkserde.Opts.Compress())
140 | if err != nil {
141 | log.Fatal(err)
142 | }
143 |
144 | if !world.Alive(currentPlayer) {
145 | q := filterPlayer.Query()
146 | q.Next()
147 | currentPlayer = q.Entity()
148 | q.Close()
149 | }
150 |
151 | animPlayer.Update()
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/sys_debug.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "fmt"
5 | "image"
6 |
7 | "github.com/setanarut/kar/items"
8 |
9 | "github.com/hajimehoshi/ebiten/v2"
10 | "github.com/hajimehoshi/ebiten/v2/ebitenutil"
11 | "github.com/hajimehoshi/ebiten/v2/inpututil"
12 | "github.com/setanarut/v"
13 | )
14 |
15 | type Debug struct {
16 | drawItemHitboxEnabled bool
17 | drawPlayerTileHitboxEnabled bool
18 | drawDebugTextEnabled bool
19 | tile uint8
20 | }
21 |
22 | func (d *Debug) Init() {}
23 | func (d *Debug) Update() {
24 | if inpututil.IsKeyJustPressed(ebiten.Key1) {
25 | pos := tileMapRes.FloorToBlockCenter(cameraRes.ScreenToWorld(ebiten.CursorPosition()))
26 | mapPlatform.NewEntity(
27 | &AABB{
28 | Pos: v.Vec{pos.X, pos.Y},
29 | Half: v.Vec{20, 10},
30 | },
31 | &Velocity{1, 0},
32 | ptr(PlatformType("solid")),
33 | )
34 | }
35 | if inpututil.IsKeyJustPressed(ebiten.Key2) {
36 | pos := tileMapRes.FloorToBlockCenter(cameraRes.ScreenToWorld(ebiten.CursorPosition()))
37 | mapPlatform.NewEntity(
38 | &AABB{
39 | Pos: v.Vec{pos.X, pos.Y},
40 | Half: v.Vec{10, 10},
41 | },
42 | &Velocity{1, 0},
43 | ptr(PlatformType("oneway")),
44 | )
45 | }
46 | if inpututil.IsKeyJustPressed(ebiten.Key3) {
47 | x, y := cameraRes.ScreenToWorld(ebiten.CursorPosition())
48 | mapEnemy.NewEntity(
49 | &AABB{
50 | Pos: Vec{x, y},
51 | Half: Vec{8, 4.5},
52 | },
53 | &Velocity{0.4, 0},
54 | ptr(CrabID),
55 | ptr(AnimationTick(0)),
56 | )
57 | }
58 |
59 | if inpututil.IsKeyJustPressed(ebiten.KeyEqual) {
60 | inventoryRes.SetSize(len(inventoryRes.Slots) + 1)
61 | }
62 | if inpututil.IsKeyJustPressed(ebiten.KeyMinus) {
63 | inventoryRes.SetSize(len(inventoryRes.Slots) - 1)
64 | }
65 |
66 | if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
67 | x, y := cameraRes.ScreenToWorld(ebiten.CursorPosition())
68 | p := tileMapRes.WorldToTile(x, y)
69 | tileMapRes.Set(p.X, p.Y, d.tile)
70 | }
71 | if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) {
72 | x, y := cameraRes.ScreenToWorld(ebiten.CursorPosition())
73 | p := tileMapRes.WorldToTile(x, y)
74 | d.tile = tileMapRes.GetID(p.X, p.Y)
75 | }
76 |
77 | if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
78 | inventoryRes.ClearCurrentSlot()
79 | }
80 | if inpututil.IsKeyJustPressed(ebiten.KeyK) {
81 | inventoryRes.RandomFillAllSlots()
82 | }
83 | if inpututil.IsKeyJustPressed(ebiten.KeyM) {
84 | inventoryRes.SetSlot(0, items.Coal, 64, 0)
85 | inventoryRes.SetSlot(1, items.RawGold, 64, 0)
86 | inventoryRes.SetSlot(2, items.RawIron, 64, 0)
87 | inventoryRes.SetSlot(3, items.Stick, 64, 0)
88 | inventoryRes.SetSlot(4, items.DiamondPickaxe, 1, items.GetDefaultDurability(items.DiamondPickaxe))
89 | inventoryRes.SetSlot(5, items.DiamondShovel, 1, items.GetDefaultDurability(items.DiamondShovel))
90 | inventoryRes.SetSlot(6, items.DiamondAxe, 1, items.GetDefaultDurability(items.DiamondAxe))
91 | inventoryRes.SetSlot(7, items.Diamond, 64, 0)
92 | inventoryRes.SetSlot(8, items.Snowball, 64, 0)
93 |
94 | }
95 | if inpututil.IsKeyJustPressed(ebiten.KeyV) {
96 | d.drawDebugTextEnabled = !d.drawDebugTextEnabled
97 | }
98 | if inpututil.IsKeyJustPressed(ebiten.KeyO) {
99 |
100 | }
101 |
102 | if inpututil.IsKeyJustPressed(ebiten.KeyC) {
103 | d.drawItemHitboxEnabled = !d.drawItemHitboxEnabled
104 | }
105 | if inpututil.IsKeyJustPressed(ebiten.KeyB) {
106 | d.drawPlayerTileHitboxEnabled = !d.drawPlayerTileHitboxEnabled
107 | }
108 |
109 | if inpututil.IsKeyJustPressed(ebiten.KeyF12) {
110 | dataManager.SaveItem("map.png", tileMapRes.GetImageByte())
111 | }
112 | if inpututil.IsKeyJustPressed(ebiten.KeyF11) {
113 | box := mapAABB.GetUnchecked(currentPlayer)
114 | tileMapRes.Set(tileMapRes.W/2, tileMapRes.H-3, items.Air)
115 | box.Pos = tileMapRes.TileToWorld(image.Point{tileMapRes.W / 2, tileMapRes.H - 3})
116 | cameraRes.SetCenter(box.Pos.X, box.Pos.Y)
117 | }
118 |
119 | }
120 | func (d *Debug) Draw() {
121 | if world.Alive(currentPlayer) {
122 | box, vel, _, playerController, _ := mapPlayer.GetUnchecked(currentPlayer)
123 | if d.drawPlayerTileHitboxEnabled {
124 | drawAABB(box)
125 | }
126 | if d.drawDebugTextEnabled {
127 | ebitenutil.DebugPrintAt(Screen, fmt.Sprintf(
128 | "state %v\nVel.X: %.2f\nVel.Y: %.2f\nCamera: %v",
129 | playerController.CurrentState,
130 | vel.X,
131 | vel.Y,
132 | cameraRes,
133 | ), 0, 10)
134 | }
135 |
136 | } else {
137 | ebitenutil.DebugPrintAt(Screen, fmt.Sprintf("Camera: %v", cameraRes), 0, 10)
138 | }
139 |
140 | ebitenutil.DebugPrintAt(Screen, "debug mode", 8, int(ScreenSize.Y-20))
141 | }
142 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
2 | github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1 h1:+kz5iTT3L7uU+VhlMfTb8hHcxLO3TlaELlX8wa4XjA0=
6 | github.com/ebitengine/gomobile v0.0.0-20250923094054-ea854a63cce1/go.mod h1:lKJoeixeJwnFmYsBny4vvCJGVFc3aYDalhuDsfZzWHI=
7 | github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
8 | github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
9 | github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
10 | github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
11 | github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4=
12 | github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY=
13 | github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
14 | github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
15 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
16 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
17 | github.com/hajimehoshi/bitmapfont/v4 v4.1.0 h1:eE3qa5Do4qhowZVIHjsrX5pYyyPN6sAFWMsO7QREm3U=
18 | github.com/hajimehoshi/bitmapfont/v4 v4.1.0/go.mod h1:/PD+aLjAJ0F2UoQx6hkOfXqWN7BkroDUMr5W+IT1dpE=
19 | github.com/hajimehoshi/ebiten/v2 v2.9.4 h1:IlPJpwtksylmmvNhQjv4W2bmCFWXtjY7Z10Esise1bk=
20 | github.com/hajimehoshi/ebiten/v2 v2.9.4/go.mod h1:DAt4tnkYYpCvu3x9i1X/nK/vOruNXIlYq/tBXxnhrXM=
21 | github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
22 | github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
23 | github.com/mlange-42/ark v0.6.4 h1:VSMLeDMqQiLsMV6FjqMU2xSluHu2LGAm5oFugg6myGE=
24 | github.com/mlange-42/ark v0.6.4/go.mod h1:gkS9cuklENPTmSjL2z4DcJgJsIVqF1yNwFlx48Hz/Sw=
25 | github.com/mlange-42/ark-serde v0.3.0 h1:3oy0Bd3m4YO9d/h15ULUIcpSokGSaYSs2R0jKZzmYNU=
26 | github.com/mlange-42/ark-serde v0.3.0/go.mod h1:gpQHLwLO5lqwgKba5fw2peDG4VfUus7u6zvhnN3Jj/A=
27 | github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
28 | github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
29 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
31 | github.com/quasilyte/gdata v0.8.1 h1:cR9TFUHrRciVq1E4hqofan6jcQvmJd9lM1E7YLeUfi8=
32 | github.com/quasilyte/gdata v0.8.1/go.mod h1:VZbd2RCpKR2cbTGuLC1Esnyl1+KFv/jXBJrs/ijL8TA=
33 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
34 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
35 | github.com/setanarut/anim v1.4.0 h1:mbmuByqhCLTOwTdqjgAf5/7GRu92+47I3Ekr/DyQn/g=
36 | github.com/setanarut/anim v1.4.0/go.mod h1:A1RG+zfn+zzWUW3DV8Xy8FMzt+yU5DmMt0X1wn8CHUs=
37 | github.com/setanarut/fastnoise v1.1.1 h1:cD9gUjY9GMxVab+B7AY09j2GAHMdiKzchDE5z6dQ7eE=
38 | github.com/setanarut/fastnoise v1.1.1/go.mod h1:74vG3/RcPPcNi2M0riHJXQp9/+eTSh0rnQ/0WTEY6SU=
39 | github.com/setanarut/kamera/v2 v2.97.2 h1:GiLiYkwq42b5jwXdbjK5OVuQoJK2IrPkOn+JF5bnexg=
40 | github.com/setanarut/kamera/v2 v2.97.2/go.mod h1:jw5q24L5alrt9OfCRbDWyfswlnnZOXMkiETc3NI/zPo=
41 | github.com/setanarut/v v1.2.1 h1:p35oGXDf3HbQ1vW9Wnh8jqCkeSM5s5JcQbf+bl25Qwk=
42 | github.com/setanarut/v v1.2.1/go.mod h1:b+qEuRKbtbcJvFqlu/ozRSp/P0ouWDeLWSMyog6/7hg=
43 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
44 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
45 | golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
46 | golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
47 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
48 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
49 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
50 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
51 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
52 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
53 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
54 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
55 |
--------------------------------------------------------------------------------
/tilemap/generator.go:
--------------------------------------------------------------------------------
1 | package tilemap
2 |
3 | import (
4 | "image"
5 | "math/rand/v2"
6 |
7 | "github.com/setanarut/kar/items"
8 |
9 | "github.com/setanarut/fastnoise"
10 | )
11 |
12 | type WorldOpts struct {
13 | Seed int
14 | SurfaceFlatness float64
15 | HighestSurfaceLevel float64
16 | LowestSurfaceLevel float64
17 | HighestGoldLevel float64
18 | LowestGoldLevel float64
19 | HighestIronLevel float64
20 | LowestIronLevel float64
21 | HighestCoalLevel float64
22 | LowestCoalLevel float64
23 | HighestDiamondLevel float64
24 | LowestDiamondLevel float64
25 | }
26 |
27 | type Generator struct {
28 | NoiseState *fastnoise.State[float64]
29 | PCG *rand.PCG
30 | Rand *rand.Rand
31 | Opts *WorldOpts
32 | Tilemap TileMap
33 | }
34 |
35 | func DefaultWorldOpts() WorldOpts {
36 | return WorldOpts{
37 | SurfaceFlatness: 0,
38 | HighestSurfaceLevel: 10,
39 | LowestSurfaceLevel: 30,
40 | HighestCoalLevel: 32,
41 | LowestCoalLevel: 100,
42 | HighestGoldLevel: 35,
43 | LowestGoldLevel: 45,
44 | HighestIronLevel: 50,
45 | LowestIronLevel: 70,
46 | HighestDiamondLevel: 100,
47 | LowestDiamondLevel: 110,
48 | }
49 | }
50 |
51 | func NewGenerator(t TileMap) Generator {
52 | g := Generator{
53 | NoiseState: fastnoise.New[float64](),
54 | Tilemap: t,
55 | }
56 | g.Opts = &WorldOpts{}
57 | *g.Opts = DefaultWorldOpts()
58 | g.PCG = rand.NewPCG(0, 1024)
59 | g.Rand = rand.New(g.PCG)
60 | g.NoiseState.Seed = 0
61 | return g
62 | }
63 |
64 | func (g *Generator) Generate() {
65 | g.StoneLayer() // Base stone layer
66 | g.Surface() // Fill surface (tree/dirt)
67 | g.CoalOreLayer(20)
68 | g.GoldOreLayer(20)
69 | g.IronOreLayer(150)
70 | g.DiamondOreLayer(20)
71 | g.FillBedrockLayer()
72 | }
73 |
74 | func (g *Generator) FillBedrockLayer() {
75 | for x := range g.Tilemap.W {
76 | g.Tilemap.Set(x, g.Tilemap.H-1, items.Bedrock)
77 | }
78 | }
79 | func (g *Generator) DiamondOreLayer(n int) {
80 | rect := image.Rect(0, int(g.Opts.HighestDiamondLevel), g.Tilemap.W, int(g.Opts.LowestDiamondLevel))
81 | for range n {
82 | x, y := g.RandomPointInRect(rect)
83 | if g.Rand.Float64() < 0.5 {
84 | g.Tilemap.Set(x, y, items.DiamondOre)
85 | }
86 | if g.Rand.Float64() < 0.5 {
87 | g.Tilemap.Set(x, y-1, items.DiamondOre)
88 | }
89 | if g.Rand.Float64() < 0.5 {
90 | g.Tilemap.Set(x-1, y, items.DiamondOre)
91 | }
92 | if g.Rand.Float64() < 0.5 {
93 | g.Tilemap.Set(x-1, y-1, items.DiamondOre)
94 | }
95 |
96 | }
97 | }
98 | func (g *Generator) CoalOreLayer(n int) {
99 | rect := image.Rect(0, int(g.Opts.HighestCoalLevel), g.Tilemap.W, int(g.Opts.LowestCoalLevel))
100 | for range n {
101 | x, y := g.RandomPointInRect(rect)
102 | if g.Rand.Float64() < 0.5 {
103 | g.Tilemap.Set(x, y, items.CoalOre)
104 | }
105 | if g.Rand.Float64() < 0.5 {
106 | g.Tilemap.Set(x, y-1, items.CoalOre)
107 | }
108 | if g.Rand.Float64() < 0.5 {
109 | g.Tilemap.Set(x-1, y, items.CoalOre)
110 | }
111 | if g.Rand.Float64() < 0.5 {
112 | g.Tilemap.Set(x-1, y-1, items.CoalOre)
113 | }
114 |
115 | }
116 | }
117 | func (g *Generator) GoldOreLayer(n int) {
118 | rect := image.Rect(0, int(g.Opts.HighestGoldLevel), g.Tilemap.W, int(g.Opts.LowestGoldLevel))
119 | for range n {
120 | x, y := g.RandomPointInRect(rect)
121 | if g.Rand.Float64() < 0.5 {
122 | g.Tilemap.Set(x, y, items.GoldOre)
123 | }
124 | if g.Rand.Float64() < 0.5 {
125 | g.Tilemap.Set(x, y-1, items.GoldOre)
126 | }
127 | if g.Rand.Float64() < 0.5 {
128 | g.Tilemap.Set(x-1, y, items.GoldOre)
129 | }
130 | if g.Rand.Float64() < 0.5 {
131 | g.Tilemap.Set(x-1, y-1, items.GoldOre)
132 | }
133 |
134 | }
135 | }
136 | func (g *Generator) IronOreLayer(n int) {
137 | rect := image.Rect(0, int(g.Opts.HighestIronLevel), g.Tilemap.W, int(g.Opts.LowestIronLevel))
138 | for range n {
139 | x, y := g.RandomPointInRect(rect)
140 | if g.Rand.Float64() < 0.5 {
141 | g.Tilemap.Set(x, y, items.IronOre)
142 | }
143 | if g.Rand.Float64() < 0.5 {
144 | g.Tilemap.Set(x, y-1, items.IronOre)
145 | }
146 | if g.Rand.Float64() < 0.5 {
147 | g.Tilemap.Set(x-1, y, items.IronOre)
148 | }
149 | if g.Rand.Float64() < 0.5 {
150 | g.Tilemap.Set(x-1, y-1, items.IronOre)
151 | }
152 |
153 | }
154 | }
155 | func (g *Generator) StoneLayer() {
156 | for y := range g.Tilemap.H {
157 | for x := range g.Tilemap.W {
158 | val := mapRange(g.NoiseState.Noise2D(x, 0), -1, 1, g.Opts.LowestSurfaceLevel, g.Opts.HighestSurfaceLevel)
159 | if y > int(val) {
160 | g.Tilemap.Grid[y][x] = items.Stone
161 | } else {
162 | g.Tilemap.Grid[y][x] = items.Air
163 | }
164 | }
165 | }
166 | }
167 |
168 | func (g *Generator) Surface() {
169 | // surface bounds
170 | for y := int(g.Opts.HighestSurfaceLevel); y <= int(g.Opts.LowestSurfaceLevel); y++ {
171 | for x := range g.Tilemap.W {
172 | upperBlockID := g.Tilemap.GetID(x, y-1)
173 | currentBlockID := g.Tilemap.GetID(x, y)
174 | if upperBlockID == items.Air && currentBlockID == items.Stone {
175 | g.Tilemap.Set(x, y, items.GrassBlock)
176 |
177 | if g.Rand.Float64() < 0.15 {
178 | if g.Tilemap.GetID(x-1, y-1) != items.OakLog && g.Tilemap.GetID(x+1, y-1) != items.OakLog {
179 | g.makeTree(x, y-1)
180 | }
181 | }
182 |
183 | g.Tilemap.Set(x, y+1, items.Dirt)
184 | g.Tilemap.Set(x, y+2, items.Dirt)
185 | if g.Rand.Float64() < 0.9 {
186 | g.Tilemap.Set(x, y+3, items.Dirt)
187 | }
188 | if g.Rand.Float64() < 0.7 {
189 | g.Tilemap.Set(x, y+4, items.Dirt)
190 | }
191 | if g.Rand.Float64() < 0.5 {
192 | g.Tilemap.Set(x, y+5, items.Dirt)
193 | }
194 |
195 | if g.Rand.Float64() < 0.3 {
196 | g.Tilemap.Set(x, y+6, items.Dirt)
197 | }
198 | if g.Rand.Float64() < 0.1 {
199 | g.Tilemap.Set(x, y+7, items.Dirt)
200 | }
201 | }
202 | }
203 | }
204 |
205 | }
206 |
207 | func (g *Generator) makeTree(x, y int) {
208 |
209 | if g.Rand.Float64() < 0.8 {
210 | g.Tilemap.Set(x, y-7, items.OakLeaves)
211 | g.Tilemap.Set(x+1, y-6, items.OakLeaves)
212 | g.Tilemap.Set(x, y-6, items.OakLeaves)
213 | g.Tilemap.Set(x-1, y-6, items.OakLeaves)
214 | g.Tilemap.Set(x+1, y-5, items.OakLeaves)
215 | g.Tilemap.Set(x, y-5, items.OakLeaves)
216 | g.Tilemap.Set(x-1, y-5, items.OakLeaves)
217 | g.Tilemap.Set(x+1, y-4, items.OakLeaves)
218 | g.Tilemap.Set(x, y-4, items.OakLeaves)
219 | g.Tilemap.Set(x-1, y-4, items.OakLeaves)
220 | g.Tilemap.Set(x, y-3, items.OakLog)
221 | g.Tilemap.Set(x, y-2, items.OakLog)
222 | g.Tilemap.Set(x, y-1, items.OakLog)
223 | g.Tilemap.Set(x, y, items.OakLog)
224 | } else {
225 | g.Tilemap.Set(x, y-3, items.OakLeaves)
226 | g.Tilemap.Set(x-1, y-2, items.OakLeaves)
227 | g.Tilemap.Set(x+1, y-2, items.OakLeaves)
228 | g.Tilemap.Set(x, y-2, items.OakLeaves)
229 | g.Tilemap.Set(x, y-1, items.OakLog)
230 | g.Tilemap.Set(x, y, items.OakLog)
231 |
232 | }
233 |
234 | }
235 |
236 | func mapRange(v, a, b, c, d float64) float64 {
237 | return (v-a)/(b-a)*(d-c) + c
238 | }
239 |
240 | func (g *Generator) SetSeed(seed int) {
241 | g.PCG.Seed(uint64(seed), 1024)
242 | g.NoiseState.Seed = seed
243 | }
244 |
245 | func (g *Generator) RandomPointInRect(rect image.Rectangle) (int, int) {
246 | return rect.Min.X + g.Rand.IntN(rect.Dx()), rect.Min.Y + g.Rand.IntN(rect.Dy())
247 | }
248 |
--------------------------------------------------------------------------------
/items/property.go:
--------------------------------------------------------------------------------
1 | package items
2 |
3 | type tag uint
4 |
5 | // Item Bitmask tag
6 | const (
7 | None tag = 0
8 | Block tag = 1 << iota
9 | OreBlock
10 | UnbreakableBlock
11 | NonSolidBlock
12 | Harvestable
13 | DropItem
14 | Item
15 | RawOre
16 | Tool
17 | Weapon
18 | Food
19 | MaterialWooden
20 | MaterialGold
21 | MaterialStone
22 | MaterialIron
23 | MaterialDiamond
24 | ToolHand
25 | ToolAxe
26 | ToolPickaxe
27 | ToolShovel
28 | ToolBucket
29 | )
30 | const All = tag(^uint(0))
31 |
32 | type ItemProperty struct {
33 | DisplayName string
34 | DropID uint8
35 | MaxStackSize uint8
36 | Tags tag
37 | BestToolTag tag
38 | }
39 |
40 | var Property = map[uint8]ItemProperty{
41 | Air: {
42 | DisplayName: "Air",
43 | DropID: Air,
44 | MaxStackSize: 1,
45 | Tags: None | NonSolidBlock | UnbreakableBlock,
46 | },
47 | Bedrock: {
48 | DisplayName: "Bedrock",
49 | DropID: Bedrock,
50 | MaxStackSize: 1,
51 | Tags: Block | UnbreakableBlock,
52 | },
53 | Random: {
54 | DisplayName: "Random",
55 | MaxStackSize: 64,
56 | Tags: Block | UnbreakableBlock,
57 | },
58 | Bread: {
59 | DisplayName: "Bread",
60 | MaxStackSize: 64,
61 | Tags: Item | Food,
62 | },
63 | Bucket: {
64 | DisplayName: "Bucket",
65 | MaxStackSize: 1,
66 | Tags: Item | Tool,
67 | },
68 | Coal: {
69 | DisplayName: "Coal",
70 | MaxStackSize: 64,
71 | Tags: Item | DropItem | RawOre,
72 | },
73 | CoalOre: {
74 | DisplayName: "Coal Ore",
75 | DropID: Coal,
76 | MaxStackSize: 64,
77 | Tags: Block | OreBlock,
78 | BestToolTag: ToolPickaxe,
79 | },
80 | CraftingTable: {
81 | DisplayName: "Crafting Table",
82 | DropID: CraftingTable,
83 | MaxStackSize: 1,
84 | Tags: Block,
85 | BestToolTag: ToolAxe,
86 | },
87 | Diamond: {
88 | DisplayName: "Diamond",
89 | MaxStackSize: 64,
90 | Tags: Item | DropItem | RawOre,
91 | },
92 | DiamondAxe: {
93 | DisplayName: "Diamond Axe",
94 | MaxStackSize: 1,
95 | Tags: Item | Tool | ToolAxe | Weapon | MaterialDiamond,
96 | },
97 | DiamondOre: {
98 | DisplayName: "Diamond Ore",
99 | DropID: Diamond,
100 | MaxStackSize: 64,
101 | Tags: Block | OreBlock,
102 | BestToolTag: ToolPickaxe,
103 | },
104 | DiamondPickaxe: {
105 | DisplayName: "Diamond Pickaxe",
106 | MaxStackSize: 1,
107 | Tags: Item | Tool | ToolPickaxe | MaterialDiamond,
108 | },
109 | DiamondShovel: {
110 | DisplayName: "Diamond Shovel",
111 | MaxStackSize: 1,
112 | Tags: Item | Tool | ToolShovel | MaterialDiamond,
113 | },
114 | Dirt: {
115 | DisplayName: "Dirt",
116 | DropID: Dirt,
117 | MaxStackSize: 64,
118 | Tags: Block,
119 | BestToolTag: ToolShovel,
120 | },
121 | Furnace: {
122 | DisplayName: "Furnace",
123 | DropID: Furnace,
124 | MaxStackSize: 1,
125 | Tags: Block,
126 | BestToolTag: ToolPickaxe,
127 | },
128 | GoldIngot: {
129 | DisplayName: "Gold Ingot",
130 | MaxStackSize: 64,
131 | Tags: Item,
132 | },
133 | GoldOre: {
134 | DisplayName: "Gold Ore",
135 | DropID: RawGold,
136 | MaxStackSize: 64,
137 | Tags: Block | OreBlock,
138 | BestToolTag: ToolPickaxe,
139 | },
140 | GrassBlock: {
141 | DisplayName: "Grass Block",
142 | DropID: Dirt,
143 | MaxStackSize: 64,
144 | Tags: Block,
145 | BestToolTag: ToolShovel,
146 | },
147 | GrassBlockSnow: {
148 | DisplayName: "Grass Block Snow",
149 | DropID: Dirt,
150 | MaxStackSize: 64,
151 | Tags: Block,
152 | BestToolTag: ToolShovel,
153 | },
154 | IronAxe: {
155 | DisplayName: "Iron Axe",
156 | MaxStackSize: 1,
157 | Tags: Item | Tool | ToolAxe | MaterialIron,
158 | },
159 | IronIngot: {
160 | DisplayName: "Iron Ingot",
161 | MaxStackSize: 64,
162 | Tags: Item,
163 | },
164 | IronOre: {
165 | DisplayName: "Iron Ore",
166 | DropID: RawIron,
167 | MaxStackSize: 64,
168 | Tags: Block | OreBlock,
169 | BestToolTag: ToolPickaxe,
170 | },
171 | IronPickaxe: {
172 | DisplayName: "Iron Pickaxe",
173 | MaxStackSize: 1,
174 | Tags: Item | Tool | ToolPickaxe | MaterialIron,
175 | },
176 | IronShovel: {
177 | DisplayName: "Iron Shovel",
178 | MaxStackSize: 1,
179 | Tags: Item | Tool | ToolShovel | MaterialIron,
180 | },
181 | OakLeaves: {
182 | DisplayName: "Oak Leaves",
183 | DropID: OakSapling,
184 | MaxStackSize: 64,
185 | Tags: Block,
186 | BestToolTag: All,
187 | },
188 | OakLog: {
189 | DisplayName: "Oak Log",
190 | DropID: OakLog,
191 | MaxStackSize: 64,
192 | Tags: Block,
193 | BestToolTag: ToolAxe,
194 | },
195 | OakPlanks: {
196 | DisplayName: "Oak Planks",
197 | DropID: OakPlanks,
198 | MaxStackSize: 64,
199 | Tags: Block,
200 | BestToolTag: ToolAxe,
201 | },
202 | OakSapling: {
203 | DisplayName: "Oak Sapling",
204 | DropID: OakSapling,
205 | MaxStackSize: 64,
206 | Tags: Block | Item | NonSolidBlock,
207 | BestToolTag: ToolAxe,
208 | },
209 | Obsidian: {
210 | DisplayName: "Obsidian",
211 | DropID: Obsidian,
212 | MaxStackSize: 64,
213 | Tags: Block,
214 | BestToolTag: ToolPickaxe,
215 | },
216 | RawGold: {
217 | DisplayName: "Raw Gold",
218 | MaxStackSize: 64,
219 | Tags: Item | DropItem | RawOre,
220 | },
221 | RawIron: {
222 | DisplayName: "Raw Iron",
223 | MaxStackSize: 64,
224 | Tags: Item | DropItem | RawOre,
225 | },
226 | Sand: {
227 | DisplayName: "Sand",
228 | DropID: Sand,
229 | MaxStackSize: 64,
230 | Tags: Block,
231 | BestToolTag: ToolShovel,
232 | },
233 | SmoothStone: {
234 | DisplayName: "Smooth Stone",
235 | DropID: SmoothStone,
236 | MaxStackSize: 64,
237 | Tags: Block,
238 | },
239 | Snow: {
240 | DisplayName: "Snow",
241 | DropID: Dirt,
242 | MaxStackSize: 64,
243 | Tags: Block,
244 | BestToolTag: ToolShovel,
245 | },
246 | Snowball: {
247 | DisplayName: "Snowball",
248 | DropID: Snowball,
249 | MaxStackSize: 64,
250 | Tags: Item,
251 | },
252 | Stick: {
253 | DisplayName: "Stick",
254 | MaxStackSize: 64,
255 | Tags: Item,
256 | },
257 | Stone: {
258 | DisplayName: "Stone",
259 | DropID: Stone,
260 | MaxStackSize: 64,
261 | Tags: Block,
262 | BestToolTag: ToolPickaxe,
263 | },
264 | StoneAxe: {
265 | DisplayName: "Stone Axe",
266 | MaxStackSize: 1,
267 | Tags: Item | Tool | ToolAxe | MaterialStone,
268 | },
269 | StoneBricks: {
270 | DisplayName: "Stone Bricks",
271 | DropID: StoneBricks,
272 | MaxStackSize: 64,
273 | Tags: Block,
274 | BestToolTag: ToolPickaxe,
275 | },
276 | StonePickaxe: {
277 | DisplayName: "Stone Pickaxe",
278 | MaxStackSize: 1,
279 | Tags: Item | Tool | ToolPickaxe | MaterialStone,
280 | },
281 | StoneShovel: {
282 | DisplayName: "Stone Shovel",
283 | MaxStackSize: 1,
284 | Tags: Item | Tool | ToolShovel | MaterialStone,
285 | },
286 | Tnt: {
287 | DisplayName: "TNT",
288 | DropID: Tnt,
289 | MaxStackSize: 64,
290 | Tags: Block,
291 | BestToolTag: All,
292 | },
293 | Torch: {
294 | DisplayName: "Torch",
295 | DropID: Torch,
296 | MaxStackSize: 64,
297 | Tags: Item | Block | NonSolidBlock,
298 | BestToolTag: All,
299 | },
300 | WaterBucket: {
301 | DisplayName: "Water Bucket",
302 | MaxStackSize: 1,
303 | Tags: Item | Tool | ToolBucket | MaterialIron,
304 | },
305 | WoodenAxe: {
306 | DisplayName: "Wooden Axe",
307 | MaxStackSize: 1,
308 | Tags: Item | Tool | ToolAxe | MaterialWooden,
309 | },
310 | WoodenPickaxe: {
311 | DisplayName: "Wooden Pickaxe",
312 | MaxStackSize: 1,
313 | Tags: Item | Tool | ToolPickaxe | MaterialWooden,
314 | },
315 | WoodenShovel: {
316 | DisplayName: "Wooden Shovel",
317 | MaxStackSize: 1,
318 | Tags: Item | Tool | ToolShovel | MaterialWooden,
319 | },
320 | }
321 |
--------------------------------------------------------------------------------
/res/resources.go:
--------------------------------------------------------------------------------
1 | // res is resources
2 | package res
3 |
4 | import (
5 | "bytes"
6 | "embed"
7 | "image"
8 | "log"
9 |
10 | "github.com/setanarut/kar/items"
11 |
12 | "github.com/anthonynsimon/bild/blend"
13 | "github.com/hajimehoshi/ebiten/v2"
14 | "github.com/hajimehoshi/ebiten/v2/text/v2"
15 | "github.com/setanarut/anim"
16 | "golang.org/x/text/language"
17 | )
18 |
19 | //go:embed assets/*
20 | var fs embed.FS
21 |
22 | var (
23 | Crab = anim.SubImages(ReadEbImgFS(fs, "assets/img/crab.png"), 0, 0, 16, 9, 2, false)
24 | Icon8 = make(map[uint8]*ebiten.Image, 0)
25 | BlockCrackFrames = make(map[uint8][]*ebiten.Image, 0)
26 | BlockUnbreakable = make(map[uint8]*ebiten.Image, 0)
27 | HotbarEdge = ReadEbImgFS(fs, "assets/img/gui/hotbar_edge.png")
28 | HotbarMid = ReadEbImgFS(fs, "assets/img/gui/hotbar_mid.png")
29 | CraftingTable1x2 = ReadEbImgFS(fs, "assets/img/gui/table1x2.png")
30 | CraftingTable3x3 = ReadEbImgFS(fs, "assets/img/gui/table3x3.png")
31 | CraftingTable2x2 = ReadEbImgFS(fs, "assets/img/gui/table2x2.png")
32 | BlockBorder = ReadEbImgFS(fs, "assets/img/gui/block_border.png")
33 | SlotBorder = ReadEbImgFS(fs, "assets/img/gui/slot_border.png")
34 | cracks = ImgFromFS(fs, "assets/img/cracks.png")
35 | Font = LoadFontFromFS("assets/font/arkpixel10.ttf", 10, fs)
36 | )
37 |
38 | var (
39 | Player = ReadEbImgFS(fs, "assets/img/player/player.png")
40 | PlayerWoodenAxeAtlas = ReadEbImgFS(fs, "assets/img/player/player_wood_axe.png")
41 | PlayerStoneAxeAtlas = ReadEbImgFS(fs, "assets/img/player/player_stone_axe.png")
42 | PlayerIronAxeAtlas = ReadEbImgFS(fs, "assets/img/player/player_iron_axe.png")
43 | PlayerDiamondAxeAtlas = ReadEbImgFS(fs, "assets/img/player/player_diamond_axe.png")
44 | PlayerWoodenPickaxeAtlas = ReadEbImgFS(fs, "assets/img/player/player_wood_pickaxe.png")
45 | PlayerStonePickaxeAtlas = ReadEbImgFS(fs, "assets/img/player/player_stone_pickaxe.png")
46 | PlayerIronPickaxeAtlas = ReadEbImgFS(fs, "assets/img/player/player_iron_pickaxe.png")
47 | PlayerDiamondPickaxeAtlas = ReadEbImgFS(fs, "assets/img/player/player_diamond_pickaxe.png")
48 | PlayerWoodenShovelAtlas = ReadEbImgFS(fs, "assets/img/player/player_wood_shovel.png")
49 | PlayerStoneShovelAtlas = ReadEbImgFS(fs, "assets/img/player/player_stone_shovel.png")
50 | PlayerIronShovelAtlas = ReadEbImgFS(fs, "assets/img/player/player_iron_shovel.png")
51 | PlayerDiamondShovelAtlas = ReadEbImgFS(fs, "assets/img/player/player_diamond_shovel.png")
52 | )
53 |
54 | func init() {
55 |
56 | // Unbreakable blocks
57 | BlockUnbreakable[items.Bedrock] = ReadEbImgFS(fs, "assets/img/blocks/bedrock.png")
58 | BlockUnbreakable[items.Random] = ReadEbImgFS(fs, "assets/img/blocks/random.png")
59 |
60 | // Breakable blocks
61 | BlockCrackFrames[items.CoalOre] = blockImgs("coal_ore.png")
62 | BlockCrackFrames[items.CraftingTable] = blockImgs("crafting_table.png")
63 | BlockCrackFrames[items.DiamondOre] = blockImgs("diamond_ore.png")
64 | BlockCrackFrames[items.Dirt] = blockImgs("dirt.png")
65 | BlockCrackFrames[items.Furnace] = blockImgs("furnace.png")
66 | BlockCrackFrames[items.GoldOre] = blockImgs("gold_ore.png")
67 | BlockCrackFrames[items.GrassBlock] = blockImgs("grass_block.png")
68 | BlockCrackFrames[items.GrassBlockSnow] = blockImgs("grass_block_snow.png")
69 | BlockCrackFrames[items.IronOre] = blockImgs("iron_ore.png")
70 | BlockCrackFrames[items.OakLeaves] = blockImgs("oak_leaves.png")
71 | BlockCrackFrames[items.OakLog] = blockImgs("oak_log.png")
72 | BlockCrackFrames[items.OakPlanks] = blockImgs("oak_planks.png")
73 | BlockCrackFrames[items.OakSapling] = blockImgs("oak_sapling.png")
74 | BlockCrackFrames[items.Obsidian] = blockImgs("obsidian.png")
75 | BlockCrackFrames[items.Sand] = blockImgs("sand.png")
76 | BlockCrackFrames[items.SmoothStone] = blockImgs("smooth_stone.png")
77 | BlockCrackFrames[items.Snow] = blockImgs("snow.png")
78 | BlockCrackFrames[items.Stone] = blockImgs("stone.png")
79 | BlockCrackFrames[items.StoneBricks] = blockImgs("stone_bricks.png")
80 | BlockCrackFrames[items.Tnt] = blockImgs("tnt.png")
81 | BlockCrackFrames[items.Torch] = blockImgs("torch.png")
82 |
83 | // Block icons
84 | Icon8[items.Bedrock] = blockIconImg("bedrock.png")
85 | Icon8[items.Random] = blockIconImg("random.png")
86 | Icon8[items.CoalOre] = blockIconImg("coal_ore.png")
87 | Icon8[items.CraftingTable] = blockIconImg("crafting_table.png")
88 | Icon8[items.DiamondOre] = blockIconImg("diamond_ore.png")
89 | Icon8[items.Dirt] = blockIconImg("dirt.png")
90 | Icon8[items.Furnace] = blockIconImg("furnace.png")
91 | Icon8[items.GoldOre] = blockIconImg("gold_ore.png")
92 | Icon8[items.GrassBlock] = blockIconImg("grass_block.png")
93 | Icon8[items.GrassBlockSnow] = blockIconImg("grass_block_snow.png")
94 | Icon8[items.IronOre] = blockIconImg("iron_ore.png")
95 | Icon8[items.OakLeaves] = blockIconImg("oak_leaves.png")
96 | Icon8[items.OakLog] = blockIconImg("oak_log.png")
97 | Icon8[items.OakPlanks] = blockIconImg("oak_planks.png")
98 | Icon8[items.OakSapling] = blockIconImg("oak_sapling.png")
99 | Icon8[items.Obsidian] = blockIconImg("obsidian.png")
100 | Icon8[items.Sand] = blockIconImg("sand.png")
101 | Icon8[items.SmoothStone] = blockIconImg("smooth_stone.png")
102 | Icon8[items.Snow] = blockIconImg("snow.png")
103 | Icon8[items.Stone] = blockIconImg("stone.png")
104 | Icon8[items.StoneBricks] = blockIconImg("stone_bricks.png")
105 | Icon8[items.Tnt] = blockIconImg("tnt.png")
106 | Icon8[items.Torch] = blockIconImg("torch.png")
107 |
108 | // Item icons
109 | Icon8[items.Bread] = itemIconImg("bread.png")
110 | Icon8[items.Bucket] = itemIconImg("bucket.png")
111 | Icon8[items.Coal] = itemIconImg("coal.png")
112 | Icon8[items.Diamond] = itemIconImg("diamond.png")
113 | Icon8[items.DiamondAxe] = itemIconImg("diamond_axe.png")
114 | Icon8[items.DiamondPickaxe] = itemIconImg("diamond_pickaxe.png")
115 | Icon8[items.DiamondShovel] = itemIconImg("diamond_shovel.png")
116 | Icon8[items.GoldIngot] = itemIconImg("gold_ingot.png")
117 | Icon8[items.IronAxe] = itemIconImg("iron_axe.png")
118 | Icon8[items.IronIngot] = itemIconImg("iron_ingot.png")
119 | Icon8[items.IronPickaxe] = itemIconImg("iron_pickaxe.png")
120 | Icon8[items.IronShovel] = itemIconImg("iron_shovel.png")
121 | Icon8[items.RawGold] = itemIconImg("raw_gold.png")
122 | Icon8[items.RawIron] = itemIconImg("raw_iron.png")
123 | Icon8[items.Snowball] = itemIconImg("snowball.png")
124 | Icon8[items.Stick] = itemIconImg("stick.png")
125 | Icon8[items.StoneAxe] = itemIconImg("stone_axe.png")
126 | Icon8[items.StonePickaxe] = itemIconImg("stone_pickaxe.png")
127 | Icon8[items.StoneShovel] = itemIconImg("stone_shovel.png")
128 | Icon8[items.WaterBucket] = itemIconImg("water_bucket.png")
129 | Icon8[items.WoodenAxe] = itemIconImg("wooden_axe.png")
130 | Icon8[items.WoodenPickaxe] = itemIconImg("wooden_pickaxe.png")
131 | Icon8[items.WoodenShovel] = itemIconImg("wooden_shovel.png")
132 | }
133 |
134 | func toEbiten(st []image.Image) []*ebiten.Image {
135 | l := make([]*ebiten.Image, 0)
136 | for _, v := range st {
137 | l = append(l, ebiten.NewImageFromImage(v))
138 | }
139 | return l
140 | }
141 |
142 | func makeStages(block, stages image.Image) []image.Image {
143 | frames := make([]image.Image, 0)
144 | frames = append(frames, block)
145 | for i := range 4 {
146 | x := i * 20
147 | rec := image.Rect(x, 0, x+20, x+20)
148 | si := stages.(*image.NRGBA).SubImage(rec)
149 | frames = append(frames, blend.Normal(block, si))
150 | }
151 | return frames
152 | }
153 | func blockImgs(f string) []*ebiten.Image {
154 | frames := makeStages(ImgFromFS(fs, "assets/img/blocks/"+f), cracks)
155 | return toEbiten(frames)
156 | }
157 | func itemIconImg(f string) *ebiten.Image {
158 | return ReadEbImgFS(fs, "assets/img/items_icon/"+f)
159 | }
160 | func blockIconImg(f string) *ebiten.Image {
161 | return ReadEbImgFS(fs, "assets/img/blocks_icon/"+f)
162 | }
163 |
164 | func ReadEbImgFS(fs embed.FS, filePath string) *ebiten.Image {
165 | return ebiten.NewImageFromImage(ImgFromFS(fs, filePath))
166 | }
167 |
168 | func ImgFromFS(fs embed.FS, filePath string) image.Image {
169 | f, err := fs.Open(filePath)
170 | if err != nil {
171 | panic(err)
172 | }
173 | defer f.Close()
174 | image, _, err := image.Decode(f)
175 | if err != nil {
176 | panic(err)
177 | }
178 | return image
179 | }
180 |
181 | func LoadFontFromFS(file string, size float64, fs embed.FS) *text.GoTextFace {
182 | f, err := fs.ReadFile(file)
183 | if err != nil {
184 | log.Fatal(err)
185 | }
186 | src, err := text.NewGoTextFaceSource(bytes.NewReader(f))
187 | if err != nil {
188 | log.Fatal(err)
189 | }
190 | gtf := &text.GoTextFace{
191 | Source: src,
192 | Size: size,
193 | Language: language.English,
194 | }
195 | return gtf
196 | }
197 |
--------------------------------------------------------------------------------
/collision.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "image"
5 | "math"
6 |
7 | "github.com/setanarut/kar/items"
8 |
9 | "github.com/setanarut/v"
10 | )
11 |
12 | const EPSILON = 1e-8
13 |
14 | // const EPSILON float64 = 0.0
15 |
16 | type AABB struct {
17 | Pos Vec
18 | Half Vec
19 | }
20 |
21 | func (a AABB) Left() float64 { return a.Pos.X - a.Half.X }
22 | func (a AABB) Right() float64 { return a.Pos.X + a.Half.X }
23 | func (a AABB) Top() float64 { return a.Pos.Y - a.Half.Y }
24 | func (a AABB) Bottom() float64 { return a.Pos.Y + a.Half.Y }
25 | func (a AABB) TopLeft() Vec { return Vec{a.Pos.X - a.Half.X, a.Pos.Y - a.Half.Y} }
26 | func (a AABB) Size() Vec { return Vec{a.Half.X * 2, a.Half.Y * 2} }
27 |
28 | func (a *AABB) SetLeft(l float64) { a.Pos.X = l + a.Half.X }
29 | func (a *AABB) SetRight(r float64) { a.Pos.X = r - a.Half.X }
30 | func (a *AABB) SetTop(t float64) { a.Pos.Y = t + a.Half.Y }
31 | func (a *AABB) SetBottom(b float64) { a.Pos.Y = b - a.Half.Y }
32 |
33 | type HitInfo struct {
34 | Pos Vec
35 | Delta Vec
36 | Normal Vec
37 | Time float64
38 | }
39 |
40 | func (h *HitInfo) Reset() {
41 | *h = HitInfo{}
42 | }
43 |
44 | type HitInfo2 struct {
45 | Right, Bottom, Left, Top bool
46 | Delta Vec
47 | }
48 |
49 | func (h *HitInfo2) Reset() {
50 | *h = HitInfo2{}
51 | }
52 |
53 | // AABBPlatform moving platform b collision
54 | func AABBPlatform(a, b *AABB, aVel, bVel *Vec, h *HitInfo2) bool {
55 | // Calculate old positions using velocities
56 | aOldPos := Vec{a.Pos.X - aVel.X, a.Pos.Y - aVel.Y}
57 | bOldPos := Vec{b.Pos.X - bVel.X, b.Pos.Y - bVel.Y}
58 |
59 | // Check collision at current positions using half dimensions
60 | xDist := math.Abs(a.Pos.X - b.Pos.X)
61 | yDist := math.Abs(a.Pos.Y - b.Pos.Y)
62 |
63 | // Combined half widths and heights
64 | combinedHalfW := a.Half.X + b.Half.X
65 | combinedHalfH := a.Half.Y + b.Half.Y
66 |
67 | // Early exit check
68 | if xDist > combinedHalfW || yDist > combinedHalfH {
69 | return false
70 | }
71 |
72 | // Calculate old distances using calculated old positions
73 | oldXDist := math.Abs(aOldPos.X - bOldPos.X)
74 | oldYDist := math.Abs(aOldPos.Y - bOldPos.Y)
75 |
76 | // Check collision direction and calculate position delta
77 | if yDist < combinedHalfH {
78 | if a.Pos.Y > b.Pos.Y && oldYDist >= combinedHalfH {
79 | h.Delta.Y = (b.Pos.Y + combinedHalfH + EPSILON) - a.Pos.Y
80 | h.Top = true
81 | } else if a.Pos.Y < b.Pos.Y && oldYDist >= combinedHalfH {
82 | h.Delta.Y = (b.Pos.Y - combinedHalfH - EPSILON) - a.Pos.Y
83 | h.Bottom = true
84 | }
85 | }
86 |
87 | if xDist < combinedHalfW {
88 | if a.Pos.X > b.Pos.X && oldXDist >= combinedHalfW {
89 | h.Delta.X = (b.Pos.X + combinedHalfW + EPSILON) - a.Pos.X
90 | h.Left = true
91 | } else if a.Pos.X < b.Pos.X && oldXDist >= combinedHalfW {
92 | h.Delta.X = (b.Pos.X - combinedHalfW - EPSILON) - a.Pos.X
93 | h.Right = true
94 | }
95 | }
96 |
97 | return true
98 | }
99 |
100 | func Segment(a *AABB, pos, delta, padding Vec, hit *HitInfo) bool {
101 | scale := v.One.Div(delta)
102 | signX := math.Copysign(1, scale.X)
103 | signY := math.Copysign(1, scale.Y)
104 | nearTimeX := (a.Pos.X - signX*(a.Half.X+padding.X) - pos.X) * scale.X
105 | nearTimeY := (a.Pos.Y - signY*(a.Half.Y+padding.Y) - pos.Y) * scale.Y
106 | farTimeX := (a.Pos.X + signX*(a.Half.X+padding.X) - pos.X) * scale.X
107 | farTimeY := (a.Pos.Y + signY*(a.Half.Y+padding.Y) - pos.Y) * scale.Y
108 | if math.IsNaN(nearTimeY) {
109 | nearTimeY = math.Inf(1)
110 | }
111 | if math.IsNaN(farTimeY) {
112 | farTimeY = math.Inf(1)
113 | }
114 | if nearTimeX > farTimeY || nearTimeY > farTimeX {
115 | return false
116 | }
117 | nearTime := max(nearTimeX, nearTimeY)
118 | farTime := min(farTimeX, farTimeY)
119 | if nearTime >= 1 || farTime <= 0 {
120 | return false
121 | }
122 | if hit == nil {
123 | return true
124 | }
125 | hit.Time = max(0, min(1, nearTime))
126 |
127 | if nearTimeX > nearTimeY {
128 | hit.Normal.X = -signX
129 | hit.Normal.Y = 0
130 | } else {
131 | hit.Normal.X = 0
132 | hit.Normal.Y = -signY
133 | }
134 | hit.Delta.X = (1.0 - hit.Time) * -delta.X
135 | hit.Delta.Y = (1.0 - hit.Time) * -delta.Y
136 |
137 | hit.Pos = pos.Add(delta.Scale(hit.Time))
138 | return true
139 | }
140 |
141 | // OverlapSweep returns hit info for b
142 | func Overlap(a, b *AABB, hit *HitInfo) bool {
143 | dx := b.Pos.X - a.Pos.X
144 | px := b.Half.X + a.Half.X - math.Abs(dx)
145 | if px <= 0 {
146 | return false
147 | }
148 |
149 | dy := b.Pos.Y - a.Pos.Y
150 | py := b.Half.Y + a.Half.Y - math.Abs(dy)
151 | if py <= 0 {
152 | return false
153 | }
154 |
155 | if hit == nil {
156 | return true
157 | }
158 |
159 | // hit reset here
160 | if px < py {
161 | sx := math.Copysign(1, dx)
162 | hit.Delta.X = px * sx
163 | hit.Normal.X = sx
164 | hit.Pos.X = a.Pos.X + a.Half.X*sx
165 | hit.Pos.Y = b.Pos.Y
166 | } else {
167 | sy := math.Copysign(1, dy)
168 | hit.Delta.Y = py * sy
169 | hit.Normal.Y = sy
170 | hit.Pos.X = b.Pos.X
171 | hit.Pos.Y = a.Pos.Y + a.Half.Y*sy
172 | }
173 | return true
174 | }
175 |
176 | // OverlapSweep returns hit info for b
177 | func OverlapSweep(staticA, b *AABB, bDelta Vec, hit *HitInfo) bool {
178 | if bDelta.IsZero() {
179 | return Overlap(staticA, b, hit)
180 | }
181 | result := Segment(staticA, b.Pos, bDelta, b.Half, hit)
182 | if result {
183 | hit.Time = max(0, min(1, hit.Time))
184 | direction := bDelta.Unit()
185 | hit.Pos.X = max(staticA.Pos.X-staticA.Half.X, min(staticA.Pos.X+staticA.Half.X, hit.Pos.X+direction.X*b.Half.X))
186 | hit.Pos.Y = max(hit.Pos.Y+direction.Y*b.Half.Y, min(staticA.Pos.Y-staticA.Half.Y, staticA.Pos.Y+staticA.Half.Y))
187 | }
188 | return result
189 | }
190 |
191 | // OverlapSweep2 returns hit info for b
192 | func OverlapSweep2(a, b *AABB, aDelta, bDelta Vec, hit *HitInfo) bool {
193 | delta := bDelta.Sub(aDelta)
194 | isCollide := OverlapSweep(a, b, delta, hit)
195 | if isCollide {
196 | hit.Pos = hit.Pos.Add(aDelta.Scale(hit.Time))
197 | if hit.Normal.X != 0 {
198 | hit.Pos.X = b.Pos.X + (bDelta.X * hit.Time) - (hit.Normal.X * b.Half.X)
199 | } else {
200 | hit.Pos.Y = b.Pos.Y + (bDelta.Y * hit.Time) - (hit.Normal.Y * b.Half.Y)
201 | }
202 | }
203 | return isCollide
204 | }
205 |
206 | // HitTileInfo stores information about a collision with a tile
207 | type HitTileInfo struct {
208 | TileCoords image.Point // X,Y coordinates of the tile in the tilemap
209 | Normal Vec // Normal vector of the collision (-1/0/1)
210 | }
211 |
212 | // Collider handles collision detection between rectangles and a 2D tilemap
213 | type Collider struct {
214 | Collisions []HitTileInfo // List of collisions from last check
215 | TileSize image.Point // Width and height of tiles
216 | TileMap [][]uint8 // 2D grid of tile IDs
217 | }
218 |
219 | // CollisionCallback is called when collisions occur, receiving collision info and final movement
220 | type CollisionCallback func(hitInfos []HitTileInfo, delta Vec)
221 |
222 | // Collide checks for collisions when moving a rectangle and returns the allowed movement
223 | func (c *Collider) Collide(rect AABB, delta Vec, onCollide CollisionCallback) Vec {
224 | c.Collisions = c.Collisions[:0]
225 |
226 | if delta.IsZero() {
227 | return delta
228 | }
229 |
230 | if math.Abs(delta.X) > math.Abs(delta.Y) {
231 | if delta.X != 0 {
232 | delta.X = c.CollideX(&rect, delta.X)
233 | }
234 | if delta.Y != 0 {
235 | rect.Pos.X += delta.X
236 | delta.Y = c.CollideY(&rect, delta.Y)
237 | }
238 | } else {
239 | if delta.Y != 0 {
240 | delta.Y = c.CollideY(&rect, delta.Y)
241 | }
242 | if delta.X != 0 {
243 | rect.Pos.Y += delta.Y
244 | delta.X = c.CollideX(&rect, delta.X)
245 | }
246 | }
247 |
248 | if onCollide != nil {
249 | onCollide(c.Collisions, delta)
250 | }
251 | return delta
252 | }
253 |
254 | // CollideX checks for collisions along the X axis and returns the allowed X movement
255 | //
256 | // reset list before -> tileCollider.Collisions = tileCollider.Collisions[:0]
257 | func (c *Collider) CollideX(rect *AABB, deltaX float64) float64 {
258 | checkLimit := max(1, int(math.Ceil(math.Abs(deltaX)/float64(c.TileSize.Y)))+1)
259 | rectTileTopCoord := int(math.Floor(rect.Top() / float64(c.TileSize.Y)))
260 | rectTileBottomCoord := int(math.Ceil(rect.Bottom()/float64(c.TileSize.Y))) - 1
261 | if deltaX > 0 {
262 | rectRight := rect.Right()
263 | startRightX := int(math.Floor(rectRight / float64(c.TileSize.X)))
264 | endX := startRightX + checkLimit
265 | endX = min(endX, len(c.TileMap[0]))
266 | for y := rectTileTopCoord; y <= rectTileBottomCoord; y++ {
267 | if y < 0 || y >= len(c.TileMap) {
268 | continue
269 | }
270 | for x := startRightX; x < endX; x++ {
271 | if x < 0 || x >= len(c.TileMap[0]) {
272 | continue
273 | }
274 | if !items.HasTag(c.TileMap[y][x], items.NonSolidBlock) {
275 | tileLeft := float64(x * c.TileSize.X)
276 | collision := tileLeft - rectRight
277 | if collision <= deltaX {
278 | deltaX = collision
279 | c.Collisions = append(c.Collisions, HitTileInfo{
280 | TileCoords: image.Point{x, y},
281 | Normal: v.Left,
282 | })
283 | }
284 | }
285 | }
286 | }
287 | }
288 | if deltaX < 0 {
289 | rectLeft := rect.Left()
290 | endX := int(math.Floor(rectLeft / float64(c.TileSize.X)))
291 | startX := endX - checkLimit
292 | startX = max(startX, 0)
293 | for y := rectTileTopCoord; y <= rectTileBottomCoord; y++ {
294 | if y < 0 || y >= len(c.TileMap) {
295 | continue
296 | }
297 | for x := startX; x <= endX; x++ {
298 | if x < 0 || x >= len(c.TileMap[0]) {
299 | continue
300 | }
301 | if !items.HasTag(c.TileMap[y][x], items.NonSolidBlock) {
302 | tileRight := float64((x + 1) * c.TileSize.X)
303 | collision := tileRight - rectLeft
304 | if collision >= deltaX {
305 | deltaX = collision
306 | c.Collisions = append(c.Collisions, HitTileInfo{
307 | TileCoords: image.Point{x, y},
308 | Normal: v.Right,
309 | })
310 | }
311 | }
312 | }
313 | }
314 | }
315 | return deltaX
316 | }
317 |
318 | // CollideY checks for collisions along the Y axis and returns the allowed Y movement
319 | //
320 | // reset list before -> tileCollider.Collisions = tileCollider.Collisions[:0]
321 | func (c *Collider) CollideY(rect *AABB, deltaY float64) float64 {
322 | checkLimit := max(1, int(math.Ceil(math.Abs(deltaY)/float64(c.TileSize.Y)))+1)
323 | rectTileLeftCoord := int(math.Floor(rect.Left() / float64(c.TileSize.X)))
324 | rectTileRightCoord := int(math.Ceil(rect.Right()/float64(c.TileSize.X))) - 1
325 | if deltaY > 0 {
326 | rectBottom := rect.Bottom()
327 | startBottomY := int(math.Floor(rectBottom / float64(c.TileSize.Y)))
328 | endY := startBottomY + checkLimit
329 | endY = min(endY, len(c.TileMap))
330 |
331 | for x := rectTileLeftCoord; x <= rectTileRightCoord; x++ {
332 | if x < 0 || x >= len(c.TileMap[0]) {
333 | continue
334 | }
335 | for y := startBottomY; y < endY; y++ {
336 | if y < 0 || y >= len(c.TileMap) {
337 | continue
338 | }
339 | if !items.HasTag(c.TileMap[y][x], items.NonSolidBlock) {
340 | tileTop := float64(y * c.TileSize.Y)
341 | collision := tileTop - rectBottom
342 | if collision <= deltaY {
343 | deltaY = collision
344 | c.Collisions = append(c.Collisions, HitTileInfo{
345 | TileCoords: image.Point{x, y},
346 | Normal: v.Up,
347 | })
348 | }
349 | }
350 | }
351 | }
352 | }
353 | if deltaY < 0 {
354 | rectTop := rect.Top()
355 | endY := int(math.Floor(rectTop / float64(c.TileSize.Y)))
356 | startY := endY - checkLimit
357 | startY = max(startY, 0)
358 | for x := rectTileLeftCoord; x <= rectTileRightCoord; x++ {
359 | if x < 0 || x >= len(c.TileMap[0]) {
360 | continue
361 | }
362 | for y := startY; y <= endY; y++ {
363 | if y < 0 || y >= len(c.TileMap) {
364 | continue
365 | }
366 | if !items.HasTag(c.TileMap[y][x], items.NonSolidBlock) {
367 | tileBottom := float64((y + 1) * c.TileSize.Y)
368 | collision := tileBottom - rectTop
369 | if collision >= deltaY {
370 | deltaY = collision
371 | c.Collisions = append(c.Collisions, HitTileInfo{
372 | TileCoords: image.Point{x, y},
373 | Normal: v.Down,
374 | })
375 | }
376 | }
377 | }
378 | }
379 | }
380 | return deltaY
381 | }
382 |
--------------------------------------------------------------------------------
/sys_ui.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "strconv"
7 |
8 | "github.com/setanarut/kar/items"
9 |
10 | "github.com/setanarut/kar/res"
11 |
12 | "github.com/hajimehoshi/ebiten/v2"
13 | "github.com/hajimehoshi/ebiten/v2/colorm"
14 | "github.com/hajimehoshi/ebiten/v2/ebitenutil"
15 | "github.com/hajimehoshi/ebiten/v2/inpututil"
16 | "github.com/hajimehoshi/ebiten/v2/text/v2"
17 | )
18 |
19 | const SpaceStr string = " "
20 |
21 | var (
22 | hotbarPos = Vec{4, 9}
23 | )
24 |
25 | type UI struct {
26 | craftingTablePos Vec
27 | hotbarW float64
28 | }
29 |
30 | func (ui *UI) Init() {
31 | ui.craftingTablePos = hotbarPos.Add(Vec{49, 39})
32 | ui.hotbarW = (17 * float64(len(inventoryRes.Slots))) + 1
33 | }
34 | func (ui *UI) Update() {
35 | ui.hotbarW = (17 * float64(len(inventoryRes.Slots))) + 1
36 |
37 | if world.Alive(currentPlayer) {
38 | // toggle crafting state
39 | if inpututil.IsKeyJustPressed(ebiten.KeyUp) {
40 |
41 | if gameDataRes.GameplayState == Playing {
42 | craftingTableRes.Pos = image.Point{}
43 | targetBlockID := tileMapRes.GetID(gameDataRes.TargetBlockCoord.X, gameDataRes.TargetBlockCoord.Y)
44 | switch targetBlockID {
45 | case items.CraftingTable:
46 | gameDataRes.GameplayState = CraftingTable3x3
47 | case items.Furnace:
48 | gameDataRes.GameplayState = Furnace1x2
49 | default:
50 | gameDataRes.GameplayState = Crafting2x2
51 | }
52 | } else {
53 | // clear crafting table when exit
54 | for y := range 3 {
55 | for x := range 3 {
56 | itemID := craftingTableRes.Slots[y][x].ID
57 | if itemID != 0 {
58 | quantity := craftingTableRes.Slots[y][x].Quantity
59 | for range quantity {
60 | durability := craftingTableRes.Slots[y][x].Durability
61 | // move items from crafting table to hotbar if possible
62 | if inventoryRes.AddItemIfEmpty(craftingTableRes.Slots[y][x].ID, durability) {
63 | craftingTableRes.RemoveItem(x, y)
64 | } else {
65 | // move items from crafting table to world if hotbar is full
66 | craftingTableRes.RemoveItem(x, y)
67 | SpawnItem(
68 | mapAABB.GetUnchecked(currentPlayer).Pos,
69 | itemID,
70 | craftingTableRes.Slots[y][x].Durability,
71 | )
72 | }
73 | }
74 | }
75 | }
76 | }
77 | craftingTableRes.ResultSlot = items.Slot{}
78 | gameDataRes.GameplayState = Playing
79 | }
80 |
81 | onInventorySlotChanged()
82 |
83 | }
84 |
85 | // -------------------- HOTBAR --------------------
86 |
87 | if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
88 | inventoryRes.SelectPrevSlot()
89 | onInventorySlotChanged()
90 | }
91 |
92 | if inpututil.IsKeyJustPressed(ebiten.KeyE) {
93 | inventoryRes.SelectNextSlot()
94 | onInventorySlotChanged()
95 | }
96 | if inpututil.IsKeyJustPressed(ebiten.KeyR) {
97 | if inventoryRes.CurrentSlotIndex != inventoryRes.QuickSlot2 {
98 | inventoryRes.QuickSlot1 = inventoryRes.CurrentSlotIndex
99 | }
100 |
101 | }
102 | if inpututil.IsKeyJustPressed(ebiten.KeyT) {
103 | if inventoryRes.CurrentSlotIndex != inventoryRes.QuickSlot1 {
104 | inventoryRes.QuickSlot2 = inventoryRes.CurrentSlotIndex
105 | }
106 | }
107 | if inpututil.IsKeyJustPressed(ebiten.KeyTab) {
108 | switch inventoryRes.CurrentSlotIndex {
109 | case inventoryRes.QuickSlot1:
110 | inventoryRes.CurrentSlotIndex = inventoryRes.QuickSlot2
111 | case inventoryRes.QuickSlot2:
112 | inventoryRes.CurrentSlotIndex = inventoryRes.QuickSlot1
113 | default:
114 | inventoryRes.CurrentSlotIndex = inventoryRes.QuickSlot1
115 | }
116 | onInventorySlotChanged()
117 | }
118 |
119 | // -------------------- CRAFTING TABLES --------------------
120 |
121 | if gameDataRes.GameplayState != Playing {
122 |
123 | if inpututil.IsKeyJustPressed(ebiten.KeyD) {
124 | switch gameDataRes.GameplayState {
125 | case Furnace1x2:
126 | craftingTableRes.Pos.X = min(craftingTableRes.Pos.X+1, 0)
127 | case Crafting2x2:
128 | craftingTableRes.Pos.X = min(craftingTableRes.Pos.X+1, 1)
129 | case CraftingTable3x3:
130 | craftingTableRes.Pos.X = min(craftingTableRes.Pos.X+1, 2)
131 | }
132 | }
133 | if inpututil.IsKeyJustPressed(ebiten.KeyS) {
134 | switch gameDataRes.GameplayState {
135 | case Furnace1x2, Crafting2x2:
136 | craftingTableRes.Pos.Y = min(craftingTableRes.Pos.Y+1, 1)
137 | case CraftingTable3x3:
138 | craftingTableRes.Pos.Y = min(craftingTableRes.Pos.Y+1, 2)
139 | }
140 | }
141 |
142 | if inpututil.IsKeyJustPressed(ebiten.KeyA) {
143 | craftingTableRes.Pos.X = max(craftingTableRes.Pos.X-1, 0)
144 | }
145 |
146 | if inpututil.IsKeyJustPressed(ebiten.KeyW) {
147 | craftingTableRes.Pos.Y = max(craftingTableRes.Pos.Y-1, 0)
148 | }
149 |
150 | // Move items from hotbar to crafting table
151 | if inpututil.IsKeyJustPressed(ebiten.KeyRight) {
152 | invSlot := inventoryRes.CurrentSlot()
153 | tableSlot := craftingTableRes.CurrentSlot()
154 | if invSlot.ID != 0 {
155 | if tableSlot.ID == 0 {
156 | id, dur := inventoryRes.RemoveItemFromSelectedSlot()
157 | tableSlot.ID = id
158 | tableSlot.Durability = dur
159 | tableSlot.Quantity = 1
160 | } else if tableSlot.ID == invSlot.ID {
161 | inventoryRes.RemoveItemFromSelectedSlot()
162 | tableSlot.Quantity++
163 | }
164 | }
165 | updateCraftingResultSlot()
166 | onInventorySlotChanged()
167 | }
168 | // Move items from crafting table to hotbar
169 | if inpututil.IsKeyJustPressed(ebiten.KeyLeft) {
170 | tableSlot := craftingTableRes.CurrentSlot()
171 | if tableSlot.ID != 0 {
172 | if tableSlot.Quantity == 1 {
173 | if inventoryRes.AddItemIfEmpty(tableSlot.ID, tableSlot.Durability) {
174 | craftingTableRes.ClearCurrenSlot()
175 | }
176 | } else if tableSlot.Quantity > 1 {
177 | if inventoryRes.AddItemIfEmpty(tableSlot.ID, tableSlot.Durability) {
178 | tableSlot.Quantity--
179 | }
180 | }
181 | }
182 | updateCraftingResultSlot()
183 | onInventorySlotChanged()
184 | }
185 | // apply recipe
186 | if inpututil.IsKeyJustPressed(ebiten.KeyDown) {
187 | minimum := updateCraftingResultSlot()
188 | resultID := craftingTableRes.ResultSlot.ID
189 | dur := items.GetDefaultDurability(resultID)
190 | if resultID != 0 {
191 | for range minimum {
192 | if inventoryRes.AddItemIfEmpty(resultID, dur) {
193 | for y := range 3 {
194 | for x := range 3 {
195 | if craftingTableRes.Slots[y][x].Quantity > 0 {
196 | craftingTableRes.Slots[y][x].Quantity--
197 | }
198 | if craftingTableRes.Slots[y][x].Quantity == 0 {
199 | craftingTableRes.Slots[y][x].ID = 0
200 | }
201 | }
202 | }
203 | craftingTableRes.ResultSlot.ID = 0
204 | }
205 | }
206 | }
207 | updateCraftingResultSlot()
208 | onInventorySlotChanged()
209 | }
210 | }
211 | }
212 | }
213 |
214 | func (ui *UI) Draw() {
215 | if world.Alive(currentPlayer) {
216 |
217 | // Draw hotbar background
218 | colorMDIO.ColorScale.Reset()
219 | colorMDIO.GeoM.Reset()
220 | colorMDIO.GeoM.Translate(hotbarPos.X, hotbarPos.Y)
221 | colorm.DrawImage(Screen, res.HotbarEdge, colorM, colorMDIO)
222 |
223 | colorMDIO.GeoM.Reset()
224 | colorMDIO.GeoM.Scale(ui.hotbarW-8, 1)
225 | colorMDIO.GeoM.Translate(hotbarPos.X+4, hotbarPos.Y)
226 | colorm.DrawImage(Screen, res.HotbarMid, colorM, colorMDIO)
227 |
228 | colorMDIO.GeoM.Reset()
229 | colorMDIO.GeoM.Scale(-1, 1)
230 | colorMDIO.GeoM.Translate(hotbarPos.X+ui.hotbarW, hotbarPos.Y)
231 | colorm.DrawImage(Screen, res.HotbarEdge, colorM, colorMDIO)
232 |
233 | // Draw Quick-slot numbers
234 | dotX := float64(inventoryRes.QuickSlot1)*17 + float64(hotbarPos.X) + 7
235 | textDO.GeoM.Reset()
236 | textDO.GeoM.Translate(dotX, -4)
237 | text.Draw(Screen, strconv.Itoa(inventoryRes.QuickSlot1+1), res.Font, textDO)
238 | dotX = float64(inventoryRes.QuickSlot2)*17 + float64(hotbarPos.X) + 7
239 | textDO.GeoM.Reset()
240 | textDO.GeoM.Translate(dotX, -4)
241 | text.Draw(Screen, strconv.Itoa(inventoryRes.QuickSlot2+1), res.Font, textDO)
242 |
243 | // Draw slots
244 | for x := range len(inventoryRes.Slots) {
245 | slotID := inventoryRes.Slots[x].ID
246 | quantity := inventoryRes.Slots[x].Quantity
247 | SlotOffsetX := float64(x) * 17
248 | SlotOffsetX += hotbarPos.X
249 |
250 | // draw hotbar item icons
251 | colorMDIO.GeoM.Reset()
252 | colorMDIO.GeoM.Translate(SlotOffsetX+(5), hotbarPos.Y+(5))
253 | if slotID != items.Air && inventoryRes.Slots[x].Quantity > 0 {
254 | colorm.DrawImage(Screen, res.Icon8[slotID], colorM, colorMDIO)
255 | }
256 | // Draw hotbar selected slot border
257 | if x == inventoryRes.CurrentSlotIndex {
258 | // border
259 | colorMDIO.GeoM.Translate(-5, -5)
260 | colorm.DrawImage(Screen, res.SlotBorder, colorM, colorMDIO)
261 | // Draw hotbar slot item display name
262 | if !inventoryRes.IsCurrentSlotEmpty() {
263 | textDO.GeoM.Reset()
264 | textDO.GeoM.Translate(SlotOffsetX-1, hotbarPos.Y+14)
265 | if items.HasTag(slotID, items.Tool) {
266 | text.Draw(Screen, fmt.Sprintf(
267 | "%v\nDurability %v",
268 | items.Property[slotID].DisplayName,
269 | inventoryRes.Slots[x].Durability,
270 | ), res.Font, textDO)
271 | } else {
272 | text.Draw(Screen, items.Property[slotID].DisplayName, res.Font, textDO)
273 | }
274 | }
275 | }
276 |
277 | // Draw item quantity number
278 | if quantity > 1 && items.IsStackable(slotID) {
279 | textDO.GeoM.Reset()
280 | textDO.GeoM.Translate(SlotOffsetX+6, hotbarPos.Y+4)
281 | num := strconv.FormatUint(uint64(quantity), 10)
282 | if quantity < 10 {
283 | num = SpaceStr + num
284 | }
285 | text.Draw(Screen, num, res.Font, textDO)
286 | }
287 | }
288 |
289 | // Draw player health text
290 | textDO.GeoM.Reset()
291 | textDO.GeoM.Translate(ui.hotbarW+8, hotbarPos.Y)
292 | playerHealth := mapHealth.GetUnchecked(currentPlayer)
293 | text.Draw(Screen, fmt.Sprintf("Health %v", playerHealth.Current), res.Font, textDO)
294 |
295 | // Draw Game time duration text
296 | textDO.GeoM.Reset()
297 | textDO.GeoM.Translate(ScreenSize.X-58, ScreenSize.Y-18)
298 | text.Draw(Screen, formatDuration(gameDataRes.Duration), res.Font, textDO)
299 |
300 | if gameDataRes.GameplayState != Playing {
301 | // crafting table Background
302 | colorMDIO.GeoM.Reset()
303 | colorMDIO.GeoM.Translate(ui.craftingTablePos.X, ui.craftingTablePos.Y)
304 |
305 | switch gameDataRes.GameplayState {
306 | case Furnace1x2:
307 | colorm.DrawImage(Screen, res.CraftingTable1x2, colorM, colorMDIO)
308 | case Crafting2x2:
309 | colorm.DrawImage(Screen, res.CraftingTable2x2, colorM, colorMDIO)
310 | case CraftingTable3x3:
311 | colorm.DrawImage(Screen, res.CraftingTable3x3, colorM, colorMDIO)
312 | }
313 |
314 | // draw crafting table item icons
315 | for x := range 3 {
316 | for y := range 3 {
317 | if craftingTableRes.Slots[y][x].ID != items.Air {
318 | sx := ui.craftingTablePos.X + float64(x*17)
319 | sy := ui.craftingTablePos.Y + float64(y*17)
320 | colorMDIO.GeoM.Reset()
321 | colorMDIO.GeoM.Translate(sx+6, sy+6)
322 | colorm.DrawImage(
323 | Screen,
324 | res.Icon8[craftingTableRes.Slots[y][x].ID],
325 | colorM,
326 | colorMDIO,
327 | )
328 |
329 | // Draw item quantity number
330 | quantity := craftingTableRes.Slots[y][x].Quantity
331 | if quantity > 1 {
332 | textDO.GeoM.Reset()
333 | textDO.GeoM.Translate(sx+7, sy+5)
334 | num := strconv.FormatUint(uint64(quantity), 10)
335 | if quantity < 10 {
336 | num = SpaceStr + num
337 | }
338 | text.Draw(Screen, num, res.Font, textDO)
339 | }
340 | }
341 |
342 | // draw selected slot border of crafting table
343 | if x == craftingTableRes.Pos.X && y == craftingTableRes.Pos.Y {
344 | sx := ui.craftingTablePos.X + float64(x*17)
345 | sy := ui.craftingTablePos.Y + float64(y*17)
346 | colorMDIO.GeoM.Reset()
347 | colorMDIO.GeoM.Translate(sx+1, sy+1)
348 | colorm.DrawImage(Screen, res.SlotBorder, colorM, colorMDIO)
349 | }
350 |
351 | }
352 | }
353 |
354 | // draw crafting table result item icon
355 | if craftingTableRes.ResultSlot.ID != 0 {
356 | colorMDIO.GeoM.Reset()
357 |
358 | switch gameDataRes.GameplayState {
359 | case Furnace1x2:
360 | colorMDIO.GeoM.Translate(ui.craftingTablePos.X+23, ui.craftingTablePos.Y+14)
361 | case CraftingTable3x3:
362 | colorMDIO.GeoM.Translate(ui.craftingTablePos.X+58, ui.craftingTablePos.Y+23)
363 | case Crafting2x2:
364 | colorMDIO.GeoM.Translate(ui.craftingTablePos.X+41, ui.craftingTablePos.Y+14)
365 | }
366 |
367 | colorm.DrawImage(Screen, res.Icon8[craftingTableRes.ResultSlot.ID], colorM, colorMDIO)
368 |
369 | // Draw result item quantity number
370 | quantity := craftingTableRes.ResultSlot.Quantity
371 |
372 | if quantity > 1 {
373 | textDO.GeoM.Reset()
374 |
375 | switch gameDataRes.GameplayState {
376 | case Furnace1x2:
377 | textDO.GeoM.Translate(ui.craftingTablePos.X+24, ui.craftingTablePos.Y+13)
378 | case CraftingTable3x3:
379 | textDO.GeoM.Translate(ui.craftingTablePos.X+58, ui.craftingTablePos.Y+22)
380 | case Crafting2x2:
381 | textDO.GeoM.Translate(ui.craftingTablePos.X+42, ui.craftingTablePos.Y+13)
382 | }
383 |
384 | num := strconv.FormatUint(uint64(quantity), 10)
385 | if quantity < 10 {
386 | num = SpaceStr + num
387 | }
388 | text.Draw(Screen, num, res.Font, textDO)
389 | }
390 | }
391 |
392 | }
393 |
394 | } else if currentGameState != "menu" {
395 | ebitenutil.DebugPrintAt(Screen, "YOU ARE DEAD!", int(ScreenSize.X/2)-30, int(ScreenSize.Y/2))
396 | }
397 | }
398 |
399 | func onInventorySlotChanged() {
400 | switch inventoryRes.CurrentSlotID() {
401 | case items.WoodenAxe:
402 | animPlayer.SetAtlas("WoodenAxe")
403 | case items.WoodenPickaxe:
404 | animPlayer.SetAtlas("WoodenPickaxe")
405 | case items.WoodenShovel:
406 | animPlayer.SetAtlas("WoodenShovel")
407 | case items.StoneAxe:
408 | animPlayer.SetAtlas("StoneAxe")
409 | case items.StonePickaxe:
410 | animPlayer.SetAtlas("StonePickaxe")
411 | case items.StoneShovel:
412 | animPlayer.SetAtlas("StoneShovel")
413 | case items.IronAxe:
414 | animPlayer.SetAtlas("IronAxe")
415 | case items.IronPickaxe:
416 | animPlayer.SetAtlas("IronPickaxe")
417 | case items.IronShovel:
418 | animPlayer.SetAtlas("IronShovel")
419 | case items.DiamondAxe:
420 | animPlayer.SetAtlas("DiamondAxe")
421 | case items.DiamondPickaxe:
422 | animPlayer.SetAtlas("DiamondPickaxe")
423 | case items.DiamondShovel:
424 | animPlayer.SetAtlas("DiamondShovel")
425 | default:
426 | animPlayer.SetAtlas("Default")
427 | }
428 | }
429 |
430 | func updateCraftingResultSlot() (minimum uint8) {
431 | if gameDataRes.GameplayState == Furnace1x2 {
432 | minimum = craftingTableRes.UpdateResultSlot(items.FurnaceRecipes)
433 | } else {
434 | minimum = craftingTableRes.UpdateResultSlot(items.CraftingRecipes)
435 | }
436 | return minimum
437 | }
438 |
--------------------------------------------------------------------------------
/sys_player.go:
--------------------------------------------------------------------------------
1 | package kar
2 |
3 | import (
4 | "image"
5 | "math"
6 | "math/rand/v2"
7 |
8 | "github.com/setanarut/kar/items"
9 | "github.com/setanarut/kar/tilemap"
10 |
11 | "github.com/hajimehoshi/ebiten/v2"
12 | "github.com/hajimehoshi/ebiten/v2/inpututil"
13 | "github.com/setanarut/v"
14 | )
15 |
16 | var elapsed = 0
17 | var elapsed2 = 5
18 | var blinking bool
19 |
20 | type Player struct {
21 | placeTile image.Point
22 | itemHit *HitInfo
23 | isOnFloor bool
24 | playerTile image.Point
25 | }
26 |
27 | func (p *Player) Init() {
28 |
29 | p.itemHit = &HitInfo{}
30 | }
31 |
32 | func (p *Player) Update() {
33 |
34 | if world.Alive(currentPlayer) {
35 | animPlayer.Update()
36 |
37 | if elapsed > 0 {
38 | elapsed--
39 | if elapsed2 > 0 {
40 | elapsed2--
41 | }
42 | if elapsed2 == 0 {
43 | elapsed2 = 5
44 | blinking = !blinking
45 | }
46 | }
47 |
48 | playerAABB, vl, pHealth, ctrl, fc := mapPlayer.GetUnchecked(currentPlayer)
49 |
50 | pFacing := (*Vec)(fc)
51 | playerVel := (*Vec)(vl)
52 |
53 | absXVelocity := math.Abs(playerVel.X)
54 | // Update input
55 | isBreakKeyPressed := ebiten.IsKeyPressed(ebiten.KeyRight)
56 | isRunKeyPressed := ebiten.IsKeyPressed(ebiten.KeyShift)
57 | isJumpKeyPressed := ebiten.IsKeyPressed(ebiten.KeySpace)
58 | isAttackKeyJustPressed := inpututil.IsKeyJustPressed(ebiten.KeyLeft)
59 | isJumpKeyJustPressed := inpututil.IsKeyJustPressed(ebiten.KeySpace)
60 | inputAxis := Vec{}
61 | if ebiten.IsKeyPressed(ebiten.KeyW) {
62 | inputAxis.Y -= 1
63 | }
64 | if ebiten.IsKeyPressed(ebiten.KeyS) {
65 | inputAxis.Y += 1
66 | }
67 | if ebiten.IsKeyPressed(ebiten.KeyA) {
68 | inputAxis.X -= 1
69 | }
70 | if ebiten.IsKeyPressed(ebiten.KeyD) {
71 | inputAxis.X += 1
72 | }
73 | if !inputAxis.IsZero() {
74 | // restrict facing direction to 4 directions (no diagonal)
75 | switch inputAxis {
76 | case v.Up, v.Down, v.Left, v.Right:
77 | *pFacing = inputAxis
78 | default:
79 | *pFacing = Vec{}
80 | }
81 | }
82 |
83 | if absXVelocity > 0.01 {
84 | *pFacing = Vec{math.Copysign(1, playerVel.X), 0}
85 | }
86 |
87 | isSkidding := inputAxis.X != 0 && (playerVel.X*inputAxis.X < 0)
88 |
89 | if pHealth.Current <= 0 {
90 | // TODO ayrı olarak ölme animasyonu sistemi yaz
91 | world.RemoveEntity(currentPlayer)
92 | }
93 |
94 | // Update states
95 | switch ctrl.CurrentState {
96 | case "idle":
97 | // enter idle
98 | if ctrl.PreviousState != "idle" {
99 | ctrl.PreviousState = ctrl.CurrentState
100 | if pFacing.Y == 0 {
101 | animPlayer.SetAnim("idleRight")
102 | }
103 | if pFacing.X == 0 {
104 | animPlayer.SetAnim("idleUp")
105 | }
106 | }
107 |
108 | // while idle
109 | if pFacing.Y == -1 {
110 | animPlayer.SetAnim("idleUp")
111 | } else if pFacing.Y == 1 {
112 | animPlayer.SetAnim("idleDown")
113 | } else if pFacing.X == 1 {
114 | animPlayer.SetAnim("idleRight")
115 | } else if pFacing.X == -1 {
116 | animPlayer.SetAnim("idleRight")
117 | }
118 |
119 | // Handle specific transitions
120 | if isJumpKeyJustPressed {
121 | if absXVelocity > ctrl.MinSpeedThresForJumpBoostMultiplier {
122 | playerVel.Y = ctrl.JumpPower * ctrl.JumpBoostMultiplier
123 | } else {
124 | playerVel.Y = ctrl.JumpPower
125 | }
126 | ctrl.JumpTimer = 0
127 | ctrl.CurrentState = "jumping"
128 | } else if p.isOnFloor && absXVelocity > 0.01 {
129 | if absXVelocity > ctrl.MaxWalkSpeed {
130 | ctrl.CurrentState = "running"
131 | } else {
132 | ctrl.CurrentState = "walking"
133 | }
134 | } else if !p.isOnFloor && playerVel.Y > 0.01 {
135 | ctrl.CurrentState = "falling"
136 | } else if isBreakKeyPressed && gameDataRes.IsRayHit {
137 | ctrl.CurrentState = "breaking"
138 | } else if playerVel.Y != 0 && playerVel.Y < -0.1 {
139 | ctrl.CurrentState = "jumping"
140 | }
141 | // exit idle
142 | if ctrl.PreviousState != ctrl.CurrentState {
143 | // fmt.Println("exit idle")
144 | }
145 | case "walking":
146 | // enter walking
147 | if ctrl.PreviousState != "walking" {
148 | ctrl.PreviousState = ctrl.CurrentState
149 | animPlayer.SetAnim("walkRight")
150 | }
151 | animPlayer.SetAnimFPS("walkRight", mapRange(absXVelocity, 0, ctrl.MaxRunSpeed, 4, 23))
152 |
153 | // Handle specific transitions
154 | if isSkidding {
155 | ctrl.CurrentState = "skidding"
156 | } else if playerVel.Y > 0 && !p.isOnFloor {
157 | ctrl.CurrentState = "falling"
158 | } else if isJumpKeyJustPressed {
159 | ctrl.CurrentState = "jumping"
160 | if absXVelocity > ctrl.MinSpeedThresForJumpBoostMultiplier {
161 | playerVel.Y = ctrl.JumpPower * ctrl.JumpBoostMultiplier
162 | } else {
163 | playerVel.Y = ctrl.JumpPower
164 | }
165 | ctrl.JumpTimer = 0
166 | } else if absXVelocity == 0 {
167 | ctrl.CurrentState = "idle"
168 | } else if absXVelocity > ctrl.MaxWalkSpeed {
169 | ctrl.CurrentState = "running"
170 | }
171 |
172 | // exit walking
173 | if ctrl.PreviousState != ctrl.CurrentState {
174 | // fmt.Println("exit walking")
175 | }
176 | case "running":
177 | // enter running
178 | if ctrl.PreviousState != "running" {
179 | ctrl.PreviousState = ctrl.CurrentState
180 | animPlayer.SetAnim("walkRight")
181 | }
182 |
183 | // while running
184 | animPlayer.SetAnimFPS("walkRight", mapRange(absXVelocity, 0, ctrl.MaxRunSpeed, 4, 23))
185 |
186 | // Handle specific transitions
187 | if isSkidding {
188 | ctrl.CurrentState = "skidding"
189 | } else if playerVel.Y > 0 && !p.isOnFloor {
190 | ctrl.CurrentState = "falling"
191 | } else if isJumpKeyJustPressed {
192 | if absXVelocity > ctrl.MinSpeedThresForJumpBoostMultiplier {
193 | playerVel.Y = ctrl.JumpPower * ctrl.JumpBoostMultiplier
194 | } else {
195 | playerVel.Y = ctrl.JumpPower
196 | }
197 | ctrl.JumpTimer = 0
198 | ctrl.CurrentState = "jumping"
199 | } else if absXVelocity < 0.01 {
200 | ctrl.CurrentState = "idle"
201 | } else if absXVelocity <= ctrl.MaxWalkSpeed {
202 | ctrl.CurrentState = "walking"
203 | }
204 | // exit running
205 | if ctrl.PreviousState != ctrl.CurrentState {
206 | // fmt.Println("exit running")
207 | }
208 | case "jumping":
209 | // enter running
210 | if ctrl.PreviousState != "jumping" {
211 | ctrl.PreviousState = ctrl.CurrentState
212 | animPlayer.SetAnim("jump")
213 | }
214 |
215 | // skidding jumpg physics
216 | if ctrl.PreviousState == "skidding" {
217 | if !isJumpKeyPressed && ctrl.JumpTimer < ctrl.JumpReleaseTimer {
218 | playerVel.Y = ctrl.ShortJumpVelocity * 0.7 // Kısa zıplama gücünü azalt
219 | ctrl.JumpTimer = ctrl.JumpHoldTime
220 | } else if isJumpKeyPressed && ctrl.JumpTimer < ctrl.JumpHoldTime {
221 | playerVel.Y += ctrl.JumpBoost * 0.7 // Boost gücünü azalt
222 | ctrl.JumpTimer++
223 | } else if playerVel.Y >= 0.01 {
224 | ctrl.CurrentState = "falling"
225 | }
226 | } else {
227 | // normal skidding
228 | if !isJumpKeyPressed && ctrl.JumpTimer < ctrl.JumpReleaseTimer {
229 | playerVel.Y = ctrl.ShortJumpVelocity
230 | ctrl.JumpTimer = ctrl.JumpHoldTime
231 | } else if isJumpKeyPressed && ctrl.JumpTimer < ctrl.JumpHoldTime {
232 | speedFactor := (absXVelocity / ctrl.MaxRunSpeed) * ctrl.SpeedJumpFactor
233 | playerVel.Y += ctrl.JumpBoost * (1 + speedFactor)
234 | ctrl.JumpTimer++
235 | } else if playerVel.Y >= 0 {
236 | ctrl.CurrentState = "falling"
237 | }
238 | }
239 |
240 | // apply air skidding Decel
241 | if isSkidding {
242 | playerVel.X += math.Copysign(ctrl.AirSkiddingDecel, -playerVel.X)
243 | }
244 |
245 | // exit jumping
246 | if ctrl.PreviousState != ctrl.CurrentState {
247 | // fmt.Println("exit jumping")
248 | }
249 | case "falling":
250 | // enter falling
251 | if ctrl.PreviousState != "falling" {
252 | ctrl.PreviousState = ctrl.CurrentState
253 | ctrl.FallingDamageTempPosY = playerAABB.Pos.Y + playerAABB.Half.Y
254 | animPlayer.SetAnim("jump")
255 | }
256 |
257 | // TODO erken zıplama toleransı için mantık yaz.
258 |
259 | // transitions
260 | if p.isOnFloor {
261 | if absXVelocity <= 0 {
262 | ctrl.CurrentState = "idle"
263 | } else if isRunKeyPressed {
264 | ctrl.CurrentState = "running"
265 | } else {
266 | ctrl.CurrentState = "walking"
267 | }
268 |
269 | }
270 | // exit falling
271 | if ctrl.PreviousState != ctrl.CurrentState {
272 | // fmt.Println("exit falling")
273 | d := int(((playerAABB.Pos.Y + playerAABB.Half.Y) - ctrl.FallingDamageTempPosY) / 30)
274 | if d > 3 {
275 | pHealth.Current -= d - 3
276 | }
277 | }
278 | case "breaking":
279 | // enter breaking
280 | if ctrl.PreviousState != "breaking" {
281 | // fmt.Println("enter breaking")
282 | ctrl.PreviousState = ctrl.CurrentState
283 | }
284 | // update animation states
285 | if pFacing.X == 1 {
286 | if absXVelocity > 0.01 {
287 | animPlayer.SetAnim("attackWalk")
288 | } else {
289 | animPlayer.SetAnim("attackRight")
290 | }
291 | } else if pFacing.X == -1 {
292 | if absXVelocity > 0.01 {
293 | animPlayer.SetAnim("attackWalk")
294 | } else {
295 | animPlayer.SetAnim("attackRight")
296 | }
297 | } else if pFacing.Y == 1 {
298 | animPlayer.SetAnim("attackDown")
299 | } else if pFacing.Y == -1 {
300 | animPlayer.SetAnim("attackUp")
301 | }
302 |
303 | // break block
304 | if gameDataRes.IsRayHit {
305 | blockID := tileMapRes.GetID(gameDataRes.TargetBlockCoord.X, gameDataRes.TargetBlockCoord.Y)
306 | if !items.HasTag(blockID, items.UnbreakableBlock) {
307 | if items.IsBestTool(blockID, inventoryRes.CurrentSlotID()) {
308 | gameDataRes.BlockHealth += PlayerBestToolDamage
309 | } else {
310 | gameDataRes.BlockHealth += PlayerDefaultDamage
311 | }
312 | }
313 | // Destroy block
314 | if gameDataRes.BlockHealth >= 180 {
315 |
316 | // set air
317 | tileMapRes.Set(gameDataRes.TargetBlockCoord.X, gameDataRes.TargetBlockCoord.Y, items.Air)
318 | gameDataRes.BlockHealth = 0
319 |
320 | if items.HasTag(inventoryRes.CurrentSlotID(), items.Tool) {
321 | // damage the tool
322 | inventoryRes.CurrentSlot().Durability--
323 | // If durability is 0, destroy the tool.
324 | if inventoryRes.CurrentSlot().Durability <= 0 {
325 | inventoryRes.ClearCurrentSlot()
326 | }
327 | }
328 | // Spawn drop item
329 | pos := tileMapRes.TileToWorld(gameDataRes.TargetBlockCoord)
330 | dropid := items.Property[blockID].DropID
331 | if blockID == items.OakLeaves {
332 | if rand.N(2) == 0 {
333 | dropid = items.OakLeaves
334 | }
335 | }
336 | toSpawnItem = append(toSpawnItem, spawnItemData{pos, dropid, 0})
337 | }
338 | }
339 | // transitions
340 | if !gameDataRes.IsRayHit || (!isBreakKeyPressed && p.isOnFloor) {
341 | ctrl.CurrentState = "idle"
342 | } else if !p.isOnFloor && playerVel.Y > 0.01 {
343 | ctrl.CurrentState = "falling"
344 | } else if !isBreakKeyPressed && isJumpKeyJustPressed {
345 | playerVel.Y = ctrl.JumpPower
346 | ctrl.JumpTimer = 0
347 | ctrl.CurrentState = "jumping"
348 | }
349 | // exit breaking
350 | if ctrl.PreviousState != ctrl.CurrentState {
351 | // fmt.Println("exit breaking")
352 | gameDataRes.BlockHealth = 0
353 | }
354 | case "skidding":
355 | // enter skidding
356 | if ctrl.PreviousState != "skidding" {
357 | // fmt.Println("enter skidding")
358 | ctrl.PreviousState = ctrl.CurrentState
359 | }
360 | // Apply Skidding decel
361 | if absXVelocity > ctrl.SkiddingFriction {
362 | playerVel.X += math.Copysign(ctrl.SkiddingFriction, -playerVel.X)
363 | }
364 | if absXVelocity > 0.5 {
365 | animPlayer.SetAnim("skidding")
366 | }
367 |
368 | // Handle specific transitions
369 | if ctrl.SkiddingJumpEnabled && isJumpKeyJustPressed {
370 | // Yeni yöne doğru çok küçük sabit değerle başla
371 | playerVel.X = math.Copysign(0.3, float64(inputAxis.X))
372 | playerVel.Y = ctrl.JumpPower * 0.7 // Zıplama gücünü azalt
373 | ctrl.JumpTimer = 0
374 | ctrl.CurrentState = "jumping"
375 | } else if absXVelocity < 0.01 {
376 | ctrl.CurrentState = "idle"
377 | } else if !isSkidding {
378 | if absXVelocity > ctrl.MaxWalkSpeed {
379 | ctrl.CurrentState = "running"
380 | } else {
381 | ctrl.CurrentState = "walking"
382 | }
383 | }
384 | if ctrl.PreviousState != ctrl.CurrentState {
385 | // fmt.Println("exit skidding")
386 | }
387 | }
388 |
389 | /// Update Physics
390 | currentAccel := ctrl.WalkAcceleration
391 | currentDecel := ctrl.WalkDeceleration
392 | maxSpeed := ctrl.MaxWalkSpeed
393 | playerVel.Y = min(ctrl.MaxFallSpeed, playerVel.Y+ctrl.Gravity)
394 |
395 | if !isSkidding {
396 | if isRunKeyPressed {
397 | maxSpeed, currentAccel, currentDecel = ctrl.MaxRunSpeed, ctrl.RunAcceleration, ctrl.RunDeceleration
398 | } else if math.Abs(playerVel.X) > ctrl.MaxWalkSpeed {
399 | currentDecel = ctrl.RunDeceleration
400 | }
401 | }
402 |
403 | targetSpeed := maxSpeed
404 | if inputAxis.X == 0 {
405 | targetSpeed = 0
406 | } else if inputAxis.X < 0 {
407 | targetSpeed = -maxSpeed
408 | }
409 |
410 | if playerVel.X < targetSpeed {
411 | playerVel.X = min(targetSpeed, playerVel.X+currentAccel)
412 | } else {
413 | playerVel.X = max(targetSpeed, playerVel.X-currentDecel)
414 | }
415 |
416 | // Player and tilemap collision
417 | *playerVel = tileCollider.Collide(*playerAABB, *playerVel, func(hitInfos []HitTileInfo, delta Vec) {
418 | p.isOnFloor = false
419 | for _, hit := range hitInfos {
420 | tileID := tileMapRes.GetIDUnchecked(hit.TileCoords)
421 | if hit.Normal.Y == -1 {
422 | // Ground collision
423 | playerVel.Y = 0
424 | p.isOnFloor = true
425 | }
426 | // Ceil collision
427 | if hit.Normal.Y == 1 {
428 | playerVel.Y = 0
429 |
430 | switch tileID {
431 | case items.StoneBricks:
432 | // Destroy block when ceil hit
433 | tileMapRes.Set(hit.TileCoords.X, hit.TileCoords.Y, items.Air)
434 | SpawnEffect(tileMapRes.TileToWorld(hit.TileCoords), tileID)
435 | case items.Random:
436 | if tileMapRes.GetIDUnchecked(hit.TileCoords.Add(tilemap.Up)) == items.Air {
437 | tileMapRes.Set(hit.TileCoords.X, hit.TileCoords.Y, items.Bedrock)
438 | ceilBlockCoord = hit.TileCoords
439 | ceilBlockTick = 3
440 | }
441 | }
442 | }
443 | // Right or Left wall collision
444 | if hit.Normal.X == -1 || hit.Normal.X == 1 {
445 | // While running at maximum speed, hold down the right arrow key and hit the block to destroy it.
446 | if absXVelocity == ctrl.MaxRunSpeed && isBreakKeyPressed {
447 | tileMapRes.Set(hit.TileCoords.X, hit.TileCoords.Y, items.Air)
448 | SpawnEffect(tileMapRes.TileToWorld(hit.TileCoords), tileID)
449 | }
450 | playerVel.X = 0
451 | }
452 | }
453 | },
454 | )
455 |
456 | // Platform collision
457 | playerAABB.Pos = playerAABB.Pos.Add(*playerVel)
458 | hit := &HitInfo2{}
459 | pq := filterPlatform.Query()
460 | for pq.Next() {
461 |
462 | platformAABB, platformVel, platformType := pq.Get()
463 |
464 | if AABBPlatform(playerAABB, platformAABB, playerVel, (*Vec)(platformVel), hit) {
465 | if hit.Top {
466 | if *platformType == "solid" {
467 | playerAABB.Pos = playerAABB.Pos.Add(hit.Delta)
468 | playerVel.Y = 0
469 | }
470 | }
471 | if hit.Bottom {
472 | playerAABB.Pos = playerAABB.Pos.Add(hit.Delta)
473 | playerVel.Y = 0
474 | pvel := tileCollider.Collide(*playerAABB, Vec(*platformVel), nil)
475 | playerAABB.Pos = playerAABB.Pos.Add(pvel)
476 | p.isOnFloor = true
477 | }
478 | if hit.Right {
479 | playerAABB.Pos = playerAABB.Pos.Add(hit.Delta)
480 | playerVel.X = -1.01
481 | }
482 | if hit.Left {
483 | playerAABB.Pos = playerAABB.Pos.Add(hit.Delta)
484 | playerVel.X = +1.01
485 | }
486 | }
487 | }
488 |
489 | // Enemy collision
490 | hit.Reset()
491 | eq := filterEnemy.Query()
492 | for eq.Next() {
493 | enemyAABB, enemyVel, mobileID, _ := eq.Get()
494 | if AABBPlatform(playerAABB, enemyAABB, playerVel, (*Vec)(enemyVel), hit) {
495 | if hit.Top {
496 | switch *mobileID {
497 | case CrabID:
498 | playerAABB.Pos = playerAABB.Pos.Add(hit.Delta)
499 | playerVel.Y = 0
500 | if elapsed == 0 {
501 | pHealth.Current -= 5
502 | elapsed = 2 * 60
503 | }
504 | }
505 | }
506 | if hit.Bottom {
507 | playerAABB.Pos = playerAABB.Pos.Add(hit.Delta)
508 | playerVel.Y = 0
509 | pvel := tileCollider.Collide(*playerAABB, Vec(*enemyVel), nil)
510 | playerAABB.Pos = playerAABB.Pos.Add(pvel)
511 | p.isOnFloor = true
512 | toRemove = append(toRemove, eq.Entity())
513 | }
514 | if hit.Right {
515 | playerAABB.Pos = playerAABB.Pos.Add(hit.Delta)
516 | playerVel.X = -1.01
517 | if elapsed == 0 {
518 | pHealth.Current -= 5
519 | elapsed = 2 * 60
520 | }
521 | }
522 | if hit.Left {
523 | playerAABB.Pos = playerAABB.Pos.Add(hit.Delta)
524 | playerVel.X = +1.01
525 | if elapsed == 0 {
526 | pHealth.Current -= 5
527 | elapsed = 2 * 60
528 | }
529 | }
530 | }
531 | }
532 |
533 | // player facing raycast for target block
534 | p.playerTile = tileMapRes.WorldToTile(playerAABB.Pos.X, playerAABB.Pos.Y)
535 | targetBlockTemp := gameDataRes.TargetBlockCoord
536 | gameDataRes.TargetBlockCoord, gameDataRes.IsRayHit = tileMapRes.Raycast(
537 | p.playerTile,
538 | int(pFacing.X), int(pFacing.Y),
539 | RaycastDist,
540 | )
541 |
542 | // reset attack if block focus changed
543 | if !gameDataRes.TargetBlockCoord.Eq(targetBlockTemp) || !gameDataRes.IsRayHit {
544 | gameDataRes.BlockHealth = 0
545 | }
546 |
547 | // place block if IsAttackKeyJustPressed
548 | if isAttackKeyJustPressed {
549 | anyItemOverlapsWithPlaceRect := false
550 | // if slot item is block
551 | if gameDataRes.IsRayHit && items.HasTag(inventoryRes.CurrentSlot().ID, items.Block) {
552 | // Get tile rect
553 | p.placeTile = gameDataRes.TargetBlockCoord.Sub(image.Point{int(pFacing.X), int(pFacing.Y)})
554 | placeTileBox := &AABB{
555 | Pos: Vec{float64(p.placeTile.X*20) + 10, float64(p.placeTile.Y*20) + 10},
556 | Half: Vec{10, 10},
557 | }
558 | // check place block overlaps
559 | queryItem := filterDroppedItem.Query()
560 | for queryItem.Next() {
561 | dropItemAABB.Pos = *(*Vec)(mapPos.GetUnchecked(queryItem.Entity()))
562 | anyItemOverlapsWithPlaceRect = Overlap(placeTileBox, dropItemAABB, nil)
563 | if anyItemOverlapsWithPlaceRect {
564 | queryItem.Close()
565 | break
566 | }
567 | }
568 | if !anyItemOverlapsWithPlaceRect {
569 | if !Overlap(playerAABB, placeTileBox, nil) {
570 | // place block
571 | tileMapRes.Set(p.placeTile.X, p.placeTile.Y, inventoryRes.CurrentSlotID())
572 | // remove item
573 | inventoryRes.RemoveItemFromSelectedSlot()
574 | }
575 | }
576 | // if slot item snowball, throw snowball
577 | } else if inventoryRes.CurrentSlot().ID == items.Snowball {
578 | if ctrl.CurrentState != "skidding" {
579 | spawnPos := Vec{playerAABB.Pos.X, playerAABB.Pos.Y - 4}
580 | spawnVel := Vec{SnowballSpeedX, SnowballMaxFallVelocity}
581 | switch *pFacing {
582 | case v.Right:
583 | SpawnProjectile(items.Snowball, spawnPos, spawnVel)
584 | case v.Left:
585 | SpawnProjectile(items.Snowball, spawnPos, spawnVel.NegX())
586 | }
587 | inventoryRes.RemoveItemFromSelectedSlot()
588 | }
589 | }
590 | }
591 |
592 | // drop Item
593 | if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) {
594 | currentSlot := inventoryRes.CurrentSlot()
595 | if currentSlot.ID != items.Air {
596 | toSpawnItem = append(toSpawnItem, spawnItemData{
597 | playerAABB.Pos,
598 | currentSlot.ID,
599 | currentSlot.Durability,
600 | })
601 | inventoryRes.RemoveItemFromSelectedSlot()
602 | onInventorySlotChanged()
603 | }
604 | }
605 |
606 | }
607 | }
608 | func (c *Player) Draw() {
609 | // Draw player
610 | if world.Alive(currentPlayer) {
611 | playerBox := mapAABB.GetUnchecked(currentPlayer)
612 |
613 | colorMDIO.GeoM.Reset()
614 | x := playerBox.Pos.X - playerBox.Half.X
615 | y := playerBox.Pos.Y - playerBox.Half.Y
616 | if mapFacing.GetUnchecked(currentPlayer).X == -1 {
617 | colorMDIO.GeoM.Scale(-1, 1)
618 | colorMDIO.GeoM.Translate(playerBox.Pos.X+playerBox.Half.X, y)
619 | } else {
620 | colorMDIO.GeoM.Translate(x, y)
621 | }
622 | if blinking {
623 | colorMDIO.ColorScale.SetA(0.2)
624 | } else {
625 | colorMDIO.ColorScale.SetA(1)
626 | }
627 | cameraRes.DrawWithColorM(animPlayer.CurrentFrame, colorM, colorMDIO, Screen)
628 |
629 | }
630 | }
631 |
--------------------------------------------------------------------------------