├── 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 | a 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 | --------------------------------------------------------------------------------