Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be9fc24cd9 | ||
|
|
6f30cd5ff2 | ||
|
|
f2a24a37b8 | ||
|
|
2c4527ae8b | ||
|
|
d29b4f07ea | ||
|
|
3026aeadc2 | ||
|
|
c717f92b6e | ||
|
|
8585a18894 | ||
|
|
9317e83fd5 | ||
|
|
e166076e11 | ||
|
|
3e1b809199 | ||
|
|
24389bbc63 | ||
|
|
2a99d7ecf3 | ||
|
|
6bdb096e1b | ||
|
|
e7806dcd66 | ||
|
|
28165ed399 | ||
|
|
bfc39201e1 | ||
|
|
4d9c52c56a | ||
|
|
e75dd82a76 | ||
|
|
781e8653e7 | ||
|
|
7e69113d37 | ||
|
|
1e74f5b054 | ||
|
|
d57f8a126c | ||
|
|
f2f8f1fe1e | ||
|
|
f14864182c | ||
|
|
4124f39632 | ||
|
|
4d267c7f5d | ||
|
|
d6ee5abb01 | ||
|
|
4bb3e710c1 | ||
|
|
daa5f75c95 | ||
|
|
e660ce611b | ||
|
|
0815530717 | ||
|
|
fcfd1e95d8 | ||
|
|
ff341cb834 | ||
|
|
a968ad98fd | ||
|
|
0729384898 | ||
|
|
195b94076b | ||
|
|
1a18c42b9e | ||
|
|
433ac4152b | ||
|
|
2d8cc5d1d7 | ||
|
|
54b10f000b | ||
|
|
d44e9501f5 | ||
|
|
75c29a4d5e | ||
|
|
c8d191e7c7 | ||
|
|
d097e36419 | ||
|
|
cc71ee1286 |
@@ -0,0 +1,14 @@
|
||||
## Game
|
||||
|
||||
This is a very simple game.
|
||||
|
||||
## Objective
|
||||
|
||||
- Explore Nystrom's Game Programming patterns when appropriate.
|
||||
- Learn Odin
|
||||
- Enjoy the love of hand writing code (cuz I love it)
|
||||
|
||||
## Goals
|
||||
- Build a playable single player game
|
||||
- Build a multiplayer aspect
|
||||
- Keep things as simple as possible
|
||||
@@ -1,601 +0,0 @@
|
||||
package clay
|
||||
|
||||
import "core:c"
|
||||
|
||||
when ODIN_OS == .Windows {
|
||||
foreign import Clay "windows/clay.lib"
|
||||
} else when ODIN_OS == .Linux {
|
||||
foreign import Clay "linux/clay.a"
|
||||
} else when ODIN_OS == .Darwin {
|
||||
when ODIN_ARCH == .arm64 {
|
||||
foreign import Clay "macos-arm64/clay.a"
|
||||
} else {
|
||||
foreign import Clay "macos/clay.a"
|
||||
}
|
||||
} else when ODIN_ARCH == .wasm32 || ODIN_ARCH == .wasm64p32 {
|
||||
foreign import Clay "wasm/clay.o"
|
||||
}
|
||||
|
||||
String :: struct {
|
||||
isStaticallyAllocated: c.bool,
|
||||
length: c.int32_t,
|
||||
chars: [^]c.char,
|
||||
}
|
||||
|
||||
StringSlice :: struct {
|
||||
length: c.int32_t,
|
||||
chars: [^]c.char,
|
||||
baseChars: [^]c.char,
|
||||
}
|
||||
|
||||
Vector2 :: [2]c.float
|
||||
|
||||
Dimensions :: struct {
|
||||
width: c.float,
|
||||
height: c.float,
|
||||
}
|
||||
|
||||
Arena :: struct {
|
||||
nextAllocation: uintptr,
|
||||
capacity: c.size_t,
|
||||
memory: [^]c.char,
|
||||
}
|
||||
|
||||
BoundingBox :: struct {
|
||||
x: c.float,
|
||||
y: c.float,
|
||||
width: c.float,
|
||||
height: c.float,
|
||||
}
|
||||
|
||||
Color :: [4]c.float
|
||||
|
||||
CornerRadius :: struct {
|
||||
topLeft: c.float,
|
||||
topRight: c.float,
|
||||
bottomLeft: c.float,
|
||||
bottomRight: c.float,
|
||||
}
|
||||
|
||||
ElementId :: struct {
|
||||
id: u32,
|
||||
offset: u32,
|
||||
baseId: u32,
|
||||
stringId: String,
|
||||
}
|
||||
|
||||
ElementIdArray :: struct {
|
||||
capacity: i32,
|
||||
length: i32,
|
||||
internalArray: [^]ElementId,
|
||||
}
|
||||
|
||||
when ODIN_OS == .Windows {
|
||||
EnumBackingType :: u32
|
||||
} else {
|
||||
EnumBackingType :: u8
|
||||
}
|
||||
|
||||
RenderCommandType :: enum EnumBackingType {
|
||||
None,
|
||||
Rectangle,
|
||||
Border,
|
||||
Text,
|
||||
Image,
|
||||
ScissorStart,
|
||||
ScissorEnd,
|
||||
OverlayColorStart,
|
||||
OverlayColorEnd,
|
||||
Custom,
|
||||
}
|
||||
|
||||
RectangleElementConfig :: struct {
|
||||
color: Color,
|
||||
}
|
||||
|
||||
TextWrapMode :: enum EnumBackingType {
|
||||
Words,
|
||||
Newlines,
|
||||
None,
|
||||
}
|
||||
|
||||
TextAlignment :: enum EnumBackingType {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
TextElementConfig :: struct {
|
||||
userData: rawptr,
|
||||
textColor: Color,
|
||||
fontId: u16,
|
||||
fontSize: u16,
|
||||
letterSpacing: u16,
|
||||
lineHeight: u16,
|
||||
wrapMode: TextWrapMode,
|
||||
textAlignment: TextAlignment,
|
||||
}
|
||||
|
||||
AspectRatioElementConfig :: struct {
|
||||
aspectRatio: f32,
|
||||
}
|
||||
|
||||
ImageElementConfig :: struct {
|
||||
imageData: rawptr,
|
||||
}
|
||||
|
||||
CustomElementConfig :: struct {
|
||||
customData: rawptr,
|
||||
}
|
||||
|
||||
BorderWidth :: struct {
|
||||
left: u16,
|
||||
right: u16,
|
||||
top: u16,
|
||||
bottom: u16,
|
||||
betweenChildren: u16,
|
||||
}
|
||||
|
||||
BorderElementConfig :: struct {
|
||||
color: Color,
|
||||
width: BorderWidth,
|
||||
}
|
||||
|
||||
TransitionData :: struct {
|
||||
boundingBox: BoundingBox,
|
||||
backgroundColor: Color,
|
||||
overlayColor: Color,
|
||||
borderColor: Color,
|
||||
borderWidth: BorderWidth,
|
||||
}
|
||||
|
||||
TransitionState :: enum c.int {
|
||||
Idle,
|
||||
Entering,
|
||||
Transitioning,
|
||||
Exiting,
|
||||
}
|
||||
|
||||
TransitionProperty :: enum c.int {
|
||||
X,
|
||||
Y,
|
||||
Width,
|
||||
Height,
|
||||
BackgroundColor,
|
||||
OverlayColor,
|
||||
CornerRadius,
|
||||
BorderColor,
|
||||
BorderWidth,
|
||||
}
|
||||
|
||||
TransitionPropertyFlags :: bit_set[TransitionProperty;c.int]
|
||||
TransitionPropertyPosition :: TransitionPropertyFlags{.X, .Y}
|
||||
TransitionPropertyDimensions :: TransitionPropertyFlags{.Width, .Height}
|
||||
TransitionPropertyBoundingBox :: TransitionPropertyPosition + TransitionPropertyDimensions
|
||||
TransitionPropertyBorder :: TransitionPropertyFlags{.BorderColor, .BorderWidth}
|
||||
|
||||
TransitionCallbackArguments :: struct {
|
||||
transitionState: TransitionState,
|
||||
initial: TransitionData,
|
||||
current: ^TransitionData,
|
||||
target: TransitionData,
|
||||
elapsedTime: f32,
|
||||
duration: f32,
|
||||
properties: TransitionPropertyFlags,
|
||||
}
|
||||
|
||||
TransitionEnterTriggerType :: enum EnumBackingType {
|
||||
SkipOnFirstParentFrame,
|
||||
TriggerOnFirstParentFrame,
|
||||
}
|
||||
|
||||
TransitionExitTriggerType :: enum EnumBackingType {
|
||||
SkipWhenParentExits,
|
||||
TriggerWhenParentExits,
|
||||
}
|
||||
|
||||
TransitionInteractionHandlingType :: enum EnumBackingType {
|
||||
DisableInteractionsWhileTransitioningPosition,
|
||||
AllowInteractionsWhileTransitioningPosition,
|
||||
}
|
||||
|
||||
ExitTransitionSiblingOrdering :: enum EnumBackingType {
|
||||
UnderneathSiblings,
|
||||
NaturalOrder,
|
||||
AboveSiblings,
|
||||
}
|
||||
|
||||
TransitionElementConfig :: struct {
|
||||
handler: proc "c" (args: TransitionCallbackArguments) -> bool,
|
||||
duration: f32,
|
||||
properties: TransitionPropertyFlags,
|
||||
interactionHandling: TransitionInteractionHandlingType,
|
||||
enter: struct {
|
||||
setInitialState: proc "c" (initialState: TransitionData, properties: TransitionPropertyFlags) -> TransitionData,
|
||||
trigger: TransitionEnterTriggerType,
|
||||
},
|
||||
exit: struct {
|
||||
setFinalState: proc "c" (finalState: TransitionData, properties: TransitionPropertyFlags) -> TransitionData,
|
||||
trigger: TransitionExitTriggerType,
|
||||
siblingOrdering: ExitTransitionSiblingOrdering,
|
||||
},
|
||||
}
|
||||
|
||||
ClipElementConfig :: struct {
|
||||
horizontal: bool, // clip overflowing elements on the "X" axis
|
||||
vertical: bool, // clip overflowing elements on the "Y" axis
|
||||
childOffset: Vector2, // offsets the [X,Y] positions of all child elements, primarily for scrolling containers
|
||||
}
|
||||
|
||||
FloatingAttachPointType :: enum EnumBackingType {
|
||||
LeftTop,
|
||||
LeftCenter,
|
||||
LeftBottom,
|
||||
CenterTop,
|
||||
CenterCenter,
|
||||
CenterBottom,
|
||||
RightTop,
|
||||
RightCenter,
|
||||
RightBottom,
|
||||
}
|
||||
|
||||
FloatingAttachPoints :: struct {
|
||||
element: FloatingAttachPointType,
|
||||
parent: FloatingAttachPointType,
|
||||
}
|
||||
|
||||
PointerCaptureMode :: enum EnumBackingType {
|
||||
Capture,
|
||||
Passthrough,
|
||||
}
|
||||
|
||||
FloatingAttachToElement :: enum EnumBackingType {
|
||||
None,
|
||||
Parent,
|
||||
ElementWithId,
|
||||
Root,
|
||||
}
|
||||
|
||||
FloatingClipToElement :: enum EnumBackingType {
|
||||
None,
|
||||
AttachedParent,
|
||||
}
|
||||
|
||||
FloatingElementConfig :: struct {
|
||||
offset: Vector2,
|
||||
expand: Dimensions,
|
||||
parentId: u32,
|
||||
zIndex: i16,
|
||||
attachment: FloatingAttachPoints,
|
||||
pointerCaptureMode: PointerCaptureMode,
|
||||
attachTo: FloatingAttachToElement,
|
||||
clipTo: FloatingClipToElement,
|
||||
}
|
||||
|
||||
TextRenderData :: struct {
|
||||
stringContents: StringSlice,
|
||||
textColor: Color,
|
||||
fontId: u16,
|
||||
fontSize: u16,
|
||||
letterSpacing: u16,
|
||||
lineHeight: u16,
|
||||
}
|
||||
|
||||
RectangleRenderData :: struct {
|
||||
backgroundColor: Color,
|
||||
cornerRadius: CornerRadius,
|
||||
}
|
||||
|
||||
ImageRenderData :: struct {
|
||||
backgroundColor: Color,
|
||||
cornerRadius: CornerRadius,
|
||||
imageData: rawptr,
|
||||
}
|
||||
|
||||
CustomRenderData :: struct {
|
||||
backgroundColor: Color,
|
||||
cornerRadius: CornerRadius,
|
||||
customData: rawptr,
|
||||
}
|
||||
|
||||
ClipRenderData :: struct {
|
||||
horizontal: bool,
|
||||
vertical: bool,
|
||||
}
|
||||
|
||||
OverlayColorRenderData :: struct {
|
||||
color: Color,
|
||||
}
|
||||
|
||||
BorderRenderData :: struct {
|
||||
color: Color,
|
||||
cornerRadius: CornerRadius,
|
||||
width: BorderWidth,
|
||||
}
|
||||
|
||||
RenderCommandData :: struct #raw_union {
|
||||
rectangle: RectangleRenderData,
|
||||
text: TextRenderData,
|
||||
image: ImageRenderData,
|
||||
custom: CustomRenderData,
|
||||
border: BorderRenderData,
|
||||
clip: ClipRenderData,
|
||||
overlayColor: OverlayColorRenderData,
|
||||
}
|
||||
|
||||
RenderCommand :: struct {
|
||||
boundingBox: BoundingBox,
|
||||
renderData: RenderCommandData,
|
||||
userData: rawptr,
|
||||
id: u32,
|
||||
zIndex: i16,
|
||||
commandType: RenderCommandType,
|
||||
}
|
||||
|
||||
ScrollContainerData :: struct {
|
||||
// Note: This is a pointer to the real internal scroll position, mutating it may cause a change in final layout.
|
||||
// Intended for use with external functionality that modifies scroll position, such as scroll bars or auto scrolling.
|
||||
scrollPosition: ^Vector2,
|
||||
scrollContainerDimensions: Dimensions,
|
||||
contentDimensions: Dimensions,
|
||||
config: ClipElementConfig,
|
||||
// Indicates whether an actual scroll container matched the provided ID or if the default struct was returned.
|
||||
found: bool,
|
||||
}
|
||||
|
||||
ElementData :: struct {
|
||||
boundingBox: BoundingBox,
|
||||
found: bool,
|
||||
}
|
||||
|
||||
PointerDataInteractionState :: enum EnumBackingType {
|
||||
PressedThisFrame,
|
||||
Pressed,
|
||||
ReleasedThisFrame,
|
||||
Released,
|
||||
}
|
||||
|
||||
PointerData :: struct {
|
||||
position: Vector2,
|
||||
state: PointerDataInteractionState,
|
||||
}
|
||||
|
||||
SizingType :: enum EnumBackingType {
|
||||
Fit,
|
||||
Grow,
|
||||
Percent,
|
||||
Fixed,
|
||||
}
|
||||
|
||||
SizingConstraintsMinMax :: struct {
|
||||
min: c.float,
|
||||
max: c.float,
|
||||
}
|
||||
|
||||
SizingConstraints :: struct #raw_union {
|
||||
sizeMinMax: SizingConstraintsMinMax,
|
||||
sizePercent: c.float,
|
||||
}
|
||||
|
||||
SizingAxis :: struct {
|
||||
// Note: `min` is used for CLAY_SIZING_PERCENT, slightly different to clay.h due to lack of C anonymous unions
|
||||
constraints: SizingConstraints,
|
||||
type: SizingType,
|
||||
}
|
||||
|
||||
Sizing :: struct {
|
||||
width: SizingAxis,
|
||||
height: SizingAxis,
|
||||
}
|
||||
|
||||
Padding :: struct {
|
||||
left: u16,
|
||||
right: u16,
|
||||
top: u16,
|
||||
bottom: u16,
|
||||
}
|
||||
|
||||
LayoutDirection :: enum EnumBackingType {
|
||||
LeftToRight,
|
||||
TopToBottom,
|
||||
}
|
||||
|
||||
LayoutAlignmentX :: enum EnumBackingType {
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
}
|
||||
|
||||
LayoutAlignmentY :: enum EnumBackingType {
|
||||
Top,
|
||||
Bottom,
|
||||
Center,
|
||||
}
|
||||
|
||||
ChildAlignment :: struct {
|
||||
x: LayoutAlignmentX,
|
||||
y: LayoutAlignmentY,
|
||||
}
|
||||
|
||||
LayoutConfig :: struct {
|
||||
sizing: Sizing,
|
||||
padding: Padding,
|
||||
childGap: u16,
|
||||
childAlignment: ChildAlignment,
|
||||
layoutDirection: LayoutDirection,
|
||||
}
|
||||
|
||||
ClayArray :: struct($type: typeid) {
|
||||
capacity: i32,
|
||||
length: i32,
|
||||
internalArray: [^]type,
|
||||
}
|
||||
|
||||
ElementDeclaration :: struct {
|
||||
layout: LayoutConfig,
|
||||
backgroundColor: Color,
|
||||
overlayColor: Color,
|
||||
cornerRadius: CornerRadius,
|
||||
aspectRatio: AspectRatioElementConfig,
|
||||
image: ImageElementConfig,
|
||||
floating: FloatingElementConfig,
|
||||
custom: CustomElementConfig,
|
||||
clip: ClipElementConfig,
|
||||
border: BorderElementConfig,
|
||||
transition: TransitionElementConfig,
|
||||
userData: rawptr,
|
||||
}
|
||||
|
||||
ErrorType :: enum EnumBackingType {
|
||||
TextMeasurementFunctionNotProvided,
|
||||
ArenaCapacityExceeded,
|
||||
ElementsCapacityExceeded,
|
||||
TextMeasurementCapacityExceeded,
|
||||
DuplicateId,
|
||||
FloatingContainerParentNotFound,
|
||||
PercentageOver1,
|
||||
InternalError,
|
||||
UnbalancedOpenClose,
|
||||
}
|
||||
|
||||
ErrorData :: struct {
|
||||
errorType: ErrorType,
|
||||
errorText: String,
|
||||
userData: rawptr,
|
||||
}
|
||||
|
||||
ErrorHandler :: struct {
|
||||
handler: proc "c" (errorData: ErrorData),
|
||||
userData: rawptr,
|
||||
}
|
||||
|
||||
Context :: struct {} // opaque structure, only use as a pointer
|
||||
|
||||
@(link_prefix = "Clay_", default_calling_convention = "c")
|
||||
foreign Clay {
|
||||
_OpenElement :: proc() ---
|
||||
_OpenElementWithId :: proc(id: ElementId) ---
|
||||
_CloseElement :: proc() ---
|
||||
MinMemorySize :: proc() -> u32 ---
|
||||
CreateArenaWithCapacityAndMemory :: proc(capacity: c.size_t, offset: [^]u8) -> Arena ---
|
||||
SetPointerState :: proc(position: Vector2, pointerDown: bool) ---
|
||||
GetPointerState :: proc() -> PointerData ---
|
||||
Initialize :: proc(arena: Arena, layoutDimensions: Dimensions, errorHandler: ErrorHandler) -> ^Context ---
|
||||
GetCurrentContext :: proc() -> ^Context ---
|
||||
SetCurrentContext :: proc(ctx: ^Context) ---
|
||||
UpdateScrollContainers :: proc(enableDragScrolling: bool, scrollDelta: Vector2, deltaTime: c.float) ---
|
||||
SetLayoutDimensions :: proc(dimensions: Dimensions) ---
|
||||
BeginLayout :: proc() ---
|
||||
EndLayout :: proc(deltaTime: c.float) -> ClayArray(RenderCommand) ---
|
||||
GetOpenElementId :: proc() -> u32 ---
|
||||
GetElementId :: proc(id: String) -> ElementId ---
|
||||
GetElementIdWithIndex :: proc(id: String, index: u32) -> ElementId ---
|
||||
GetElementData :: proc(id: ElementId) -> ElementData ---
|
||||
Hovered :: proc() -> bool ---
|
||||
OnHover :: proc(onHoverFunction: proc "c" (id: ElementId, pointerData: PointerData, userData: rawptr), userData: rawptr) ---
|
||||
PointerOver :: proc(id: ElementId) -> bool ---
|
||||
GetPointerOverIds :: proc() -> ElementIdArray ---
|
||||
GetScrollOffset :: proc() -> Vector2 ---
|
||||
GetScrollContainerData :: proc(id: ElementId) -> ScrollContainerData ---
|
||||
SetMeasureTextFunction :: proc(measureTextFunction: proc "c" (text: StringSlice, config: ^TextElementConfig, userData: rawptr) -> Dimensions, userData: rawptr) ---
|
||||
SetQueryScrollOffsetFunction :: proc(queryScrollOffsetFunction: proc "c" (elementId: u32, userData: rawptr) -> Vector2, userData: rawptr) ---
|
||||
RenderCommandArray_Get :: proc(array: ^ClayArray(RenderCommand), index: i32) -> ^RenderCommand ---
|
||||
SetDebugModeEnabled :: proc(enabled: bool) ---
|
||||
IsDebugModeEnabled :: proc() -> bool ---
|
||||
SetCullingEnabled :: proc(enabled: bool) ---
|
||||
GetMaxElementCount :: proc() -> i32 ---
|
||||
SetMaxElementCount :: proc(maxElementCount: i32) ---
|
||||
GetMaxMeasureTextCacheWordCount :: proc() -> i32 ---
|
||||
SetMaxMeasureTextCacheWordCount :: proc(maxMeasureTextCacheWordCount: i32) ---
|
||||
ResetMeasureTextCache :: proc() ---
|
||||
EaseOut :: proc(arguments: TransitionCallbackArguments) -> bool ---
|
||||
}
|
||||
|
||||
@(link_prefix = "Clay_", default_calling_convention = "c", private)
|
||||
foreign Clay {
|
||||
_ConfigureOpenElement :: proc(config: ElementDeclaration) ---
|
||||
_HashString :: proc(key: String, seed: u32) -> ElementId ---
|
||||
_HashStringWithOffset :: proc(key: String, index: u32, seed: u32) -> ElementId ---
|
||||
_OpenTextElement :: proc(text: String, textConfig: TextElementConfig) ---
|
||||
}
|
||||
|
||||
ConfigureOpenElement :: proc(config: ElementDeclaration) -> bool {
|
||||
_ConfigureOpenElement(config)
|
||||
return true
|
||||
}
|
||||
|
||||
@(deferred_none = _CloseElement)
|
||||
UI_WithId :: proc(id: ElementId) -> proc(config: ElementDeclaration) -> bool {
|
||||
_OpenElementWithId(id)
|
||||
return ConfigureOpenElement
|
||||
}
|
||||
|
||||
@(deferred_none = _CloseElement)
|
||||
UI_AutoId :: proc() -> proc(config: ElementDeclaration) -> bool {
|
||||
_OpenElement()
|
||||
return ConfigureOpenElement
|
||||
}
|
||||
|
||||
UI :: proc {
|
||||
UI_WithId,
|
||||
UI_AutoId,
|
||||
}
|
||||
|
||||
Text :: proc {
|
||||
TextStatic,
|
||||
TextDynamic,
|
||||
}
|
||||
|
||||
TextStatic :: proc($text: string, config: TextElementConfig) {
|
||||
wrapped := MakeString(text)
|
||||
wrapped.isStaticallyAllocated = true
|
||||
_OpenTextElement(wrapped, config)
|
||||
}
|
||||
|
||||
TextDynamic :: proc(text: string, config: TextElementConfig) {
|
||||
_OpenTextElement(MakeString(text), config)
|
||||
}
|
||||
|
||||
PaddingAll :: proc(allPadding: u16) -> Padding {
|
||||
return {left = allPadding, right = allPadding, top = allPadding, bottom = allPadding}
|
||||
}
|
||||
|
||||
BorderOutside :: proc(width: u16) -> BorderWidth {
|
||||
return {width, width, width, width, 0}
|
||||
}
|
||||
|
||||
BorderAll :: proc(width: u16) -> BorderWidth {
|
||||
return {width, width, width, width, width}
|
||||
}
|
||||
|
||||
CornerRadiusAll :: proc(radius: f32) -> CornerRadius {
|
||||
return CornerRadius{radius, radius, radius, radius}
|
||||
}
|
||||
|
||||
SizingFit :: proc(sizeMinMax: SizingConstraintsMinMax = {}) -> SizingAxis {
|
||||
return SizingAxis{type = SizingType.Fit, constraints = {sizeMinMax = sizeMinMax}}
|
||||
}
|
||||
|
||||
SizingGrow :: proc(sizeMinMax: SizingConstraintsMinMax = {}) -> SizingAxis {
|
||||
return SizingAxis{type = SizingType.Grow, constraints = {sizeMinMax = sizeMinMax}}
|
||||
}
|
||||
|
||||
SizingFixed :: proc(size: c.float) -> SizingAxis {
|
||||
return SizingAxis{type = SizingType.Fixed, constraints = {sizeMinMax = {size, size}}}
|
||||
}
|
||||
|
||||
SizingPercent :: proc(sizePercent: c.float) -> SizingAxis {
|
||||
return SizingAxis{type = SizingType.Percent, constraints = {sizePercent = sizePercent}}
|
||||
}
|
||||
|
||||
MakeString :: proc(label: string) -> String {
|
||||
return String{chars = raw_data(label), length = cast(c.int)len(label)}
|
||||
}
|
||||
|
||||
ID :: proc(label: string, index: u32 = 0) -> ElementId {
|
||||
return _HashString(MakeString(label), index)
|
||||
}
|
||||
|
||||
ID_LOCAL :: proc(label: string, index: u32 = 0) -> ElementId {
|
||||
return _HashStringWithOffset(MakeString(label), index, GetOpenElementId())
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+17
-16
@@ -3,30 +3,31 @@ package game
|
||||
import rl "vendor:raylib"
|
||||
|
||||
run :: proc() {
|
||||
rl.InitWindow(SCREEN_W, SCREEN_H, "Tower Defense")
|
||||
defer rl.CloseWindow()
|
||||
rl.SetTargetFPS(60)
|
||||
|
||||
init_ui()
|
||||
defer destroy_ui()
|
||||
init_display()
|
||||
defer {
|
||||
shutdown_display()
|
||||
rl.CloseWindow()
|
||||
}
|
||||
|
||||
world := init_world()
|
||||
defer {
|
||||
delete(world.path)
|
||||
delete(world.towers)
|
||||
delete(world.events)
|
||||
delete(world.spawner.recipe)
|
||||
}
|
||||
|
||||
for !rl.WindowShouldClose() {
|
||||
dt := rl.GetFrameTime()
|
||||
|
||||
ui_prepare_frame(&world, dt)
|
||||
update_display()
|
||||
update_world(&world, dt)
|
||||
|
||||
rl.BeginDrawing()
|
||||
rl.ClearBackground(rl.Color{34, 139, 34, 255})
|
||||
|
||||
begin_game_draw()
|
||||
render_world(&world)
|
||||
render_ui()
|
||||
rl.EndDrawing()
|
||||
render_overlay(&world)
|
||||
render_controls(&world)
|
||||
end_game_draw()
|
||||
}
|
||||
|
||||
delete(world.path)
|
||||
delete(world.towers)
|
||||
delete(world.events)
|
||||
}
|
||||
|
||||
|
||||
+15
-6
@@ -6,6 +6,7 @@ Command_Kind :: enum {
|
||||
None,
|
||||
Place_Tower,
|
||||
Start_Wave,
|
||||
Upgrade_Tower,
|
||||
}
|
||||
|
||||
Command :: struct {
|
||||
@@ -15,18 +16,24 @@ Command :: struct {
|
||||
}
|
||||
|
||||
gather_commands :: proc(world: ^World) -> Command {
|
||||
ui_cmd := poll_controls_command(world)
|
||||
if ui_cmd.kind != .None do return ui_cmd
|
||||
|
||||
if rl.IsKeyPressed(.ONE) do world.selected_tower = .Archer
|
||||
if rl.IsKeyPressed(.TWO) do world.selected_tower = .Cannon
|
||||
if rl.IsKeyPressed(.THREE) do world.selected_tower = .Ice
|
||||
if world.phase == .Build && rl.IsKeyPressed(.N) {
|
||||
return Command{kind = .Start_Wave}
|
||||
}
|
||||
|
||||
if world.phase == .Build && rl.IsMouseButtonPressed(.LEFT) {
|
||||
mouse := rl.GetMousePosition()
|
||||
if rl.IsMouseButtonPressed(.RIGHT) {
|
||||
mouse := game_mouse()
|
||||
gx, gy := screen_to_grid(mouse.x, mouse.y)
|
||||
return Command{kind = .Place_Tower, gx = gx, gy = gy}
|
||||
return Command{kind = .Upgrade_Tower, gx = gx, gy = gy}
|
||||
}
|
||||
|
||||
if rl.IsMouseButtonPressed(.LEFT) {
|
||||
mouse := rl.GetMousePosition()
|
||||
if world.phase == .Build && rl.IsMouseButtonPressed(.LEFT) {
|
||||
mouse := game_mouse()
|
||||
gx, gy := screen_to_grid(mouse.x, mouse.y)
|
||||
return Command{kind = .Place_Tower, gx = gx, gy = gy}
|
||||
}
|
||||
@@ -38,9 +45,11 @@ execute_command :: proc(world: ^World, cmd: Command) {
|
||||
switch cmd.kind {
|
||||
case .None:
|
||||
case .Place_Tower:
|
||||
try_place_tower(world, cmd.gx, cmd.gy, .Archer)
|
||||
try_place_tower(world, cmd.gx, cmd.gy, world.selected_tower)
|
||||
case .Start_Wave:
|
||||
start_wave(world)
|
||||
case .Upgrade_Tower:
|
||||
try_upgrade_tower(world, cmd.gx, cmd.gy)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+54
-2
@@ -1,5 +1,7 @@
|
||||
package game
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
MAP_W :: 30 // map width in tiles
|
||||
MAP_H :: 20 // map height in tiles
|
||||
TILE_SIZE :: 32 // size of tiles in pixels
|
||||
@@ -7,13 +9,63 @@ TILE_SIZE :: 32 // size of tiles in pixels
|
||||
SCREEN_W :: MAP_W * TILE_SIZE
|
||||
SCREEN_H :: MAP_H * TILE_SIZE
|
||||
|
||||
DEFAULT_WINDOW_W :: 1280
|
||||
DEFAULT_WINDOW_H :: 720
|
||||
|
||||
MAX_ENEMIES :: 64
|
||||
MAX_WAVES :: 5
|
||||
MAX_TOWERS :: 32
|
||||
|
||||
TOWER_COST :: 50
|
||||
|
||||
MAX_PROJECTILES :: 64
|
||||
PROJECTILE_SPEED :: 280 // pixels per second
|
||||
PROJECTILE_HIT_RADIUS :: 10
|
||||
CANNON_PROJECTILES_SPEED :: 320.0
|
||||
|
||||
OVERLAY_BAR_H :: 28
|
||||
OVERLAY_PADDING :: 6
|
||||
OVERLAY_BAR_COLOR :: rl.Color{20, 24, 20, 200}
|
||||
OVERLAY_GOLD_COLOR :: rl.Color{255, 215, 80, 255}
|
||||
OVERLAY_TEXT_COLOR :: rl.Color{240, 240, 230, 255}
|
||||
OVERLAY_FONT_SIZE :: 18
|
||||
|
||||
CONTROL_BAR_H :: 40
|
||||
CONTROL_BAR_BG :: rl.Color{30, 55, 30, 220}
|
||||
CONTROL_BUTTON_COLOR :: rl.Color{60, 120, 60, 255}
|
||||
CONTROL_BUTTON_HOVER :: rl.Color{60, 150, 60, 255}
|
||||
|
||||
CONTROLS_SHOP_IDLE :: rl.Color{45, 75, 45, 255}
|
||||
CONTROL_SHOP_SELECTED :: rl.Color{70, 110, 70, 255}
|
||||
CONTROLS_SHOP_HOVER :: rl.Color{90, 130, 90, 255}
|
||||
|
||||
ICE_SLOW_DURATION :: 2.5
|
||||
ICE_SLOW_FACTOR :: 0.5
|
||||
|
||||
ENEMY_LABEL_COLOR :: rl.Color{255, 200, 200, 255}
|
||||
|
||||
// starting values for the game
|
||||
STARTING_BASE_HEALTH :: 20
|
||||
STARTING_GOLD :: 100
|
||||
|
||||
//controls constants
|
||||
BOTTOM_BAR_Y :: SCREEN_H - CONTROL_BAR_H
|
||||
ARCHER_BUTTON_W :: 120
|
||||
START_WAVE_BUTTON_W :: 110
|
||||
CONTROLS_BUTTON_GAP :: 12
|
||||
CONTROLS_BUTTON_H :: 24
|
||||
CONTROLS_BUTTON_Y :: BOTTOM_BAR_Y + 8
|
||||
CONTROLS_BUTTON_X :: (SCREEN_W - (ARCHER_BUTTON_W + CONTROLS_BUTTON_GAP + START_WAVE_BUTTON_W)) / 2
|
||||
|
||||
ARCHER_BUTTON_RECT :: rl.Rectangle {
|
||||
f32(CONTROLS_BUTTON_X),
|
||||
f32(CONTROLS_BUTTON_Y),
|
||||
ARCHER_BUTTON_W,
|
||||
CONTROLS_BUTTON_H,
|
||||
}
|
||||
|
||||
START_WAVE_BUTTON_RECT :: rl.Rectangle {
|
||||
f32(CONTROLS_BUTTON_X + ARCHER_BUTTON_W + CONTROLS_BUTTON_GAP),
|
||||
f32(CONTROLS_BUTTON_Y),
|
||||
START_WAVE_BUTTON_W,
|
||||
CONTROLS_BUTTON_H,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
|
||||
poll_controls_command :: proc(world: ^World) -> Command {
|
||||
if world.phase != .Build do return Command{kind = .None}
|
||||
|
||||
mouse := game_mouse()
|
||||
if rl.IsMouseButtonPressed(.LEFT) {
|
||||
if rl.CheckCollisionPointRec(mouse, ARCHER_BUTTON_RECT) {
|
||||
world.selected_tower = .Archer
|
||||
}
|
||||
if rl.CheckCollisionPointRec(mouse, START_WAVE_BUTTON_RECT) {
|
||||
return Command{kind = .Start_Wave}
|
||||
}
|
||||
}
|
||||
|
||||
return Command{kind = .None}
|
||||
}
|
||||
|
||||
draw_archer_button :: proc(world: ^World, archer: rl.Rectangle) {
|
||||
arch := TOWER_ARCHETYPES[.Archer]
|
||||
mouse := rl.GetMousePosition()
|
||||
|
||||
arch_bg := CONTROLS_SHOP_IDLE
|
||||
if world.selected_tower == .Archer do arch_bg = CONTROL_SHOP_SELECTED
|
||||
if rl.CheckCollisionPointRec(mouse, archer) do arch_bg = CONTROLS_SHOP_HOVER
|
||||
|
||||
label := fmt.ctprintf("Archer - %dg", arch.cost)
|
||||
draw_button(archer, label, arch_bg, 14, OVERLAY_TEXT_COLOR)
|
||||
}
|
||||
|
||||
draw_start_wave_button :: proc(start_wave_rect: rl.Rectangle, mouse: rl.Vector2) {
|
||||
bg := CONTROL_BUTTON_COLOR
|
||||
if rl.CheckCollisionPointRec(mouse, start_wave_rect) do bg = CONTROL_BUTTON_HOVER
|
||||
|
||||
draw_button(start_wave_rect, "Start Wave", bg, 16, OVERLAY_TEXT_COLOR)
|
||||
}
|
||||
|
||||
draw_build_controls :: proc(world: ^World) {
|
||||
if world.phase != .Build do return
|
||||
mouse := game_mouse()
|
||||
|
||||
rl.DrawRectangle(0, BOTTOM_BAR_Y, SCREEN_W, CONTROL_BAR_H, CONTROL_BAR_BG)
|
||||
|
||||
draw_archer_button(world, ARCHER_BUTTON_RECT)
|
||||
draw_start_wave_button(START_WAVE_BUTTON_RECT, mouse)
|
||||
}
|
||||
|
||||
render_controls :: proc(world: ^World) {
|
||||
switch world.phase {
|
||||
case .Build:
|
||||
draw_build_controls(world)
|
||||
case .Combat:
|
||||
draw_combat_banner()
|
||||
case .Game_Over, .Victory:
|
||||
}
|
||||
}
|
||||
|
||||
draw_combat_banner :: proc() {
|
||||
rl.DrawRectangle(0, BOTTOM_BAR_Y, SCREEN_W, CONTROL_BAR_H, rl.Color{120, 40, 40, 200})
|
||||
|
||||
text: cstring = "Combat!"
|
||||
font: i32 = 16
|
||||
tw := rl.MeasureText(text, font)
|
||||
rl.DrawText(
|
||||
text,
|
||||
(SCREEN_W - tw) / 2,
|
||||
BOTTOM_BAR_Y + (CONTROL_BAR_H - font) / 2,
|
||||
font,
|
||||
rl.Color{255, 220, 220, 255},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package game
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
game_target: rl.RenderTexture2D
|
||||
|
||||
init_display :: proc() {
|
||||
rl.InitWindow(DEFAULT_WINDOW_W, DEFAULT_WINDOW_H, "Tower Defense")
|
||||
rl.SetWindowState({.WINDOW_RESIZABLE})
|
||||
rl.SetTargetFPS(60)
|
||||
game_target = rl.LoadRenderTexture(i32(SCREEN_W), i32(SCREEN_H))
|
||||
}
|
||||
|
||||
shutdown_display :: proc() {
|
||||
rl.UnloadRenderTexture(game_target)
|
||||
}
|
||||
|
||||
update_display :: proc() {
|
||||
if rl.IsKeyPressed(.F11) {
|
||||
rl.ToggleFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
game_viewport_dest :: proc() -> rl.Rectangle {
|
||||
sw := f32(rl.GetRenderWidth())
|
||||
sh := f32(rl.GetRenderHeight())
|
||||
scale := min(sw / f32(SCREEN_W), sh / f32(SCREEN_H))
|
||||
dw := f32(SCREEN_W) * scale
|
||||
dh := f32(SCREEN_H) * scale
|
||||
return {x = (sw - dw) * 0.5, y = (sh - dh) * 0.5, width = dw, height = dh}
|
||||
}
|
||||
|
||||
game_mouse :: proc() -> rl.Vector2 {
|
||||
m := rl.GetMousePosition()
|
||||
dest := game_viewport_dest()
|
||||
if dest.width <= 0 || dest.height <= 0 {
|
||||
return m
|
||||
}
|
||||
return {
|
||||
(m.x - dest.x) / dest.width * f32(SCREEN_W),
|
||||
(m.y - dest.y) / dest.height * f32(SCREEN_H),
|
||||
}
|
||||
}
|
||||
|
||||
begin_game_draw :: proc() {
|
||||
rl.BeginTextureMode(game_target)
|
||||
rl.ClearBackground(rl.Color{34, 139, 34, 255})
|
||||
}
|
||||
|
||||
end_game_draw :: proc() {
|
||||
rl.EndTextureMode()
|
||||
|
||||
rl.BeginDrawing()
|
||||
rl.ClearBackground(rl.BLACK)
|
||||
|
||||
dest := game_viewport_dest()
|
||||
source := rl.Rectangle{0, 0, f32(SCREEN_W), -f32(SCREEN_H)}
|
||||
rl.DrawTexturePro(game_target.texture, source, dest, {}, 0, rl.WHITE)
|
||||
|
||||
rl.EndDrawing()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
draw_end_screen :: proc(world: ^World) {
|
||||
title: cstring = "GAME OVER"
|
||||
title_color := rl.RED
|
||||
sub: cstring = "Press close to quit"
|
||||
sub_color := OVERLAY_TEXT_COLOR
|
||||
|
||||
if world.phase == .Victory {
|
||||
title = "VICTORY!"
|
||||
title_color = rl.GREEN
|
||||
sub = fmt.ctprintf("You survived %d waves!", MAX_WAVES)
|
||||
}
|
||||
|
||||
|
||||
rl.DrawRectangle(0, 0, SCREEN_W, SCREEN_H, rl.Color{0, 0, 0, 140})
|
||||
|
||||
title_font: i32 = 40
|
||||
tw := rl.MeasureText(title, title_font)
|
||||
rl.DrawText(title, (SCREEN_W - tw) / 2, SCREEN_H / 2 - 50, title_font, title_color)
|
||||
|
||||
sub_font: i32 = 18
|
||||
sw := rl.MeasureText(sub, sub_font)
|
||||
rl.DrawText(sub, (SCREEN_W - sw) / 2, SCREEN_H / 2 + 10, sub_font, sub_color)
|
||||
}
|
||||
|
||||
+71
-13
@@ -1,6 +1,7 @@
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import "core:sys/posix"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Enemy :: struct {
|
||||
@@ -9,7 +10,55 @@ Enemy :: struct {
|
||||
max_health: int,
|
||||
active: bool,
|
||||
path_index: int,
|
||||
speed: f32,
|
||||
base_speed: f32,
|
||||
slow_timer: f32,
|
||||
kind: Enemy_Kind,
|
||||
}
|
||||
|
||||
Enemy_Kind :: enum {
|
||||
Grunt,
|
||||
Runner,
|
||||
Tank,
|
||||
}
|
||||
|
||||
Enemy_Def :: struct {
|
||||
base_health: int,
|
||||
health_per_wave: int,
|
||||
base_speed: f32,
|
||||
speed_per_wave: f32,
|
||||
color: rl.Color,
|
||||
draw_radius: f32,
|
||||
gold_reward: int,
|
||||
}
|
||||
|
||||
ENEMY_DEF: [Enemy_Kind]Enemy_Def = {
|
||||
.Grunt = {
|
||||
base_health = 15,
|
||||
health_per_wave = 5,
|
||||
base_speed = 55,
|
||||
speed_per_wave = 5,
|
||||
color = rl.MAROON,
|
||||
draw_radius = 11,
|
||||
gold_reward = 5,
|
||||
},
|
||||
.Runner = {
|
||||
base_health = 10,
|
||||
health_per_wave = 2,
|
||||
base_speed = 95,
|
||||
speed_per_wave = 3,
|
||||
color = rl.ORANGE,
|
||||
draw_radius = 11,
|
||||
gold_reward = 5,
|
||||
},
|
||||
.Tank = {
|
||||
base_health = 50,
|
||||
health_per_wave = 12,
|
||||
base_speed = 35,
|
||||
speed_per_wave = 2,
|
||||
color = rl.DARKGREEN,
|
||||
draw_radius = 14,
|
||||
gold_reward = 10,
|
||||
},
|
||||
}
|
||||
|
||||
acquire_enemy :: proc(world: ^World) -> ^Enemy {
|
||||
@@ -23,35 +72,34 @@ acquire_enemy :: proc(world: ^World) -> ^Enemy {
|
||||
return nil
|
||||
}
|
||||
|
||||
spawn_enemy :: proc(world: ^World, health: int, speed: f32) {
|
||||
spawn_enemy :: proc(world: ^World, kind: Enemy_Kind) {
|
||||
if len(world.path) == 0 do return // don't spawn enemies if no paths exist
|
||||
|
||||
e := acquire_enemy(world) // grab a free slot
|
||||
if e == nil do return // if there are no slots, skil spawning
|
||||
|
||||
def := ENEMY_DEF[kind]
|
||||
health := def.base_health + world.wave * def.health_per_wave
|
||||
speed := def.base_speed + f32(world.wave) * def.speed_per_wave
|
||||
e^ = Enemy {
|
||||
position = world.path[0],
|
||||
health = health,
|
||||
max_health = health,
|
||||
speed = speed,
|
||||
base_speed = speed,
|
||||
slow_timer = 0,
|
||||
path_index = 1,
|
||||
kind = kind,
|
||||
active = true,
|
||||
}
|
||||
}
|
||||
|
||||
spawn_group :: proc(world: ^World, count: int) {
|
||||
for _ in 0 ..< count {
|
||||
spawn_enemy(world, 20, 60)
|
||||
}
|
||||
}
|
||||
|
||||
update_enemies :: proc(world: ^World, dt: f32) {
|
||||
if len(world.path) < 2 do return
|
||||
|
||||
for i in 0 ..< MAX_ENEMIES {
|
||||
e := &world.enemies[i]
|
||||
if !e.active do continue
|
||||
|
||||
if len(world.path) == 2 do return
|
||||
|
||||
if e.path_index >= len(world.path) {
|
||||
world.base_health -= 1
|
||||
e.active = false
|
||||
@@ -59,7 +107,12 @@ update_enemies :: proc(world: ^World, dt: f32) {
|
||||
}
|
||||
|
||||
target := world.path[e.path_index]
|
||||
move_toward(&e.position, target, e.speed, dt)
|
||||
move_speed := e.base_speed
|
||||
if e.slow_timer > 0 {
|
||||
move_speed *= ICE_SLOW_FACTOR
|
||||
e.slow_timer -= dt
|
||||
}
|
||||
move_toward(&e.position, target, move_speed, dt)
|
||||
|
||||
dx := target.x - e.position.x
|
||||
dy := target.y - e.position.y
|
||||
@@ -82,7 +135,12 @@ render_enemies :: proc(world: ^World) {
|
||||
for i in 0 ..< MAX_ENEMIES {
|
||||
e := world.enemies[i]
|
||||
if !e.active do continue
|
||||
rl.DrawCircle(i32(e.position.x), i32(e.position.y), 11, rl.MAROON)
|
||||
def := ENEMY_DEF[e.kind]
|
||||
|
||||
rl.DrawCircle(i32(e.position.x), i32(e.position.y), def.draw_radius, def.color)
|
||||
if e.slow_timer > 0 {
|
||||
rl.DrawCircleLines(i32(e.position.x), i32(e.position.y), 14, rl.SKYBLUE)
|
||||
}
|
||||
rl.DrawText(
|
||||
fmt.ctprintf("%d", e.health),
|
||||
i32(e.position.x) - 4,
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package game
|
||||
|
||||
import clay "../clay-odin"
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
UI_BAR_COLOR :: clay.Color{20, 40, 20, 200}
|
||||
UI_TEXT_COLOR :: clay.Color{240, 240, 230, 255}
|
||||
UI_GOLD_COLOR :: clay.Color{255, 215, 80, 255}
|
||||
|
||||
ui_text_scratch: [64]u8
|
||||
|
||||
build_hud :: proc(world: ^World) {
|
||||
if clay.UI(clay.ID("HudBar"))(
|
||||
{
|
||||
layout = {
|
||||
layoutDirection = .LeftToRight,
|
||||
sizing = {width = clay.SizingGrow({}), height = clay.SizingFixed(28)},
|
||||
padding = clay.PaddingAll(6),
|
||||
childGap = 8,
|
||||
childAlignment = {y = .Center},
|
||||
},
|
||||
backgroundColor = UI_BAR_COLOR,
|
||||
},
|
||||
) {
|
||||
gold_text := fmt.bprintf(ui_text_scratch[:], "Gold: %d", world.gold)
|
||||
clay.Text(
|
||||
gold_text,
|
||||
clay.TextElementConfig{textColor = UI_GOLD_COLOR, fontId = UI_FONT_ID, fontSize = 18},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render_hud :: proc(world: ^World) {
|
||||
rl.DrawText(fmt.ctprintf("Base HP: %d", world.base_health), 10, 32, 22, rl.BLACK)
|
||||
rl.DrawText(fmt.ctprintf("Enemies: %d", active_enemy_count(world)), 10, 54, 22, rl.BLACK)
|
||||
rl.DrawText(fmt.ctprintf("Wave: %d / %d", world.wave, MAX_WAVES), 10, 76, 22, rl.BLACK)
|
||||
|
||||
switch world.phase {
|
||||
case .Build:
|
||||
rl.DrawText("Build - click towers, N = start wave", 10, 98, 22, rl.BLACK)
|
||||
case .Combat:
|
||||
rl.DrawText("Combat!", 10, 98, 22, rl.MAROON)
|
||||
case .Game_Over:
|
||||
rl.DrawText("GAME_OVER", 260, 280, 40, rl.RED)
|
||||
case .Victory:
|
||||
rl.DrawText("Victory!", 260, 280, 40, rl.GREEN)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-12
@@ -11,17 +11,10 @@ Map :: struct {
|
||||
tiles: [MAP_H][MAP_W]Tile_Kind,
|
||||
}
|
||||
|
||||
tile_color :: proc(kind: Tile_Kind) -> rl.Color {
|
||||
switch kind {
|
||||
case .Blocked:
|
||||
return {35, 95, 35, 255}
|
||||
case .Path:
|
||||
return {130, 85, 45, 255}
|
||||
case .Build:
|
||||
return {50, 130, 50, 255}
|
||||
}
|
||||
|
||||
return rl.MAGENTA
|
||||
TILE_COLORS: [Tile_Kind]rl.Color = {
|
||||
.Blocked = {35, 95, 35, 255},
|
||||
.Path = {130, 85, 45, 255},
|
||||
.Build = {50, 130, 50, 255},
|
||||
}
|
||||
|
||||
init_map :: proc(world_map: ^Map) {
|
||||
@@ -57,7 +50,7 @@ can_build_at :: proc(world_map: ^Map, gx, gy: int) -> bool {
|
||||
render_map :: proc(world: ^World) {
|
||||
for y in 0 ..< MAP_H {
|
||||
for x in 0 ..< MAP_W {
|
||||
c := tile_color(world.world_map.tiles[y][x])
|
||||
c := TILE_COLORS[world.world_map.tiles[y][x]]
|
||||
rl.DrawRectangle(i32(x * TILE_SIZE), i32(y * TILE_SIZE), TILE_SIZE, TILE_SIZE, c)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ screen_to_grid :: proc(px, py: f32) -> (int, int) {
|
||||
distance :: proc(a, b: Vec2) -> f32 {
|
||||
dx := b.x - a.x
|
||||
dy := b.y - a.y
|
||||
return math.sqrt(dx * dx + dy + dy)
|
||||
return math.sqrt(dx * dx + dy * dy)
|
||||
}
|
||||
|
||||
move_toward :: proc(pos: ^Vec2, target: Vec2, speed, dt: f32) {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package game
|
||||
|
||||
import "core:math"
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_distance :: proc(t: ^testing.T) {
|
||||
a := Vec2 {
|
||||
x = 3,
|
||||
y = 2,
|
||||
}
|
||||
b := Vec2 {
|
||||
x = 1,
|
||||
y = 2,
|
||||
}
|
||||
|
||||
d := distance(a, b)
|
||||
|
||||
testing.expect(t, math.abs(d - 2.0) < 0.0001)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_move_toward_already_close :: proc(t: ^testing.T) {
|
||||
pos := Vec2{0.5, 0}
|
||||
target := Vec2{0, 0}
|
||||
|
||||
move_toward(&pos, target, 10, 1.0)
|
||||
|
||||
testing.expect(t, math.abs(pos.x - 0.5) < 0.0001)
|
||||
testing.expect(t, math.abs(pos.y - 0.0) < 0.0001)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_move_toward_reaches_target :: proc(t: ^testing.T) {
|
||||
pos := Vec2{0, 0}
|
||||
target := Vec2{3, 4}
|
||||
|
||||
move_toward(&pos, target, 100, 1.0)
|
||||
|
||||
testing.expect(t, math.abs(pos.x - 3) < 0.0001)
|
||||
testing.expect(t, math.abs(pos.y - 4) < 0.0001)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_move_toward_partial :: proc(t: ^testing.T) {
|
||||
pos := Vec2{0, 0}
|
||||
target := Vec2{10, 0}
|
||||
|
||||
move_toward(&pos, target, 2, 1.0)
|
||||
|
||||
testing.expect(t, math.abs(pos.x - 2) < 0.0001)
|
||||
testing.expect(t, math.abs(pos.y) < 0.0001)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_move_toward_diagonal :: proc(t: ^testing.T) {
|
||||
pos := Vec2{0, 0}
|
||||
target := Vec2{3, 4}
|
||||
|
||||
move_toward(&pos, target, 5, 0.5)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
render_overlay :: proc(world: ^World) {
|
||||
rl.DrawRectangle(0, 0, SCREEN_W, OVERLAY_BAR_H, OVERLAY_BAR_COLOR)
|
||||
|
||||
y: i32 = (OVERLAY_BAR_H - OVERLAY_FONT_SIZE) / 2
|
||||
x: i32 = OVERLAY_PADDING
|
||||
gap: i32 : 16
|
||||
|
||||
|
||||
gold := fmt.ctprintf("Gold: %d", world.gold)
|
||||
rl.DrawText(gold, OVERLAY_PADDING, OVERLAY_PADDING, 18, OVERLAY_GOLD_COLOR)
|
||||
x += rl.MeasureText(gold, OVERLAY_FONT_SIZE) + gap
|
||||
|
||||
hp := fmt.ctprintf("Health: %d", world.base_health)
|
||||
rl.DrawText(hp, x, y, OVERLAY_FONT_SIZE, OVERLAY_TEXT_COLOR)
|
||||
x += rl.MeasureText(hp, OVERLAY_FONT_SIZE) + gap
|
||||
|
||||
enemies := fmt.ctprintf("Enemies: %d", active_enemy_count(world))
|
||||
rl.DrawText(enemies, x, y, OVERLAY_FONT_SIZE, ENEMY_LABEL_COLOR)
|
||||
x += rl.MeasureText(enemies, OVERLAY_FONT_SIZE) + gap
|
||||
|
||||
waves := fmt.ctprintf("Waves: %d / %d", world.wave, MAX_WAVES)
|
||||
rl.DrawText(waves, x, y, OVERLAY_FONT_SIZE, OVERLAY_TEXT_COLOR)
|
||||
|
||||
if world.phase == .Game_Over || world.phase == .Victory {
|
||||
draw_end_screen(world)
|
||||
}
|
||||
}
|
||||
|
||||
+104
-24
@@ -3,11 +3,19 @@ package game
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Projectile :: struct {
|
||||
active: bool,
|
||||
position: Vec2,
|
||||
target_slot: int,
|
||||
damage: int,
|
||||
speed: f32,
|
||||
active: bool,
|
||||
position: Vec2,
|
||||
target_slot: int,
|
||||
damage: int,
|
||||
speed: f32,
|
||||
mode: Projectile_Mode,
|
||||
direction: Vec2,
|
||||
splash_radius: f32,
|
||||
}
|
||||
|
||||
Projectile_Mode :: enum {
|
||||
Homing, // chases target
|
||||
Ballistic, // flies in a fixed direction
|
||||
}
|
||||
|
||||
acquire_projectile :: proc(world: ^World) -> ^Projectile {
|
||||
@@ -33,37 +41,87 @@ spawn_projectile :: proc(world: ^World, from: Vec2, target_slot: int, damage: in
|
||||
p := acquire_projectile(world)
|
||||
if p == nil do return
|
||||
|
||||
p.mode = .Homing
|
||||
p^ = Projectile {
|
||||
active = true,
|
||||
position = from,
|
||||
target_slot = target_slot,
|
||||
damage = damage,
|
||||
speed = PROJECTILE_SPEED,
|
||||
active = true,
|
||||
position = from,
|
||||
target_slot = target_slot,
|
||||
damage = damage,
|
||||
speed = PROJECTILE_SPEED,
|
||||
splash_radius = 0,
|
||||
}
|
||||
}
|
||||
|
||||
spawn_ballistic :: proc(world: ^World, from, dir: Vec2, damage: int, splash: f32) {
|
||||
p := acquire_projectile(world)
|
||||
if p == nil do return
|
||||
p^ = Projectile {
|
||||
active = true,
|
||||
position = from,
|
||||
direction = dir,
|
||||
mode = .Ballistic,
|
||||
damage = damage,
|
||||
speed = CANNON_PROJECTILES_SPEED,
|
||||
splash_radius = splash,
|
||||
}
|
||||
}
|
||||
|
||||
hit_enemy_at :: proc(world: ^World, pos: Vec2, damage: int) -> bool {
|
||||
for i in 0 ..< MAX_ENEMIES {
|
||||
e := &world.enemies[i]
|
||||
if !e.active do continue
|
||||
if distance(pos, e.position) <= PROJECTILE_HIT_RADIUS {
|
||||
apply_tower_hit(world, e, damage)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
update_projectiles :: proc(world: ^World, dt: f32) {
|
||||
for i in 0 ..< MAX_PROJECTILES {
|
||||
p := &world.projectiles[i]
|
||||
if !p.active do continue
|
||||
|
||||
target, ok := enemy_still_on_map(world, p.target_slot)
|
||||
if !ok {
|
||||
p.active = false
|
||||
continue
|
||||
switch p.mode {
|
||||
case .Homing:
|
||||
update_homing_projectile(world, p, dt)
|
||||
case .Ballistic:
|
||||
update_ballistic_projectile(world, p, dt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
move_toward(&p.position, target.position, p.speed, dt)
|
||||
update_homing_projectile :: proc(world: ^World, p: ^Projectile, dt: f32) {
|
||||
target, ok := enemy_still_on_map(world, p.target_slot)
|
||||
if !ok {
|
||||
p.active = false
|
||||
return
|
||||
}
|
||||
|
||||
if distance(p.position, target.position) <= PROJECTILE_HIT_RADIUS {
|
||||
target.health -= p.damage
|
||||
p.active = false
|
||||
move_toward(&p.position, target.position, p.speed, dt)
|
||||
|
||||
if target.health <= 0 {
|
||||
target.active = false
|
||||
push_event(world, .Enemy_Killed, gold_reward = 5)
|
||||
}
|
||||
}
|
||||
if distance(p.position, target.position) <= PROJECTILE_HIT_RADIUS {
|
||||
p.active = false
|
||||
apply_tower_hit(world, target, p.damage)
|
||||
}
|
||||
}
|
||||
|
||||
update_ballistic_projectile :: proc(world: ^World, p: ^Projectile, dt: f32) {
|
||||
step := p.speed * dt
|
||||
p.position.x += p.direction.x * step
|
||||
p.position.y += p.direction.y * step
|
||||
|
||||
if apply_splash_damage(world, p.position, p.damage, p.splash_radius) {
|
||||
p.active = false
|
||||
return
|
||||
}
|
||||
|
||||
if p.position.x < 0 ||
|
||||
p.position.y < 0 ||
|
||||
p.position.x > MAP_W * TILE_SIZE ||
|
||||
p.position.y > MAP_H * TILE_SIZE {
|
||||
p.active = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +129,29 @@ render_projectiles :: proc(world: ^World) {
|
||||
for i in 0 ..< MAX_PROJECTILES {
|
||||
p := world.projectiles[i]
|
||||
if !p.active do continue
|
||||
rl.DrawCircle(i32(p.position.x), i32(p.position.y), 4, rl.GOLD)
|
||||
color := rl.GOLD
|
||||
radius: f32 = 4
|
||||
if p.mode == .Ballistic {
|
||||
color = rl.GRAY
|
||||
radius = 6
|
||||
}
|
||||
rl.DrawCircle(i32(p.position.x), i32(p.position.y), radius, color)
|
||||
}
|
||||
}
|
||||
|
||||
apply_splash_damage :: proc(world: ^World, center: Vec2, damage: int, radius: f32) -> bool {
|
||||
if radius <= 0 do return hit_enemy_at(world, center, damage)
|
||||
|
||||
hit_any := false
|
||||
for i in 0 ..< MAX_ENEMIES {
|
||||
e := &world.enemies[i]
|
||||
if !e.active do continue
|
||||
if distance(center, e.position) <= radius {
|
||||
apply_tower_hit(world, e, damage)
|
||||
hit_any = true
|
||||
}
|
||||
}
|
||||
|
||||
return hit_any
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ render_world :: proc(world: ^World) {
|
||||
render_map(world)
|
||||
render_enemies(world)
|
||||
render_towers(world)
|
||||
render_placement_preview(world)
|
||||
render_projectiles(world)
|
||||
}
|
||||
|
||||
|
||||
+111
-102
@@ -1,125 +1,91 @@
|
||||
package game
|
||||
|
||||
import "core:fmt"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
Tower :: struct {
|
||||
position: Vec2,
|
||||
kind: Tower_Kind,
|
||||
cooldown: f32,
|
||||
anchor_gx: int,
|
||||
anchor_gy: int,
|
||||
upgrade_level: int,
|
||||
position: Vec2,
|
||||
kind: Tower_Kind,
|
||||
cooldown: f32,
|
||||
anchor_gx: int,
|
||||
anchor_gy: int,
|
||||
}
|
||||
|
||||
Tower_Kind :: enum {
|
||||
Archer,
|
||||
Cannon,
|
||||
Ice,
|
||||
}
|
||||
|
||||
Tower_Archetype :: struct {
|
||||
kind: Tower_Kind,
|
||||
range: f32,
|
||||
damage: int,
|
||||
fire_rate: f32,
|
||||
cost: int,
|
||||
footprint_w: int, // 1 column wide
|
||||
footprint_h: int, // two rows tall
|
||||
range: f32,
|
||||
damage: int,
|
||||
fire_rate: f32,
|
||||
cost: int,
|
||||
footprint_w: int, // 1 column wide
|
||||
footprint_h: int, // two rows tall
|
||||
max_upgrade: int,
|
||||
upgrade_cost: int,
|
||||
damage_per_level: int,
|
||||
range_per_level: f32,
|
||||
splash_radius: f32,
|
||||
color: rl.Color,
|
||||
}
|
||||
|
||||
TOWER_ARCHETYPES: [Tower_Kind]Tower_Archetype = {
|
||||
.Archer = {
|
||||
kind = .Archer,
|
||||
range = 50,
|
||||
range = 100,
|
||||
damage = 12,
|
||||
fire_rate = 0.45,
|
||||
cost = TOWER_COST,
|
||||
cost = 50,
|
||||
footprint_w = 1,
|
||||
footprint_h = 2,
|
||||
max_upgrade = 1,
|
||||
upgrade_cost = 40,
|
||||
damage_per_level = 3,
|
||||
range_per_level = 8,
|
||||
splash_radius = 0,
|
||||
color = rl.YELLOW,
|
||||
},
|
||||
.Cannon = {
|
||||
range = 110,
|
||||
damage = 35,
|
||||
fire_rate = 1.2,
|
||||
cost = 80,
|
||||
footprint_w = 2,
|
||||
footprint_h = 2,
|
||||
max_upgrade = 1,
|
||||
upgrade_cost = 40,
|
||||
damage_per_level = 3,
|
||||
range_per_level = 8,
|
||||
splash_radius = 60,
|
||||
color = rl.DARKGRAY,
|
||||
},
|
||||
.Ice = {
|
||||
range = 85,
|
||||
damage = 6,
|
||||
fire_rate = 0.55,
|
||||
cost = 65,
|
||||
footprint_w = 1,
|
||||
footprint_h = 1,
|
||||
max_upgrade = 1,
|
||||
upgrade_cost = 40,
|
||||
damage_per_level = 3,
|
||||
range_per_level = 8,
|
||||
splash_radius = 0,
|
||||
color = rl.DARKBLUE,
|
||||
},
|
||||
}
|
||||
|
||||
rects_overlap :: proc(
|
||||
left_a, top_a, width_a, height_a: int,
|
||||
left_b, top_b, width_b, height_b: int,
|
||||
) -> bool {
|
||||
return(
|
||||
left_a < left_b + width_b &&
|
||||
left_a + width_a > left_b &&
|
||||
top_a < top_b + height_b &&
|
||||
top_a + height_a > top_b \
|
||||
)
|
||||
}
|
||||
|
||||
footprint_center :: proc(gx, gy, footprint_w, footprint_h: int) -> Vec2 {
|
||||
return Vec2 {
|
||||
f32(gx * TILE_SIZE + (footprint_w * TILE_SIZE) / 2),
|
||||
f32(gy * TILE_SIZE + (footprint_h * TILE_SIZE) / 2),
|
||||
}
|
||||
}
|
||||
|
||||
footprint_fits_map :: proc(world_map: ^Map, gx, gy, footprint_w, footprint_h: int) -> bool {
|
||||
if gx < 0 || gy < 0 do return false
|
||||
if gx + footprint_w > MAP_W do return false
|
||||
if gy + footprint_h > MAP_H do return false
|
||||
|
||||
for row in gy ..< gy + footprint_h {
|
||||
for column in gx ..< gx + footprint_w {
|
||||
if !can_build_at(world_map, column, row) do return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
footprint_blocked :: proc(world: ^World, gx, gy, footprint_w, footprint_h: int) -> bool {
|
||||
for placed_tower in world.towers {
|
||||
archetype := get_archetype(placed_tower.kind)
|
||||
if rects_overlap(
|
||||
gx,
|
||||
gy,
|
||||
footprint_w,
|
||||
footprint_h,
|
||||
placed_tower.anchor_gx,
|
||||
placed_tower.anchor_gy,
|
||||
archetype.footprint_w,
|
||||
archetype.footprint_h,
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
get_archetype :: proc(kind: Tower_Kind) -> Tower_Archetype {
|
||||
return TOWER_ARCHETYPES[kind]
|
||||
}
|
||||
|
||||
try_place_tower :: proc(world: ^World, gx, gy: int, kind: Tower_Kind) -> bool {
|
||||
if world.phase != .Build do return false
|
||||
|
||||
arch := get_archetype(kind)
|
||||
footprint_w := arch.footprint_w
|
||||
footprint_h := arch.footprint_h
|
||||
|
||||
if !footprint_fits_map(&world.world_map, gx, gy, footprint_w, footprint_h) do return false
|
||||
if footprint_blocked(world, gx, gy, footprint_w, footprint_h) do return false
|
||||
if world.gold < arch.cost do return false
|
||||
|
||||
world.gold -= arch.cost
|
||||
append(
|
||||
&world.towers,
|
||||
Tower {
|
||||
position = footprint_center(gx, gy, footprint_w, footprint_h),
|
||||
anchor_gx = gx,
|
||||
anchor_gy = gy,
|
||||
kind = kind,
|
||||
cooldown = 0,
|
||||
},
|
||||
)
|
||||
return true
|
||||
tower_stats_at_level :: proc(tower: Tower) -> (damage: int, range: f32) {
|
||||
arch := TOWER_ARCHETYPES[tower.kind]
|
||||
damage = arch.damage + tower.upgrade_level * arch.damage_per_level
|
||||
range = arch.range + f32(tower.upgrade_level) * arch.range_per_level
|
||||
return
|
||||
}
|
||||
|
||||
find_target_slot :: proc(tower: Tower, world: ^World) -> int {
|
||||
arch := get_archetype(tower.kind)
|
||||
best_slot: int = -1
|
||||
best_dist: f32 = 999999
|
||||
|
||||
@@ -129,7 +95,8 @@ find_target_slot :: proc(tower: Tower, world: ^World) -> int {
|
||||
if e.path_index >= len(world.path) do continue
|
||||
|
||||
d := distance(tower.position, e.position)
|
||||
if d <= arch.range && d < best_dist {
|
||||
_, range := tower_stats_at_level(tower)
|
||||
if d <= range && d < best_dist {
|
||||
best_slot = i
|
||||
best_dist = d
|
||||
}
|
||||
@@ -147,28 +114,70 @@ update_towers :: proc(world: ^World, dt: f32) {
|
||||
slot := find_target_slot(t^, world)
|
||||
if slot < 0 do continue
|
||||
|
||||
arch := get_archetype(t.kind)
|
||||
arch := TOWER_ARCHETYPES[t.kind]
|
||||
|
||||
damage, _ := tower_stats_at_level(t^)
|
||||
switch t.kind {
|
||||
case .Archer:
|
||||
spawn_projectile(world, t.position, slot, arch.damage)
|
||||
t.cooldown = arch.fire_rate
|
||||
spawn_projectile(world, t.position, slot, damage)
|
||||
case .Cannon:
|
||||
if target := enemy_at_slot(world, slot); target != nil {
|
||||
len := distance(t.position, target.position)
|
||||
if (len > 0.001) {
|
||||
dir := Vec2 {
|
||||
(target.position.x - t.position.x) / len,
|
||||
(target.position.y - t.position.y) / len,
|
||||
}
|
||||
spawn_ballistic(world, t.position, dir, damage, arch.splash_radius)
|
||||
}
|
||||
}
|
||||
case .Ice:
|
||||
if target := enemy_at_slot(world, slot); target != nil {
|
||||
apply_ice_hit(world, target, damage)
|
||||
}
|
||||
}
|
||||
t.cooldown = arch.fire_rate
|
||||
}
|
||||
}
|
||||
|
||||
apply_tower_hit :: proc(world: ^World, target: ^Enemy, damage: int) {
|
||||
target.health -= damage
|
||||
if target.health <= 0 {
|
||||
target.active = false
|
||||
push_event(world, .Enemy_Killed, gold_reward = ENEMY_DEF[target.kind].gold_reward)
|
||||
}
|
||||
}
|
||||
|
||||
apply_ice_hit :: proc(world: ^World, target: ^Enemy, damage: int) {
|
||||
apply_tower_hit(world, target, damage)
|
||||
if target.active do target.slow_timer = ICE_SLOW_DURATION
|
||||
}
|
||||
|
||||
enemy_at_slot :: proc(world: ^World, slot: int) -> ^Enemy {
|
||||
if slot < 0 || slot >= MAX_ENEMIES do return nil
|
||||
|
||||
e := &world.enemies[slot]
|
||||
if !e.active do return nil
|
||||
return e
|
||||
}
|
||||
|
||||
render_towers :: proc(world: ^World) {
|
||||
for t in world.towers {
|
||||
archetype := get_archetype(t.kind)
|
||||
archetype := TOWER_ARCHETYPES[t.kind]
|
||||
pixel_width := f32(archetype.footprint_w * TILE_SIZE)
|
||||
pixel_height := f32(archetype.footprint_h * TILE_SIZE)
|
||||
color := archetype.color
|
||||
rl.DrawRectangle(
|
||||
i32(t.position.x - pixel_width * 0.5),
|
||||
i32(t.position.y - pixel_height * 0.5),
|
||||
i32(pixel_width),
|
||||
i32(pixel_height),
|
||||
rl.Color{255, 203, 0, 255},
|
||||
color,
|
||||
)
|
||||
if t.upgrade_level > 0 {
|
||||
label := fmt.ctprintf("Lv%d", t.upgrade_level + 1) // or just t.upgrade_level
|
||||
rl.DrawText(label, i32(t.position.x), i32(t.position.y), 14, rl.WHITE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package game
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
can_place_tower :: proc(world: ^World, gx, gy: int, kind: Tower_Kind) -> bool {
|
||||
if world.phase != .Build do return false
|
||||
arch := TOWER_ARCHETYPES[kind]
|
||||
if world.gold < arch.cost do return false
|
||||
if !footprint_fits_map(&world.world_map, gx, gy, arch.footprint_w, arch.footprint_h) do return false
|
||||
if footprint_blocked(world, gx, gy, arch.footprint_w, arch.footprint_h) do return false
|
||||
return true
|
||||
}
|
||||
|
||||
rects_overlap :: proc(
|
||||
left_a, top_a, width_a, height_a: int,
|
||||
left_b, top_b, width_b, height_b: int,
|
||||
) -> bool {
|
||||
return(
|
||||
left_a < left_b + width_b &&
|
||||
left_a + width_a > left_b &&
|
||||
top_a < top_b + height_b &&
|
||||
top_a + height_a > top_b \
|
||||
)
|
||||
}
|
||||
|
||||
footprint_center :: proc(gx, gy, footprint_w, footprint_h: int) -> Vec2 {
|
||||
return Vec2 {
|
||||
f32(gx * TILE_SIZE + (footprint_w * TILE_SIZE) / 2),
|
||||
f32(gy * TILE_SIZE + (footprint_h * TILE_SIZE) / 2),
|
||||
}
|
||||
}
|
||||
|
||||
footprint_fits_map :: proc(world_map: ^Map, gx, gy, footprint_w, footprint_h: int) -> bool {
|
||||
if gx < 0 || gy < 0 do return false
|
||||
if gx + footprint_w > MAP_W do return false
|
||||
if gy + footprint_h > MAP_H do return false
|
||||
|
||||
for column in 0 ..< footprint_h {
|
||||
for row in 0 ..< footprint_w {
|
||||
cell_column := gx + column
|
||||
cell_row := gy + row
|
||||
if !can_build_at(world_map, cell_column, cell_row) do return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
footprint_blocked :: proc(world: ^World, gx, gy, footprint_w, footprint_h: int) -> bool {
|
||||
for placed_tower in world.towers {
|
||||
archetype := TOWER_ARCHETYPES[placed_tower.kind]
|
||||
if rects_overlap(
|
||||
gx,
|
||||
gy,
|
||||
footprint_w,
|
||||
footprint_h,
|
||||
placed_tower.anchor_gx,
|
||||
placed_tower.anchor_gy,
|
||||
archetype.footprint_w,
|
||||
archetype.footprint_h,
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
try_place_tower :: proc(world: ^World, gx, gy: int, kind: Tower_Kind) -> bool {
|
||||
if !can_place_tower(world, gx, gy, kind) do return false
|
||||
|
||||
arch := TOWER_ARCHETYPES[kind]
|
||||
world.gold -= arch.cost
|
||||
append(
|
||||
&world.towers,
|
||||
Tower {
|
||||
position = footprint_center(gx, gy, arch.footprint_w, arch.footprint_h),
|
||||
anchor_gx = gx,
|
||||
anchor_gy = gy,
|
||||
kind = kind,
|
||||
cooldown = 0,
|
||||
},
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
tower_at_grid :: proc(world: ^World, gx, gy: int) -> ^Tower {
|
||||
for i in 0 ..< len(world.towers) {
|
||||
t := &world.towers[i]
|
||||
arch := TOWER_ARCHETYPES[t.kind]
|
||||
if gx >= t.anchor_gx &&
|
||||
gx < t.anchor_gx + arch.footprint_w &&
|
||||
gy >= t.anchor_gy &&
|
||||
gy < t.anchor_gy + arch.footprint_h {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
try_upgrade_tower :: proc(world: ^World, gx, gy: int) -> bool {
|
||||
if world.phase != .Build do return false
|
||||
t := tower_at_grid(world, gx, gy)
|
||||
if t == nil do return false
|
||||
|
||||
arch := TOWER_ARCHETYPES[t.kind]
|
||||
if t.upgrade_level >= arch.max_upgrade do return false
|
||||
|
||||
cost := arch.upgrade_cost + t.upgrade_level * 25
|
||||
if world.gold < cost do return false
|
||||
|
||||
world.gold -= cost
|
||||
t.upgrade_level += 1
|
||||
return true
|
||||
}
|
||||
|
||||
render_placement_preview :: proc(world: ^World) {
|
||||
if world.phase != .Build do return
|
||||
|
||||
mouse := game_mouse()
|
||||
gx, gy := screen_to_grid(mouse.x, mouse.y)
|
||||
arch := TOWER_ARCHETYPES[world.selected_tower]
|
||||
ok := can_place_tower(world, gx, gy, world.selected_tower)
|
||||
|
||||
center := footprint_center(gx, gy, arch.footprint_w, arch.footprint_h)
|
||||
rl.DrawCircleLines(i32(center.x), i32(center.y), arch.range, rl.Fade(rl.SKYBLUE, 0.4))
|
||||
|
||||
tint := rl.Fade(rl.GREEN, 0.3)
|
||||
if !ok do tint = rl.Fade(rl.RED, 0.35)
|
||||
|
||||
for row in 0 ..< arch.footprint_h {
|
||||
for col in 0 ..< arch.footprint_w {
|
||||
px := (gx + col) * TILE_SIZE
|
||||
py := (gy + row) * TILE_SIZE
|
||||
rl.DrawRectangle(i32(px), i32(py), TILE_SIZE, TILE_SIZE, tint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package game
|
||||
|
||||
import clay "../clay-odin"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
ui_memory: [^]u8
|
||||
ui_arena: clay.Arena
|
||||
ui_render_commands: clay.ClayArray(clay.RenderCommand)
|
||||
|
||||
ui_error_handler :: proc "c" (data: clay.ErrorData) {
|
||||
_ = data
|
||||
}
|
||||
|
||||
init_ui :: proc() {
|
||||
ui_register_font()
|
||||
|
||||
min_size := clay.MinMemorySize()
|
||||
ui_memory = make([^]u8, min_size)
|
||||
ui_arena = clay.CreateArenaWithCapacityAndMemory(uint(min_size), ui_memory)
|
||||
clay.Initialize(ui_arena, {f32(SCREEN_W), f32(SCREEN_H)}, {handler = ui_error_handler})
|
||||
clay.SetMeasureTextFunction(measure_text, nil)
|
||||
|
||||
}
|
||||
|
||||
destroy_ui :: proc() {
|
||||
free(ui_memory)
|
||||
}
|
||||
|
||||
ui_prepare_frame :: proc(world: ^World, dt: f32) {
|
||||
mouse := rl.GetMousePosition()
|
||||
clay.SetLayoutDimensions({f32(SCREEN_W), f32(SCREEN_H)})
|
||||
clay.SetPointerState({mouse.x, mouse.y}, rl.IsMouseButtonDown(.LEFT))
|
||||
clay.UpdateScrollContainers(false, {}, dt)
|
||||
|
||||
clay.BeginLayout()
|
||||
build_hud(world)
|
||||
ui_render_commands = clay.EndLayout(dt)
|
||||
}
|
||||
|
||||
render_ui :: proc() {
|
||||
clay_raylib_render(&ui_render_commands)
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package game
|
||||
|
||||
import clay "../clay-odin"
|
||||
import "core:math"
|
||||
import "core:strings"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
UI_FONT_ID :: 0
|
||||
|
||||
ui_font: rl.Font
|
||||
|
||||
ui_register_font :: proc() {
|
||||
ui_font = rl.GetFontDefault()
|
||||
}
|
||||
|
||||
clay_color_to_rl :: proc(c: clay.Color) -> rl.Color {
|
||||
return {u8(c.r), u8(c.g), u8(c.b), u8(c.a)}
|
||||
}
|
||||
|
||||
measure_text :: proc "c" (
|
||||
text: clay.StringSlice,
|
||||
config: ^clay.TextElementConfig,
|
||||
_: rawptr,
|
||||
) -> clay.Dimensions {
|
||||
return {width = f32(text.length) * f32(config.fontSize) * 0.55, height = f32(config.fontSize)}
|
||||
}
|
||||
|
||||
clay_raylib_render :: proc(commands: ^clay.ClayArray(clay.RenderCommand)) {
|
||||
for i in 0 ..< commands.length {
|
||||
cmd := clay.RenderCommandArray_Get(commands, i)
|
||||
bounds := cmd.boundingBox
|
||||
|
||||
#partial switch cmd.commandType {
|
||||
case .None:
|
||||
case .Text:
|
||||
cfg := cmd.renderData.text
|
||||
text := string(cfg.stringContents.chars[:cfg.stringContents.length])
|
||||
cstr := strings.clone_to_cstring(text, context.temp_allocator)
|
||||
|
||||
rl.DrawTextEx(
|
||||
ui_font,
|
||||
cstr,
|
||||
{bounds.x, bounds.y},
|
||||
f32(cfg.fontSize),
|
||||
f32(cfg.letterSpacing),
|
||||
clay_color_to_rl(cfg.textColor),
|
||||
)
|
||||
case .Rectangle:
|
||||
cfg := cmd.renderData.rectangle
|
||||
rl.DrawRectangle(
|
||||
i32(math.round(bounds.x)),
|
||||
i32(math.round(bounds.y)),
|
||||
i32(math.round(bounds.width)),
|
||||
i32(math.round(bounds.height)),
|
||||
clay_color_to_rl(cfg.backgroundColor),
|
||||
)
|
||||
case .ScissorStart:
|
||||
rl.BeginScissorMode(
|
||||
i32(math.round(bounds.x)),
|
||||
i32(math.round(bounds.y)),
|
||||
i32(math.round(bounds.width)),
|
||||
i32(math.round(bounds.height)),
|
||||
)
|
||||
case .ScissorEnd:
|
||||
rl.EndScissorMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package game
|
||||
|
||||
import rl "vendor:raylib"
|
||||
|
||||
draw_text_centered_in_rect :: proc(
|
||||
text: cstring,
|
||||
rect: rl.Rectangle,
|
||||
font_size: i32,
|
||||
color: rl.Color,
|
||||
) {
|
||||
text_w := rl.MeasureText(text, font_size)
|
||||
x := i32(rect.x) + (i32(rect.width) - text_w) / 2
|
||||
y := i32(rect.y) + (i32(rect.height) - font_size) / 2
|
||||
rl.DrawText(text, x, y, font_size, color)
|
||||
}
|
||||
|
||||
draw_button :: proc(
|
||||
rect: rl.Rectangle,
|
||||
label: cstring,
|
||||
bg: rl.Color,
|
||||
font_size: i32,
|
||||
text_color: rl.Color,
|
||||
) {
|
||||
rl.DrawRectangleRec(rect, bg)
|
||||
draw_text_centered_in_rect(label, rect, font_size, text_color)
|
||||
}
|
||||
|
||||
+70
-21
@@ -8,44 +8,93 @@ Game_Phase :: enum {
|
||||
}
|
||||
|
||||
Wave_Spawner :: struct {
|
||||
enemies_to_spawn: int,
|
||||
spawn_timer: f32,
|
||||
spawn_interval: f32,
|
||||
spawn_timer: f32,
|
||||
spawn_interval: f32,
|
||||
recipe: [dynamic]Wave_Entry,
|
||||
recipe_index: int,
|
||||
entry_remaining: int,
|
||||
}
|
||||
|
||||
start_wave :: proc(world: ^World) {
|
||||
if world.phase != .Build do return
|
||||
world.wave += 1
|
||||
world.phase = .Combat
|
||||
delete(world.spawner.recipe)
|
||||
world.spawner = Wave_Spawner {
|
||||
enemies_to_spawn = 5 + world.wave * 2,
|
||||
spawn_timer = 0,
|
||||
spawn_interval = 0.55,
|
||||
spawn_timer = 0,
|
||||
spawn_interval = 0.55,
|
||||
recipe = build_wave_recipe(world.wave),
|
||||
recipe_index = 0,
|
||||
entry_remaining = 0,
|
||||
}
|
||||
load_next_recipe_entry(world)
|
||||
}
|
||||
|
||||
Wave_Entry :: struct {
|
||||
kind: Enemy_Kind,
|
||||
count: int,
|
||||
}
|
||||
|
||||
build_wave_recipe :: proc(wave: int) -> [dynamic]Wave_Entry {
|
||||
recipe: [dynamic]Wave_Entry
|
||||
switch wave {
|
||||
case 1:
|
||||
append(&recipe, Wave_Entry{.Grunt, 6})
|
||||
case 2:
|
||||
append(&recipe, Wave_Entry{.Grunt, 4})
|
||||
append(&recipe, Wave_Entry{.Runner, 2})
|
||||
case:
|
||||
grunt := 3 + wave
|
||||
runners := wave / 2
|
||||
tanks := wave / 3
|
||||
append(&recipe, Wave_Entry{.Grunt, grunt})
|
||||
if runners > 0 do append(&recipe, Wave_Entry{.Runner, runners})
|
||||
if tanks > 0 do append(&recipe, Wave_Entry{.Tank, tanks})
|
||||
}
|
||||
|
||||
return recipe
|
||||
}
|
||||
|
||||
load_next_recipe_entry :: proc(world: ^World) {
|
||||
s := &world.spawner
|
||||
if s.recipe_index >= len(s.recipe) {
|
||||
s.entry_remaining = 0
|
||||
return
|
||||
}
|
||||
entry := s.recipe[s.recipe_index]
|
||||
s.entry_remaining = entry.count
|
||||
}
|
||||
|
||||
clear_wave :: proc(world: ^World) -> bool {
|
||||
s := world.spawner
|
||||
if s.recipe_index < len(s.recipe) do return false
|
||||
if s.entry_remaining > 0 do return false
|
||||
return active_enemy_count(world) == 0
|
||||
}
|
||||
|
||||
update_wave :: proc(world: ^World, dt: f32) {
|
||||
if world.phase != .Combat do return
|
||||
if world.spawner.enemies_to_spawn <= 0 do return
|
||||
if len(world.path) < 2 do return
|
||||
s := &world.spawner
|
||||
if s.entry_remaining <= 0 && s.recipe_index >= len(s.recipe) do return
|
||||
|
||||
world.spawner.spawn_timer -= dt
|
||||
if world.spawner.spawn_timer <= 0 {
|
||||
health := 15 + world.wave * 5
|
||||
speed := 55 + f32(world.wave) * 5
|
||||
spawn_enemy(world, health, speed)
|
||||
world.spawner.enemies_to_spawn -= 1
|
||||
world.spawner.spawn_timer = world.spawner.spawn_interval
|
||||
s.spawn_timer -= dt
|
||||
if s.spawn_timer > 0 do return
|
||||
|
||||
if s.entry_remaining <= 0 {
|
||||
s.recipe_index += 1
|
||||
load_next_recipe_entry(world)
|
||||
if s.entry_remaining <= 0 do return
|
||||
}
|
||||
}
|
||||
|
||||
wave_clear :: proc(world: ^World) -> bool {
|
||||
if world.spawner.enemies_to_spawn > 0 do return false
|
||||
return active_enemy_count(world) == 0
|
||||
kind := s.recipe[s.recipe_index].kind
|
||||
spawn_enemy(world, kind)
|
||||
s.entry_remaining -= 1
|
||||
s.spawn_timer = s.spawn_interval
|
||||
|
||||
}
|
||||
|
||||
update_phase :: proc(world: ^World) {
|
||||
if world.phase == .Combat && wave_clear(world) {
|
||||
if world.phase == .Combat && clear_wave(world) {
|
||||
world.phase = .Build
|
||||
push_event(world, .Wave_Survived, gold_reward = 25)
|
||||
}
|
||||
@@ -56,7 +105,7 @@ check_end :: proc(world: ^World) {
|
||||
world.phase = .Game_Over
|
||||
}
|
||||
|
||||
if world.wave >= MAX_WAVES && world.phase == .Build && wave_clear(world) {
|
||||
if world.wave >= MAX_WAVES && world.phase == .Build && clear_wave(world) {
|
||||
world.phase = .Victory
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package game
|
||||
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_start_wave_enters_combat :: proc(t: ^testing.T) {
|
||||
world := World {
|
||||
phase = .Combat,
|
||||
}
|
||||
world.wave = 0
|
||||
|
||||
start_wave(&world)
|
||||
|
||||
testing.expect(t, world.phase == .Combat)
|
||||
testing.expect(t, world.wave == 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_start_wave_from_build :: proc(t: ^testing.T) {
|
||||
world := World {
|
||||
phase = .Build,
|
||||
}
|
||||
world.wave = 0
|
||||
|
||||
start_wave(&world)
|
||||
defer delete(world.spawner.recipe)
|
||||
|
||||
testing.expect(t, world.phase == .Combat)
|
||||
testing.expect(t, world.wave == 1)
|
||||
testing.expect(t, world.spawner.spawn_timer == 0)
|
||||
testing.expect(t, world.spawner.spawn_interval == 0.55)
|
||||
testing.expect(t, world.spawner.recipe_index == 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_clear_wave_not_cleared :: proc(t: ^testing.T) {
|
||||
world := World{}
|
||||
world.spawner.recipe = make([dynamic]Wave_Entry, context.allocator)
|
||||
append(&world.spawner.recipe, Wave_Entry{})
|
||||
defer delete(world.spawner.recipe)
|
||||
world.spawner.recipe_index = 0
|
||||
world.spawner.entry_remaining = 0
|
||||
|
||||
result := clear_wave(&world)
|
||||
|
||||
testing.expect(t, result == false)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_clear_wave_cleared :: proc(t: ^testing.T) {
|
||||
world := World{}
|
||||
world.spawner.recipe = make([dynamic]Wave_Entry, context.allocator)
|
||||
append(&world.spawner.recipe, Wave_Entry{})
|
||||
defer delete(world.spawner.recipe)
|
||||
world.spawner.recipe_index = 1
|
||||
world.spawner.entry_remaining = 0
|
||||
|
||||
result := clear_wave(&world)
|
||||
|
||||
testing.expect(t, result == true)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_update_wave_ignores_build_phase :: proc(t: ^testing.T) {
|
||||
world := init_world()
|
||||
defer delete(world.path)
|
||||
world.phase = .Build
|
||||
world.spawner.spawn_timer = 0
|
||||
world.spawner.entry_remaining = 5
|
||||
|
||||
update_wave(&world, 1.0)
|
||||
|
||||
testing.expect(t, active_enemy_count(&world) == 0)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_update_wave_waits_for_spawn_timer :: proc(t: ^testing.T) {
|
||||
world := init_world()
|
||||
defer delete(world.path)
|
||||
world.phase = .Combat
|
||||
world.spawner.recipe = make([dynamic]Wave_Entry, context.allocator)
|
||||
append(&world.spawner.recipe, Wave_Entry{.Grunt, 1})
|
||||
defer delete(world.spawner.recipe)
|
||||
world.spawner.recipe_index = 0
|
||||
world.spawner.entry_remaining = 1
|
||||
world.spawner.spawn_timer = 0.5
|
||||
|
||||
update_wave(&world, 0.1)
|
||||
|
||||
testing.expect(t, active_enemy_count(&world) == 0)
|
||||
testing.expect(t, world.spawner.spawn_timer == 0.4)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_update_wave_spawns_and_resets_timer :: proc(t: ^testing.T) {
|
||||
world := init_world()
|
||||
defer delete(world.path)
|
||||
world.phase = .Combat
|
||||
world.wave = 1
|
||||
world.spawner.recipe = make([dynamic]Wave_Entry, context.allocator)
|
||||
append(&world.spawner.recipe, Wave_Entry{.Grunt, 2})
|
||||
defer delete(world.spawner.recipe)
|
||||
world.spawner.recipe_index = 0
|
||||
world.spawner.entry_remaining = 2
|
||||
world.spawner.spawn_timer = 0
|
||||
world.spawner.spawn_interval = 0.55
|
||||
|
||||
update_wave(&world, 0.55)
|
||||
|
||||
testing.expect(t, active_enemy_count(&world) == 1)
|
||||
testing.expect(t, world.spawner.entry_remaining == 1)
|
||||
testing.expect(t, world.spawner.spawn_timer == 0.55)
|
||||
|
||||
found_grunt := false
|
||||
for i in 0 ..< MAX_ENEMIES {
|
||||
if world.enemies[i].active {
|
||||
testing.expect(t, world.enemies[i].kind == .Grunt)
|
||||
found_grunt = true
|
||||
}
|
||||
}
|
||||
testing.expect(t, found_grunt)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_update_phase_from_build_phase :: proc(t: ^testing.T) {
|
||||
world := World{}
|
||||
defer delete(world.events)
|
||||
|
||||
world.phase = .Build
|
||||
update_phase(&world)
|
||||
|
||||
testing.expect(t, world.phase == .Build)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_update_phase_from_combat_phase :: proc(t: ^testing.T) {
|
||||
world := World{}
|
||||
defer delete(world.events)
|
||||
world.phase = .Combat
|
||||
|
||||
update_phase(&world)
|
||||
|
||||
testing.expect(t, world.phase == .Build)
|
||||
}
|
||||
|
||||
+16
-14
@@ -1,24 +1,26 @@
|
||||
package game
|
||||
|
||||
World :: struct {
|
||||
gold: int,
|
||||
base_health: int,
|
||||
world_map: Map,
|
||||
path: [dynamic]Vec2,
|
||||
enemies: [MAX_ENEMIES]Enemy,
|
||||
towers: [dynamic]Tower,
|
||||
events: [dynamic]Event,
|
||||
phase: Game_Phase,
|
||||
wave: int,
|
||||
spawner: Wave_Spawner,
|
||||
projectiles: [MAX_PROJECTILES]Projectile,
|
||||
gold: int,
|
||||
base_health: int,
|
||||
world_map: Map,
|
||||
path: [dynamic]Vec2,
|
||||
enemies: [MAX_ENEMIES]Enemy,
|
||||
towers: [dynamic]Tower,
|
||||
events: [dynamic]Event,
|
||||
phase: Game_Phase,
|
||||
wave: int,
|
||||
spawner: Wave_Spawner,
|
||||
projectiles: [MAX_PROJECTILES]Projectile,
|
||||
selected_tower: Tower_Kind,
|
||||
}
|
||||
|
||||
init_world :: proc() -> World {
|
||||
world := World {
|
||||
gold = 100,
|
||||
base_health = 20,
|
||||
phase = .Build,
|
||||
gold = STARTING_GOLD,
|
||||
base_health = STARTING_BASE_HEALTH,
|
||||
phase = .Build,
|
||||
selected_tower = .Archer,
|
||||
}
|
||||
init_map(&world.world_map)
|
||||
build_path(&world.world_map, &world.path)
|
||||
|
||||
Reference in New Issue
Block a user