Skip to content

Commit 6ff50cd

Browse files
committed
Refactor LevelMode scenes with shared UI helpers
1 parent 471cc86 commit 6ff50cd

10 files changed

Lines changed: 730 additions & 743 deletions

SOPA.xcodeproj/project.pbxproj

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,20 @@
112112
64EFCB3E240E732F00714E7C /* LevelDestroyer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EFCB3C240E732F00714E7C /* LevelDestroyer.swift */; };
113113
64EFCB40240E763A00714E7C /* LevelSolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EFCB3F240E763A00714E7C /* LevelSolver.swift */; };
114114
64EFCB41240E763A00714E7C /* LevelSolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EFCB3F240E763A00714E7C /* LevelSolver.swift */; };
115+
A11E00012602000100AAB001 /* StartMenuScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA001 /* StartMenuScene.swift */; };
116+
A11E00012602000100AAB002 /* StartMenuScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA001 /* StartMenuScene.swift */; };
117+
A11E00012602000100AAB003 /* CreditsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA002 /* CreditsScene.swift */; };
118+
A11E00012602000100AAB004 /* CreditsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA002 /* CreditsScene.swift */; };
119+
A11E00012602000100AAB005 /* TutorialScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA003 /* TutorialScene.swift */; };
120+
A11E00012602000100AAB006 /* TutorialScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA003 /* TutorialScene.swift */; };
121+
A11E00012602000100AAB007 /* TutorialGameScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA004 /* TutorialGameScene.swift */; };
122+
A11E00012602000100AAB008 /* TutorialGameScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA004 /* TutorialGameScene.swift */; };
123+
A11E00012602000100AAB009 /* SopaTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA005 /* SopaTheme.swift */; };
124+
A11E00012602000100AAB00A /* SopaTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA005 /* SopaTheme.swift */; };
125+
A11E00012602000100AAB00B /* LabelSizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA006 /* LabelSizing.swift */; };
126+
A11E00012602000100AAB00C /* LabelSizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA006 /* LabelSizing.swift */; };
127+
A11E00012602000100AAB00D /* ButtonTextureFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA007 /* ButtonTextureFactory.swift */; };
128+
A11E00012602000100AAB00E /* ButtonTextureFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA007 /* ButtonTextureFactory.swift */; };
115129
A1B2C3D424FA000100000001 /* JustPlayServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D424FA000100000011 /* JustPlayServiceTest.swift */; };
116130
A1B2C3D424FA000100000002 /* helper/LevelServiceImplUnlockingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D424FA000100000012 /* helper/LevelServiceImplUnlockingTest.swift */; };
117131
A1B2C3D424FA000100000003 /* database/JustPlayHighscorePersistenceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D424FA000100000013 /* database/JustPlayHighscorePersistenceTest.swift */; };
@@ -191,6 +205,13 @@
191205
64D66B1A21426ADC00595BD3 /* progbot.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = progbot.ttf; sourceTree = "<group>"; };
192206
64EFCB3C240E732F00714E7C /* LevelDestroyer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LevelDestroyer.swift; sourceTree = "<group>"; };
193207
64EFCB3F240E763A00714E7C /* LevelSolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LevelSolver.swift; sourceTree = "<group>"; };
208+
A11E00012602000100AAA001 /* StartMenuScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartMenuScene.swift; sourceTree = "<group>"; };
209+
A11E00012602000100AAA002 /* CreditsScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsScene.swift; sourceTree = "<group>"; };
210+
A11E00012602000100AAA003 /* TutorialScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialScene.swift; sourceTree = "<group>"; };
211+
A11E00012602000100AAA004 /* TutorialGameScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialGameScene.swift; sourceTree = "<group>"; };
212+
A11E00012602000100AAA005 /* SopaTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SopaTheme.swift; sourceTree = "<group>"; };
213+
A11E00012602000100AAA006 /* LabelSizing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelSizing.swift; sourceTree = "<group>"; };
214+
A11E00012602000100AAA007 /* ButtonTextureFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonTextureFactory.swift; sourceTree = "<group>"; };
194215
A1B2C3D424FA000100000011 /* JustPlayServiceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustPlayServiceTest.swift; sourceTree = "<group>"; };
195216
A1B2C3D424FA000100000012 /* helper/LevelServiceImplUnlockingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = helper/LevelServiceImplUnlockingTest.swift; sourceTree = "<group>"; };
196217
A1B2C3D424FA000100000013 /* database/JustPlayHighscorePersistenceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = database/JustPlayHighscorePersistenceTest.swift; sourceTree = "<group>"; };
@@ -377,10 +398,17 @@
377398
64182B18208DCE0A00CF639A /* LevelModeGameScene.swift */,
378399
64D5B8CE20ADE2680007D982 /* LevelModeScoreScene.swift */,
379400
64D5B8D020B46BD70007D982 /* LevelChoiceScene.swift */,
401+
A11E00012602000100AAA001 /* StartMenuScene.swift */,
402+
A11E00012602000100AAA002 /* CreditsScene.swift */,
403+
A11E00012602000100AAA003 /* TutorialScene.swift */,
404+
A11E00012602000100AAA004 /* TutorialGameScene.swift */,
380405
642E2B3E213B0B1E002669E5 /* LevelSelectButton.swift */,
381406
642E2B41213D257C002669E5 /* LevelButtonPositioner.swift */,
382407
646E6AE8213E80F7001D195B /* LevelButtonArea.swift */,
383408
648558C221412BBD00B61C31 /* EffectSpriteButton.swift */,
409+
A11E00012602000100AAA005 /* SopaTheme.swift */,
410+
A11E00012602000100AAA006 /* LabelSizing.swift */,
411+
A11E00012602000100AAA007 /* ButtonTextureFactory.swift */,
384412
);
385413
path = LevelMode;
386414
sourceTree = "<group>";
@@ -563,6 +591,10 @@
563591
64EFCB40240E763A00714E7C /* LevelSolver.swift in Sources */,
564592
6493FB652073D6510044B4E0 /* SOPA.xcdatamodeld in Sources */,
565593
64D5B8CF20ADE2680007D982 /* LevelModeScoreScene.swift in Sources */,
594+
A11E00012602000100AAB003 /* CreditsScene.swift in Sources */,
595+
A11E00012602000100AAB001 /* StartMenuScene.swift in Sources */,
596+
A11E00012602000100AAB005 /* TutorialScene.swift in Sources */,
597+
A11E00012602000100AAB007 /* TutorialGameScene.swift in Sources */,
566598
63F661231F9B346E0043ABCF /* AppDelegate.swift in Sources */,
567599
E1AA11122602000100ABCDEF /* SceneDelegate.swift in Sources */,
568600
6446C3031FAC80DC00806A10 /* LevelTranslator.swift in Sources */,
@@ -574,6 +606,9 @@
574606
6446C2EF1FAC802900806A10 /* GameServiceImpl.swift in Sources */,
575607
642E2B3F213B0B1E002669E5 /* LevelSelectButton.swift in Sources */,
576608
64206E6C240EBB71001151B4 /* JustPlayGameScene.swift in Sources */,
609+
A11E00012602000100AAB009 /* SopaTheme.swift in Sources */,
610+
A11E00012602000100AAB00B /* LabelSizing.swift in Sources */,
611+
A11E00012602000100AAB00D /* ButtonTextureFactory.swift in Sources */,
577612
6446C3011FAC80DC00806A10 /* LevelFileService.swift in Sources */,
578613
);
579614
runOnlyForDeploymentPostprocessing = 0;
@@ -632,9 +667,16 @@
632667
646902C6207F6B7600283F71 /* GameServiceImpl.swift in Sources */,
633668
64847A0B215A382B0012B732 /* LevelModeScoreScene.swift in Sources */,
634669
64206E6D240EBB71001151B4 /* JustPlayGameScene.swift in Sources */,
670+
A11E00012602000100AAB004 /* CreditsScene.swift in Sources */,
671+
A11E00012602000100AAB002 /* StartMenuScene.swift in Sources */,
672+
A11E00012602000100AAB006 /* TutorialScene.swift in Sources */,
673+
A11E00012602000100AAB008 /* TutorialGameScene.swift in Sources */,
635674
646902CE207F6B7D00283F71 /* LevelFileService.swift in Sources */,
636675
646902DA207F6BD500283F71 /* GameScene.swift in Sources */,
637676
642E2B40213B0B1E002669E5 /* LevelSelectButton.swift in Sources */,
677+
A11E00012602000100AAB00A /* SopaTheme.swift in Sources */,
678+
A11E00012602000100AAB00C /* LabelSizing.swift in Sources */,
679+
A11E00012602000100AAB00E /* ButtonTextureFactory.swift in Sources */,
638680
646902C7207F6B7600283F71 /* Level.swift in Sources */,
639681
642EE5BD22423AAF00680612 /* ProportionSet.swift in Sources */,
640682
);
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import SpriteKit
2+
import UIKit
3+
4+
enum ButtonTextureFactory {
5+
static func makeCircleButtonTexture(
6+
symbolName: String,
7+
side: CGFloat,
8+
circleColor: UIColor = SopaTheme.circleButtonFill,
9+
iconColor: UIColor = SopaTheme.circleButtonIcon,
10+
symbolScale: CGFloat = 0.42,
11+
symbolWeight: UIImage.SymbolWeight = .semibold
12+
) -> SKTexture {
13+
let textureSize = CGSize(width: side, height: side)
14+
let renderer = UIGraphicsImageRenderer(size: textureSize)
15+
let image = renderer.image { _ in
16+
circleColor.setFill()
17+
UIBezierPath(ovalIn: CGRect(origin: .zero, size: textureSize)).fill()
18+
19+
let config = UIImage.SymbolConfiguration(pointSize: side * symbolScale, weight: symbolWeight)
20+
if let symbol = UIImage(systemName: symbolName, withConfiguration: config)?
21+
.withTintColor(iconColor, renderingMode: .alwaysOriginal) {
22+
let symbolRect = CGRect(
23+
x: (side - symbol.size.width) / 2.0,
24+
y: (side - symbol.size.height) / 2.0,
25+
width: symbol.size.width,
26+
height: symbol.size.height
27+
)
28+
symbol.draw(in: symbolRect)
29+
}
30+
}
31+
return SKTexture(image: image)
32+
}
33+
34+
static func makeMinimalBackTexture(side: CGFloat) -> SKTexture {
35+
let textureSize = CGSize(width: side, height: side)
36+
let renderer = UIGraphicsImageRenderer(size: textureSize)
37+
let image = renderer.image { _ in
38+
let config = UIImage.SymbolConfiguration(pointSize: side * 0.72, weight: .semibold)
39+
if let symbol = UIImage(systemName: "chevron.left", withConfiguration: config)?
40+
.withTintColor(UIColor(red: 160.0 / 255.0, green: 164.0 / 255.0, blue: 170.0 / 255.0, alpha: 0.95), renderingMode: .alwaysOriginal) {
41+
let symbolRect = CGRect(
42+
x: (side - symbol.size.width) / 2.0,
43+
y: (side - symbol.size.height) / 2.0,
44+
width: symbol.size.width,
45+
height: symbol.size.height
46+
)
47+
symbol.draw(in: symbolRect)
48+
}
49+
}
50+
return SKTexture(image: image)
51+
}
52+
53+
static func makePageArrowTexture(imageNamed: String, side: CGFloat) -> SKTexture {
54+
let texture = SKTexture(imageNamed: imageNamed)
55+
let imageSize = texture.size()
56+
let ratio = imageSize.width / max(1, imageSize.height)
57+
let targetSize = CGSize(width: side * ratio, height: side)
58+
let renderer = UIGraphicsImageRenderer(size: targetSize)
59+
let image = renderer.image { _ in
60+
UIImage(named: imageNamed)?.draw(in: CGRect(origin: .zero, size: targetSize))
61+
}
62+
return SKTexture(image: image)
63+
}
64+
65+
static func makeNeonTextTexture(title: String, size: CGSize) -> SKTexture {
66+
let renderer = UIGraphicsImageRenderer(size: size)
67+
let image = renderer.image { _ in
68+
let paragraphStyle = NSMutableParagraphStyle()
69+
paragraphStyle.alignment = .center
70+
71+
let glowShadow = NSShadow()
72+
glowShadow.shadowColor = SopaTheme.neonGlowBlue
73+
glowShadow.shadowBlurRadius = size.height * 0.22
74+
glowShadow.shadowOffset = .zero
75+
76+
let font = UIFont(name: "HelveticaNeue-Light", size: size.height * 0.60)
77+
?? UIFont.systemFont(ofSize: size.height * 0.60, weight: .light)
78+
let textRect = CGRect(x: 0, y: size.height * 0.22, width: size.width, height: size.height * 0.60)
79+
80+
let glowAttributes: [NSAttributedString.Key: Any] = [
81+
.font: font,
82+
.foregroundColor: SopaTheme.neonBlue,
83+
.strokeColor: SopaTheme.neonBlue,
84+
.strokeWidth: -1.2,
85+
.paragraphStyle: paragraphStyle,
86+
.shadow: glowShadow
87+
]
88+
title.draw(in: textRect, withAttributes: glowAttributes)
89+
90+
let coreAttributes: [NSAttributedString.Key: Any] = [
91+
.font: font,
92+
.foregroundColor: SopaTheme.neonCoreBlue,
93+
.paragraphStyle: paragraphStyle
94+
]
95+
title.draw(in: textRect, withAttributes: coreAttributes)
96+
}
97+
return SKTexture(image: image)
98+
}
99+
100+
static func makeSocialButtonTexture(symbolName: String, size: CGSize, fillBackground: Bool) -> SKTexture {
101+
let renderer = UIGraphicsImageRenderer(size: size)
102+
let image = renderer.image { _ in
103+
let blueColor = UIColor(red: 99.0 / 255.0, green: 177.0 / 255.0, blue: 230.0 / 255.0, alpha: 1.0)
104+
if fillBackground {
105+
let backgroundPath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: size.width * 0.18)
106+
blueColor.setFill()
107+
backgroundPath.fill()
108+
}
109+
110+
let pointSize = fillBackground ? size.width * 0.50 : size.width * 0.75
111+
let config = UIImage.SymbolConfiguration(pointSize: pointSize, weight: .semibold)
112+
guard let baseSymbol = UIImage(systemName: symbolName, withConfiguration: config) else {
113+
return
114+
}
115+
116+
let iconColor = fillBackground ? UIColor.white : blueColor
117+
let symbol = baseSymbol.withTintColor(iconColor, renderingMode: .alwaysOriginal)
118+
let symbolRect = CGRect(
119+
x: (size.width - symbol.size.width) / 2.0,
120+
y: (size.height - symbol.size.height) / 2.0,
121+
width: symbol.size.width,
122+
height: symbol.size.height
123+
)
124+
symbol.draw(in: symbolRect)
125+
}
126+
return SKTexture(image: image)
127+
}
128+
129+
static func makeTextButtonTexture(
130+
title: String,
131+
size: CGSize,
132+
fontName: String,
133+
textColor: UIColor,
134+
weight: UIFont.Weight = .semibold
135+
) -> SKTexture {
136+
let renderer = UIGraphicsImageRenderer(size: size)
137+
let image = renderer.image { _ in
138+
let frame = CGRect(origin: .zero, size: size).insetBy(dx: size.width * 0.01, dy: size.height * 0.06)
139+
let backgroundPath = UIBezierPath(roundedRect: frame, cornerRadius: size.height * 0.23)
140+
UIColor(white: 1.0, alpha: 0.10).setFill()
141+
backgroundPath.fill()
142+
UIColor(white: 1.0, alpha: 0.22).setStroke()
143+
backgroundPath.lineWidth = size.height * 0.08
144+
backgroundPath.stroke()
145+
146+
let paragraphStyle = NSMutableParagraphStyle()
147+
paragraphStyle.alignment = .center
148+
let buttonFontSize = LabelSizing.fittedTextFontSize(
149+
text: title,
150+
fontName: fontName,
151+
preferredSize: size.height * 0.38,
152+
weight: weight,
153+
maxWidth: size.width * 0.90
154+
)
155+
let attributes: [NSAttributedString.Key: Any] = [
156+
.font: UIFont(name: fontName, size: buttonFontSize) ?? UIFont.systemFont(ofSize: buttonFontSize, weight: weight),
157+
.foregroundColor: textColor,
158+
.paragraphStyle: paragraphStyle
159+
]
160+
let textRect = CGRect(x: 0.0, y: size.height * 0.30, width: size.width, height: size.height * 0.5)
161+
title.draw(in: textRect, withAttributes: attributes)
162+
}
163+
return SKTexture(image: image)
164+
}
165+
}
166+
167+
func makeCircleButtonTexture(symbolName: String, side: CGFloat) -> SKTexture {
168+
ButtonTextureFactory.makeCircleButtonTexture(symbolName: symbolName, side: side)
169+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import Foundation
2+
import SpriteKit
3+
import UIKit
4+
5+
class CreditsScene: SKScene {
6+
private let sectionColor = SopaTheme.neonBlue
7+
8+
override init(size: CGSize) {
9+
super.init(size: size)
10+
backgroundColor = SopaTheme.blackBackground
11+
addBackButton()
12+
addTitle()
13+
addCreditsContent()
14+
}
15+
16+
required init?(coder aDecoder: NSCoder) {
17+
fatalError("init(coder:) has not been implemented")
18+
}
19+
20+
private func addBackButton() {
21+
let side = size.height * 0.08
22+
let backButton = SpriteButton(texture: ButtonTextureFactory.makeCircleButtonTexture(symbolName: "chevron.left", side: side)) {
23+
ResourcesManager.getInstance().storyService?.loadStartMenuScene()
24+
}
25+
backButton.position = CGPoint(x: size.height * 0.057, y: size.height * 0.91)
26+
addChild(backButton)
27+
}
28+
29+
private func addTitle() {
30+
let title = SKLabelNode(fontNamed: "Impact")
31+
title.text = "CREDITS"
32+
title.fontSize = size.height * 0.09
33+
title.fontColor = SopaTheme.titleGray
34+
title.position = CGPoint(x: size.width * 0.5, y: size.height * 0.78)
35+
addChild(title)
36+
}
37+
38+
private func addCreditsContent() {
39+
let content: [(section: String, entries: [String])] = [
40+
("DEVELOPMENT", ["David Schilling - @schillda710", "Raphael Schilling - @ubuntius"]),
41+
("DESIGN", ["Raphael Schilling - @ubuntius"]),
42+
("MUSIC", ["Menu - axtoncrolley on opengameart.org"])
43+
]
44+
45+
var currentY = size.height * 0.70
46+
for block in content {
47+
addSectionLine(text: block.section, y: currentY)
48+
currentY -= size.height * 0.045
49+
50+
for entry in block.entries {
51+
addBodyLine(text: entry, y: currentY)
52+
currentY -= size.height * 0.038
53+
}
54+
currentY -= size.height * 0.028
55+
}
56+
}
57+
58+
private func addSectionLine(text: String, y: CGFloat) {
59+
let line = SKLabelNode(fontNamed: "HelveticaNeue-Bold")
60+
line.horizontalAlignmentMode = .left
61+
line.verticalAlignmentMode = .center
62+
line.text = text
63+
line.fontSize = LabelSizing.fittedLabelFontSize(
64+
text: text,
65+
fontName: "HelveticaNeue-Bold",
66+
preferredSize: size.height * 0.032,
67+
maxWidth: size.width * 0.88
68+
)
69+
line.fontColor = sectionColor
70+
line.position = CGPoint(x: size.width * 0.06, y: y)
71+
addChild(line)
72+
}
73+
74+
private func addBodyLine(text: String, y: CGFloat) {
75+
let line = SKLabelNode(fontNamed: "HelveticaNeue")
76+
line.horizontalAlignmentMode = .left
77+
line.verticalAlignmentMode = .center
78+
line.text = text
79+
line.fontSize = LabelSizing.fittedLabelFontSize(
80+
text: text,
81+
fontName: "HelveticaNeue",
82+
preferredSize: size.height * 0.026,
83+
maxWidth: size.width * 0.90
84+
)
85+
line.fontColor = SopaTheme.bodyText
86+
line.position = CGPoint(x: size.width * 0.09, y: y)
87+
addChild(line)
88+
}
89+
}

0 commit comments

Comments
 (0)